Администрирование сервера: запуск команд от имени других пользователей средствами PHP

Раздел: администрирование сервера -> выполнение системных команд

Варианты выполнения команд с sudo в PHP под учётной записью www-data

Как настроить выполнение одной конкретной команды от root без ввода пароля?

Наиболее надёжный и контролируемый способ - разрешить в sudoers пользователю www-data запуск строго определённых команд без запроса пароля. После настройки вызов sudo из PHP будет работать без интерактивного взаимодействия.

  1. Откройте редактор visudo или создайте файл в /etc/sudoers.d/:
sudo visudo -f /etc/sudoers.d/php-commands

Php www data sudo (данные php с sudo)

Добавьте строку (замените /usr/bin/systemctl на нужную команду):

www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl
  1. В PHP-скрипте используйте exec или shell_exec:

<?php
$command = '/usr/bin/sudo /usr/bin/systemctl restart apache2';
exec($command, $output, $return_code);
echo implode("\n", $output);
?>

Пояснение: обязательно указывайте полные пути к sudo и к исполняемой команде. Это исключает подмену через манипуляции с переменной $PATH. Проверяйте код возврата $return_code.

Типичные проблемы:

  • sudo: sorry, you must have a tty to run sudo - отключите требование tty в /etc/sudoers строкой Defaults:www-data !requiretty.
  • sudo: no tty present and no askpass program specified - аналогично, либо используйте -S флаг (но с паролем).
  • Команда не найдена - всегда используйте абсолютные пути.
  • Аргументы, содержащие пробелы или специальные символы, экранируйте escapeshellarg().

Как разрешить выполнение одной команды без sudo через suid-бит?

Создайте небольшой скрипт-обёртку на C, скомпилируйте его, установите бит suid и владельца root. Затем вызывайте его из PHP.


// wrapper.c
#include <stdlib.h>
#include <unistd.h>
int main() {
    setuid(0);
    system("/usr/bin/systemctl restart apache2");
    return 0;
}
gcc -o /usr/local/bin/restart-apache wrapper.c
sudo chown root:root /usr/local/bin/restart-apache
sudo chmod u+s /usr/local/bin/restart-apache

PHP-вызов:

exec('/usr/local/bin/restart-apache', $out);

Недостаток: любая ошибка в C-коде может привести к уязвимости. Не рекомендуется для команд с переменными аргументами.

Как выполнить команду с sudo, если нет возможности менять sudoers?

Можно использовать proc_open для передачи пароля через stdin. Однако это крайне небезопасно - пароль виден в процессах и может быть перехвачен.


$descriptorspec = array(
    0 => array("pipe", "r"),
    1 => array("pipe", "w")
);
$process = proc_open('sudo -S /usr/bin/systemctl status apache2', $descriptorspec, $pipes);
if (is_resource($process)) {
    fwrite($pipes[0], 'your_password_here' . "\n");
    fclose($pipes[0]);
    echo stream_get_contents($pipes[1]);
    fclose($pipes[1]);
    proc_close($process);
}

Никогда не используйте в production!

Как выполнить привилегированную команду асинхронно (через очередь)?

Записывайте задание в общую очередь (например, Redis + beanstalkd). Отдельный воркер, запущенный от root, забирает задание и выполняет. PHP только публикует сообщение.


// PHP: публикация задания
$pheanstalk = new Pheanstalk\Pheanstalk('127.0.0.1');
$pheanstalk->useTube('system-tasks')->put(json_encode(['cmd' => 'restart apache2']));

// Воркер на Python (запущен от root)
import beanstalkc
import subprocess
beanstalk = beanstalkc.Connection()
beanstalk.watch('system-tasks')
while True:
    job = beanstalk.reserve()
    data = json.loads(job.body)
    subprocess.run(['systemctl', 'restart', 'apache2'])
    job.delete()

Проблемы: сложность инфраструктуры, задержка выполнения.

Как использовать CGI-скрипт с suPHP или mod_su для изменения пользователя?

Настройте suPHP так, чтобы скрипты в определённой папке выполнялись от root или другого привилегированного пользователя. Затем вызывайте системные команды напрямую без sudo.


// /var/www/admin/system.php
<?php
// теперь скрипт работает от root (если настроено suPHP)
exec('systemctl restart apache2');
?>

Настройка suPHP выходит за рамки статьи, но это легитимный способ при изолированных конфигурациях.

Расширенные примеры и детальные пояснения

1. Полный цикл настройки sudoers с проверками в PHP

Пример

// /etc/sudoers.d/php-restart
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl *

Пример PHP-функции с проверкой кода возврата и экранированием:

Пример

