Администрирование сервера: запуск команд от имени других пользователей средствами PHP
Варианты выполнения команд с sudo в PHP под учётной записью www-data
Как настроить выполнение одной конкретной команды от root без ввода пароля?
Наиболее надёжный и контролируемый способ - разрешить в sudoers пользователю www-data запуск строго определённых команд без запроса пароля. После настройки вызов sudo из PHP будет работать без интерактивного взаимодействия.
- Откройте редактор
visudoили создайте файл в/etc/sudoers.d/:
sudo visudo -f /etc/sudoers.d/php-commandsPhp www data sudo (данные php с sudo)
Добавьте строку (замените /usr/bin/systemctl на нужную команду):
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl
- В 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
Проблемы: гонки при одновременном доступе, задержка до минуты, отсутствие обратной связи.