当 escapeshellarg() 从 UTF-8 字符串中剥离我的非 ASCII 字符时,添加以下内容解决了问题
<?php
setlocale(LC_CTYPE, "en_US.UTF-8");
?>
(PHP 4 >= 4.0.3, PHP 5, PHP 7, PHP 8)
escapeshellarg — 转义用作 shell 参数的字符串
escapeshellarg() 在字符串周围添加单引号,并对任何现有的单引号进行引用/转义,允许您将字符串直接传递给 shell 函数,并将其视为单个安全参数。此函数应用于转义来自用户输入的 shell 函数的单个参数。shell 函数包括 exec()、system() 和 反引号运算符。
在 Windows 上,escapeshellarg() 将百分号、感叹号(延迟变量替换)和双引号替换为空格,并在字符串周围添加双引号。此外,每一段连续的反斜杠 (\
) 都会被一个额外的反斜杠转义。
arg
将被转义的参数。
转义后的字符串。
示例 #1 escapeshellarg() 示例
<?php
system('ls '.escapeshellarg($dir));
?>
当 escapeshellarg() 从 UTF-8 字符串中剥离我的非 ASCII 字符时,添加以下内容解决了问题
<?php
setlocale(LC_CTYPE, "en_US.UTF-8");
?>
这并不能防止所有形式的命令注入。
<?php
// GET /example.php?file[]=x&file[]=-I&file[]=bash%20-c%20touch\%20/tmp/lucwashere
$files_to_archive = [];
foreach ($_GET['file'] as $file) {
$files_to_archive[] = escapeshellarg($file);
}
exec("tar cf my.tar " . implode(' ', $files_to_archive));
?>
尽管进行了正确的转义以防止 shell 注入,但这仍将运行指定的代码。这些参数指示 tar 在 bash 中运行命令。然后,您可以检查 /tmp 目录以验证代码是否已运行。
当然,攻击者会用更恶意的变体替换它。对于黑盒漏洞测试,可以安全地使用几秒钟的睡眠来确定参数是否易受攻击。
此处的漏洞在于,tar 与几乎所有其他程序一样,会将以连字符开头的参数解释为修改其行为的选项。许多程序,如 tcpdump、man、zip、gpg、tar 等,都有一个选项可以执行另一个命令。即使您使用没有(并且永远不会有)此类执行选项的程序,其功能也会受到指定额外选项的影响,无论是故意还是偶然,因为某些字符串恰好以连字符开头(类似于如何易受 SQL 注入影响的字段在任何包含撇号或引号符号的数据上意外中断:这只是糟糕的 UX)。
许多程序允许使用双连字符 -- 将位置参数与选项分开。如果将上述代码更改为使用此 exec 行,则它将不再易受攻击
<?php
// 注意我们想要指定选项后的 --
exec("tar cf my.tar -- " . implode(' ', $files_to_archive));
?>
并非所有程序都支持此分隔符,在这种情况下,您需要找到替代输入方法(例如,nmap -iL targets.txt 而不是 nmap 2001:db8::/96)或拒绝以连字符开头的参数。
当然,理想情况下,人们会通过库使用数据绑定来避免完全执行危险的转义操作,类似于参数化 SQL 查询,但我还没有看到提及此警告,并且认为在仍然使用 escapeshellarg() 时值得添加。
在 Windows 上,此函数会天真地剥离特殊字符并将其替换为空格。生成的字符串始终可以安全地与 exec() 等一起使用,但操作并非无损 - 包含 " 或 % 的字符串不会正确传递到子进程。
在 Windows 上正确转义 shell 命令并非易事。程序必须考虑两种不同的转义机制,它们具有不同的用途
1) 子进程用于解释命令行字符串的 CommandLineToArgV() windows 系统函数使用的约定
2) cmd.exe 用于转义 shell 元字符(例如输出重定向控件)的约定
所有命令都应针对 CommandLineToArgV() 进行转义 - 此机制在每个参数附加到命令行字符串之前单独应用于每个参数。生成的字符串可以安全地与 CreateProcess() 系列系统函数一起使用。但是...
在几乎所有从 Windows 上的 PHP 创建子进程的情况下,都是通过间接调用 cmd.exe 来完成的 - 这是为了能够使用 shell 功能,例如 I/O 重定向和环境变量替换。因此,整个命令字符串必须进一步针对 cmd.exe 进行转义。如果执行的命令包含通过 cmd.exe 的进一步间接调用,则每个子命令必须针对每个间接级别再次进行转义。
以下函数可用于正确转义字符串,以便安全地将其传递到子进程
<?php
/**
* 根据 CommandLineToArgV() 对单个值进行转义
* https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85)
*/
function escape_win32_argv(string $value): string
{
static $expr = '(
[\x00-\x20\x7F"] # 控制字符、空格或双引号
| \\\\++ (?=("|$)) # 后跟引号或结尾的反斜杠
)ux';
if ($value === '') {
return '""';
}
$quote = false;
$replacer = function($match) use($value, &$quote) {
switch ($match[0][0]) { // 只检查匹配项的第一个字节
case '"': // 双引号需要转义并加引号
$match[0] = '\\"';
case ' ': case "\t": // 空格和制表符可以保留,但需要加引号
$quote = true;
return $match[0];
case '\\': // 如果加引号,则匹配的反斜杠需要转义
return $match[0] . $match[0];
default: throw new InvalidArgumentException(sprintf(
"偏移量 %d 处的无效字节:0x%02X",
strpos($value, $match[0]), ord($match[0])
));
}
};
$escaped = preg_replace_callback($expr, $replacer, (string)$value);
if ($escaped === null) {
throw preg_last_error() === PREG_BAD_UTF8_ERROR
? new InvalidArgumentException("无效的 UTF-8 字符串")
: new Error("PCRE 错误: " . preg_last_error());
}
return $quote // 仅在需要时加引号
? '"' . $escaped . '"'
: $value;
}
/** 使用 ^ 转义 cmd.exe 元字符 */
function escape_win32_cmd(string $value): string
{
return preg_replace('([()%!^"<>&|])', '^$0', $value);
}
/** 类似 shell_exec(),但绕过 cmd.exe */
function noshell_exec(string $command): string
{
static $descriptors = [['pipe', 'r'],['pipe', 'w'],['pipe', 'w']],
$options = ['bypass_shell' => true];
if (!$proc = proc_open($command, $descriptors, $pipes, null, null, $options)) {
throw new \Error('创建子进程失败');
}
fclose($pipes[0]);
$result = stream_get_contents($pipes[1]);
fclose($pipes[1]);
stream_get_contents($pipes[2]);
fclose($pipes[2]);
proc_close($proc);
return $result;
}
// 用法
$badString = '包含 "C:\\quotes\\" 或恶意 %OS% 内容的字符串 \\';
$cmdParts = [
'php',
'-d', 'display_errors=1', '-d', 'error_reporting=-1',
'-r', 'echo $argv[1];',
$badString // 子进程 $argv[1] 值
];
/* 典型方法 - 在 POSIX shell 上运行良好,但在 Windows 上完全错误
*/
$wrong = implode(' ', array_map('escapeshellarg', $cmdParts));
/* 始终单独转义每个参数 */
$escaped = implode(' ', array_map('escape_win32_argv', $cmdParts));
/* 在几乎所有情况下,也需要针对 cmd.exe 进行转义 - 唯一的例外是
使用 proc_open() 并带有 bypass_shell 选项。cmd 不会单独处理
参数,因此可以转义整个命令行字符串,
无需单独处理参数 */
$cmd = escape_win32_cmd($escaped);
$cmds = [
'escapeshellarg() - 错误' => $wrong,
'escape_win32_argv() - bypass_shell 正确' => $escaped,
'escape_win32_cmd(escape_win32_argv()) - 其他情况正确' => $cmd,
];
function check($original, $received)
{
$match = $original === $received ? '=' : 'X';
return "$match '$received'";
}
foreach ($cmds as $description => $cmd) {
echo "$description\n";
echo " $cmd\n";
echo " 原值:'$badString'\n";
echo " shell_exec(): " . check($badString, shell_exec($cmd)) . "\n";
echo " noshell_exec(): " . check($badString, noshell_exec($cmd)) . "\n";
echo "\n";
}
在 Windows 中,此函数将字符串放入双引号而不是单引号,并将 %(百分号) 替换为空格,因此无法通过此函数传递名称中包含百分号的文件名。
上面大多数评论都误解了此函数。它不需要转义 '$' 和 '`' 等字符 - 它利用了 shell 在单引号内不将任何字符视为特殊字符的事实(单引号字符本身除外)。正确使用此函数的方法是将其应用于打算作为单个参数传递给命令行程序的变量 - 您不应该将其应用于整个命令行。
上面评论说如果此函数接收空字符串作为输入则行为异常的人是正确的 - 这是一个 bug。在这种情况下,它确实应该返回两个单引号。
'rmays at castlecomm dot com' 的评论是不正确的:在构造 shell 参数时,单引号字符串内部不能使用反斜杠转义单引号。此函数的输出实际上是正确的。它跳出单引号字符串,包含一个带有反斜杠转义的字面单引号,然后恢复单引号字符串。观察
[shellarg.php]
<?php
system("echo ' single quote\'d '");
system("echo ' single quote'\''d '");
?>
$ php shellarg.php
sh: -c: line 0: unexpected EOF while looking for matching `''
sh: -c: line 1: syntax error: unexpected end of file
single quote'd
在 Windows 上,% 被替换为空格的原因是 cmd.exe 无法转义或引用它们以防止环境变量扩展。例如,如果你的参数中包含 %path% ,它将始终被扩展,因此唯一安全的方法是将 % 替换为其他字符。
或者,您可以在调用 exec() 之前清空环境,但这会产生副作用。
escapeshellarg() 将根据您的区域设置删除所有无效字符(例如,当区域设置/LC_CTYPE 为 UTF-8 时,拉丁文-1 字符会被删除)。
请记住,区域设置支持取决于您在编译时使用的 C 标准库。这可能会导致在使用标准库(除了 glibc 之外的)区域设置支持较差的嵌入式系统上出现奇怪的行为。
如果 escapeshellarg() 函数从给定的字符串中删除了您的变音符号(如 á,带“重音”的 a),请确保您的 LC_ALL 变量正确。如果通过 Web 使用它,则需要在使用 export LC_ALL=es_ES.utf8(例如)从您的 shell 设置 LC_ALL 后重新启动 Apache 或相应的 Web 服务器。
在序列化对象上使用 escapeshellarg() 时要注意。序列化对象包含空字节,而 escapeshellarg 在遇到第一个空字节时就会停止,因此您不会收到完整的参数。(我认为这是一个错误,尽管不确定在这种情况下应该怎么做。可能序列化不应该使用空字节,但现在为时已晚)。
我发现的解决方法是在命令行上传递序列化对象之前先对其进行 base64_encode(),然后在另一端解码。
在使用此函数将文件名传递到命令行时,我注意到 shell_exec() 失败了。经过进一步调查,发现 escapeshellarg() 从文件名中删除了双空格。
例如
$filename = "my super file.txt"; // 注意“my”后面的双空格
echo escapeshellarg($filename);
产生
'my super file.txt'
(第二个空格被删除了)
Ubuntu:想知道为什么您的系统区域设置(例如“en_US.UTF-8”)没有继承到您的 Apache(仍然是“C”)?
检查 `/etc/apache2/envvars` ... 激活行 `. /etc/default/locale`
如果您需要即使在 Windows 上运行时也要生成 Linux 参数,请尝试
<?php
/**
* 使用 Linux 转义规则引用参数,无论主机操作系统是什么
* (例如,即使在 Windows 上运行,它也将使用 Linux 转义规则)
*
* @param string $arg
* @throws \InvalidArgumentException if argument contains null bytes
* @return string
*/
/*public static*/ function linux_escapeshellarg(string $arg): string
{
if (false !== strpos($arg, "\x00")) {
throw new \InvalidArgumentException("参数包含空字节,无法转义空字节!");
}
return "'" . strtr($arg, [
"'" => "'\\''"
]) . "'";
}
如果您需要即使在 Windows 上运行时也要生成 Linux 参数,请尝试
<?php
/**
* 使用 Linux 转义规则引用参数,无论主机操作系统是什么
* (例如,即使在 Windows 上运行,它也将使用 Linux 转义规则)
*
* @param string $arg
* @throws \InvalidArgumentException if argument contains null bytes
* @return string
*/
/*public static*/ function linux_escapeshellarg(string $arg): string
{
if (false !== strpos($arg, "\x00")) {
throw new \InvalidArgumentException("参数包含空字节,无法转义空字节!");
}
return "'" . strtr($arg, [
"'" => "'\\''"
]) . "'";
}
我为 Windows 想出的 escapeshellarg() 的最佳替代方案是这个
<?php
function w32escapeshellarg($s)
{ return '"' . addcslashes($s, '\\"') . '"'; }
?>
如果需要处理特殊字符,这里有一个此函数的快速简易替换。
<?php
/**
* escapeshellarg() 的一个丑陋的、不安全的非 ASCII 字符替换。
*/
function escapeshellarg_special($file) {
return "'" . str_replace("'", "'\"'\"'", $file) . "'";
}
?>
如果 escapeshellarg() 在空输入上返回了一些内容,它可能会破坏比它帮助的程序更多。即使是两个“'”或两个“''”,此函数也不会按预期工作(即,不返回任何内容)。
但是,大多数人不会在他们的命令中放入"",但同时我也可以理解它在某些情况下可能很有用。
也许命令中可以有一个选项,可以返回我们想要的空类型。我可能希望返回空字符,其他人可能想要'',而其他人可能根本不想要任何内容。