<?php
function runSudoCommand(string $command): string {
    $sudo = '/usr/bin/sudo';
    $fullCmd = escapeshellcmd($sudo . ' ' . $command);
    exec($fullCmd, $output, $ret);
    if ($ret !== 0) {
        throw new RuntimeException('Command failed with code ' . $ret);
    }
    return implode("\n", $output);
}

try {
    echo runSudoCommand('/usr/bin/systemctl status nginx');
} catch (Exception $e) {
    echo 'Ошибка: ' . $e->getMessage();
}
?>
● nginx.service - The NGINX HTTP and reverse proxy server
   Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2025-03-24 10:00:00 UTC; 1h ago

Важно: шаблон /usr/bin/systemctl * в sudoers разрешает любую подкоманду. Для строгой безопасности укажите конкретные аргументы: /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginx и т.д.

2. Использование proc_open с sudo и захватом STDERR

Пример

$descriptorspec = array(
    0 => array('pipe', 'r'),
    1 => array('pipe', 'w'),
    2 => array('pipe', 'w')  // STDERR
);
$process = proc_open('sudo -k -S /usr/sbin/iptables -L -n 2>&1', $descriptorspec, $pipes);
if (is_resource($process)) {
    // Если требуется пароль (не рекомендуется), передаём его:
    // fwrite($pipes[0], "password\n");
    fclose($pipes[0]);
    $stdout = stream_get_contents($pipes[1]);
    fclose($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);
    fclose($pipes[2]);
    proc_close($process);
    echo "STDOUT:\n$stdout";
    echo "STDERR:\n$stderr";
}
STDOUT:
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
...
STDERR:
(пусто, если пароль не затребован)

Примечание: флаг -k сбрасывает кэшированные учётные данные sudo. Вместе с -S позволяет ввести пароль через STDIN.

3. Ограничение sudoers для конкретных аргументов и пользователей

Пример

# Разрешить www-data выполнять только 'restart' и 'status' для apache2
www-data ALL=(root) NOPASSWD: /usr/bin/systemctl restart apache2, /usr/bin/systemctl status apache2

Пример PHP с проверкой аргументов на стороне скрипта (дополнительная защита):

Пример

$allowed = ['restart', 'status'];
$action = $_GET['action'] ?? '';
if (!in_array($action, $allowed, true)) {
    die('Недопустимое действие');
}
exec('/usr/bin/sudo /usr/bin/systemctl ' . escapeshellarg($action) . ' apache2', $out);
print_r($out);
Array
(
    [0] => ● apache2.service - The Apache HTTP Server
       Loaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor preset: enabled)
       Active: active (running) since Mon 2025-03-24 10:00:00 UTC; 2h ago
)

4. Использование setuid-программы с параметрами (более гибкий вариант)

Создадим C-программу, принимающую аргумент:

Пример

// run_as_root.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <command>\n", argv[0]);
        return 1;
    }
    setuid(0);
    // Простейшая защита: разрешаем только известные команды
    char *allowed[] = {"systemctl", "reboot", NULL};
    int ok = 0;
    for (int i = 0; allowed[i] != NULL; i++) {
        if (strcmp(argv[1], allowed[i]) == 0) { ok = 1; break; }
    }
    if (!ok) {
        fprintf(stderr, "Command not allowed\n");
        return 1;
    }
    // Выполняем команду с аргументами (argv[2] и далее)
    execvp(argv[1], &argv[1]);
    perror("execvp");
    return 1;
}
Пример
gcc -o /usr/local/bin/run_as_root run_as_root.c
sudo chown root:root /usr/local/bin/run_as_root
sudo chmod u+s /usr/local/bin/run_as_root

PHP-вызов:

Пример
exec('/usr/local/bin/run_as_root systemctl restart apache2', $out);

Результат: команда выполняется от root без sudo. Проблема: потенциальные уязвимости при неправильной проверке аргументов.

5. Асинхронное выполнение с записью в файл и cron

Пример

// PHP записывает команду в файл-очередь
$task = '/usr/bin/systemctl restart mysql';
file_put_contents('/var/spool/system-queue/commands.txt', $task . PHP_EOL, FILE_APPEND);

В crontab (от root) задание, которое каждую минуту проверяет файл:

Пример
* * * * * /usr/local/bin/process-queue.sh
Пример

#!/bin/bash
QUEUE=/var/spool/system-queue/commands.txt
if [ -s "$QUEUE" ]; then
    head -1 "$QUEUE" | while read cmd; do
        eval $cmd
    done
    sed -i '1d' "$QUEUE"
fi

Проблемы: гонки при одновременном доступе, задержка до минуты, отсутствие обратной связи.

данные PHP с sudo - comments

En
Php www data sudo (php)