PHP 如何执行系统命令?

在 Linux 上,PHP 执行系统命令主要通过以下几种函数:


✅ PHP 在执行系统命令的常用方式:

函数 描述
exec() 执行外部程序,不输出结果,结果可存入变量。
shell_exec() 执行命令并返回完整输出结果(字符串)。
system() 执行命令并立即输出结果。
passthru() 执行命令并直接输出原始数据,适合二进制输出(如视频、图片处理)。
`command`(反引号) | 和 shell_exec() 类似,执行命令并返回结果。

exec()

string exec ( string $command [, array &$output [, int &$return_var ] )

说明: exec执行系统外部命令时不会输出结果,而是返回结果的最后一行。如果想得到结果,可以使用第二个参数,让其输出到指定的数组。此数组一个记录代表输出的一行。即如果输出结果有20行,则这个数组就有20条记录,所以如果需要反复输出调用不同系统外部命令的结果,最好在输出每一条系统外部命令结果时清空这个数组unset($output),以防混乱。第三个参数用来取得命令执行的状态码,通常 执行成功都是返回0

<?php
    // 输出运行中的 php/httpd 进程的创建者用户名
    // (在可以执行 "whoami" 命令的系统上)
    echo exec('whoami');
?>

system():

string system ( string $command [, int &$return_var ] )

说明: system和exec的区别在于,system在执行系统外部命令时,它执行给定的命令,输出和返回结果。成功则返回命令输出的最后一行, 失败则返回 FALSE。第二个参数是可选的,用来得到命令执行后的状态码。

<?php
    $res = system("pwd",$result);
    print $result;//输出命令的结果状态码
    print $res;//输出命令输出的最后一行
?>

关于第二个参数结果状态码的简单介绍:

如果返回0是运行成功

在Bash中,当错误发生在致命信号时,bash会返回128+signal number做为返回值。

如果找不到命令,将会返回127。

如果命令找到了,但该命令是不可执行的,将返回126。

除此以外,Bash本身会返回最後一个指令的返回值。

若是执行中发生错误,将会返回一个非零的值。

Fatal Signal : 128 + signo

Can't not find command : 127

Can't not execute : 126

Shell script successfully executed : return the last command exit status

Fatal during execution : return non-zero

shell_exec()

string shell_exec ( string $cmd )

说明: 直接执行命令$cmd,将完整的命令输出以字符串的方式返回。如果执行过程中发生错误或者进程不产生输出,则返回 NULL。

所以,使用本函数无法通过返回值检测进程是否成功执行。 如果需要检查进程执行的退出码,请使用 exec() 函数。

<?php
    $output = shell_exec('ls -lart');
    echo $output";
?>

passthru()

void passthru ( string $command [, int &$return_var ] )

说明: passthru与system的区别,passthru直接将结果输出到游览器,不返回任何值,且其可以输出二进制,比如图像数据。第二个参数可选,是状态码。

同 exec() 函数类似, passthru() 函数 也是用来执行外部命令(command)的。 当所执行的 Unix 命令输出二进制数据, 并且需要直接传送到浏览器的时候, 需要用此函数来替代 exec() 或 system() 函数。 常用来执行诸如 pbmplus 之类的可以直接输出图像流的命令。 通过设置 Content-type 为 image/gif, 然后调用 pbmplus 程序输出 gif 文件, 就可以从 PHP 脚本中直接输出图像到浏览器。

<?php
    header("Content-type:image/gif");
    passthru("/usr/bin/ppm2tiff  /usr/share/tk8.4/demos/images/teapot.ppm");
?>

反撇号

原型: 反撇号 执行系统外部命令,相当于 shell_exec

说明: 在使用这种方法执行系统外部命令时,要确保shell_exec函数可用,否则是无法使用这种反撇号执行系统外部命令的。

<?php
    echo `dir`; 
?>

✅ 防止命令注入的措施

PHP 执行系统命令 极易受到命令注入攻击,尤其是当用户输入未经处理被拼接到命令中。

攻击示例(命令注入):

$user = $_GET['name'];
shell_exec("ping $user");  // 用户输入 "127.0.0.1; rm -rf /"

1. 用户输入转义:

escapeshellarg()escapeshellcmd() 是 PHP 提供的两个专门用于避免 命令行注入 的函数,但它们的功能和用途不同。下面我详细解释它们的原理和安全性保障机制。

  • escapeshellarg() —— 用于参数
$user = escapeshellarg("127.0.0.1; rm -rf /");
shell_exec("ping $user");  // 实际执行:ping '127.0.0.1; rm -rf /'; 这是一个合法的参数,而不是两个命令,`rm -rf /` 不会被执行 ✅
  • 它会把参数整体用单引号包裹(即 '参数'

  • 并把内部的单引号(')转义成安全格式(在 Linux 下是 '\'\''

  • 把参数当成纯文本处理,终端不会再解释其中的任何命令字符(如 ;, &, |, > 等)

  • 有效防止 命令拼接注入攻击

  • escapeshellcmd() —— 用于整条命令

$cmd = escapeshellcmd("ls; rm -rf /");
shell_exec($cmd);  // 实际执行:ls\ rm\ -rf\ /
  • 它会在命令中的特殊字符前添加反斜杠 \ 转义,如 &, ;, |, >, <, ! 等。

  • 使这些字符在 shell 中不会被当作命令控制符处理。

  • 注意它不会加引号

  • 不能用于处理用户输入的数据参数!(比如文件名、路径、IP地址)

❌ 错误用法:

$input = $_GET['host'];
shell_exec("ping " . escapeshellcmd($input));  // ❌ 仍然可能注入

✅ 正确做法:

$input = $_GET['host'];
shell_exec("ping " . escapeshellarg($input));  // ✅ 参数应该用 escapeshellarg

🛡️ 总结对比

特性 escapeshellarg() escapeshellcmd()
用途 保护参数 保护命令行(不含参数)
行为 用单引号包裹整个参数 转义命令中的特殊字符
安全性 ✅ 高(建议用于用户输入) ⚠️ 低(不要用在用户参数上)

✅ 最安全用法(推荐模式)

$host = $_GET['host'];                      // 用户输入
$host = escapeshellarg($host);              // 转义成安全参数
$cmd  = '/bin/ping -c 4 ' . $host;          // 拼接命令
$output = shell_exec($cmd);
echo "<pre>$output</pre>";

2. 白名单校验

  • 如果必须运行用户输入的参数,先校验是否在允许的列表中。
$allowed = ['google.com', 'example.com'];
$host = $_GET['host'];
if (in_array($host, $allowed)) {
    $host = escapeshellarg($host);
    shell_exec("ping -c 4 $host");
}

3. 关闭 exec 类函数(高安全场景)

  • php.ini 中禁用高风险函数:
disable_functions = exec,shell_exec,system,passthru,popen,proc_open

4. 使用 sudo 白名单(仅限特定命令)

  • 配置 /etc/sudoers,只允许 PHP 执行有限命令:
www-data ALL=(ALL) NOPASSWD: /usr/bin/safe-command