PHP Conference Japan 2024

escapeshellarg

(PHP 4 >= 4.0.3, PHP 5, PHP 7, PHP 8)

escapeshellarg將字串轉義為可在 shell 中使用的引數

描述

escapeshellarg(字串 $arg): 字串

escapeshellarg() 在字串周圍加上單引號,並將任何現有的單引號加上引號/轉義,讓你可以將字串直接傳遞給 shell 函式,並將其視為單一安全引數。此函式應用於從使用者輸入傳遞到 shell 函式的個別引數。shell 函式包含 exec()system()反引號運算子

在 Windows 上,escapeshellarg() 會將百分比符號、驚嘆號(延遲變數替換)和雙引號替換為空格,並在字串周圍加上雙引號。此外,每個連續的反斜線 (\) 都會額外以一個反斜線進行轉義。

參數

arg

要轉義的引數。

傳回值

轉義後的字串。

範例

範例 1 escapeshellarg() 範例

<?php
system
('ls '.escapeshellarg($dir));
?>

參見

新增註解

使用者貢獻的註解 19 則註解

phil at philfreo dot com
14 年前
當 escapeshellarg() 從 UTF-8 字串中移除我的非 ASCII 字元時,加入以下內容修復了這個問題

<?php
setlocale
(LC_CTYPE, "en_US.UTF-8");
?>
lucgommans.nl
1 年前
這無法防止所有形式的命令注入。

<?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 注入攻擊的欄位,會在任何帶有單引號或引號符號的資料上意外中斷:這只是不好的使用者體驗)。

許多程式允許使用雙連字號 -- 來將位置引數與選項分開。如果你將上述程式碼變更為使用此 exec 行,則不再容易受到攻擊

<?php
// 請注意,在指定我們想要的選項之後有 --
exec("tar cf my.tar -- " . implode(' ', $files_to_archive));
?>

並非每個程式都支援此分隔符,在這種情況下,你需要尋找替代的輸入方法(例如,nmap -iL targets.txt 而不是 nmap 2001:db8::/96)或拒絕以連字號開頭的引數。

當然,理想情況下,人們會透過程式庫使用資料綁定,以避免必須完全進行危險的轉義操作,類似於參數化的 SQL 查詢,但我還沒有看到提到這個注意事項,並認為值得在仍然使用 escapeshellarg() 時新增此說明。
daverandom
5 年前
在 Windows 上,此函式會天真地移除特殊字元,並將其替換為空格。產生的字串始終可以安全地與 exec() 等一起使用,但此操作並非無損 - 包含 " 或 % 的字串不會正確地傳遞到子程序。

在 Windows 上正確轉義 shell 命令並不是一件簡單的事。程式必須考慮兩種不同的轉義機制,它們服務於不同的目的
1) Windows 系統函式 CommandLineToArgV() 使用的約定,子程序使用該約定來解釋命令列字串
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": // 空格和 Tab 字元可以,但必須用引號括起來
$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 跳脫 - 唯一的例外是
當使用具有 bypass_shell 選項的 proc_open() 時。 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
" original: '$badString'\n";
echo
" shell_exec(): " . check($badString, shell_exec($cmd)) . "\n";
echo
" noshell_exec(): " . check($badString, noshell_exec($cmd)) . "\n";
echo
"\n";
}
egorinsk at gmail dot com
17 年前
在 Windows 下,此函式會將字串放入雙引號中,而不是單引號,並將 %(百分比符號) 取代為空格,這就是為什麼無法透過此函式傳遞名稱中帶有百分比符號的檔案名稱的原因。
Anonymous
18 年前
以上大多數的評論都誤解了此函式。它不需要跳脫 '$' 和 '`' 之類的字元 - 它利用了 shell 不會將單引號內的任何字元視為特殊字元 (單引號字元本身除外) 的事實。使用此函式的正確方法是對要作為單一參數傳遞給命令列程式的變數呼叫它 - 您不會在整個命令列上呼叫它。

上面評論如果給定空字串作為輸入,此函式行為不佳的人是對的 - 這是一個錯誤。在這種情況下,它確實應該傳回兩個單引號。
Jasen
6 年前
如果您想要空輸入的空參數

請使用以下形式

escapeshellarg($input)."''"

shell 會將 foo'' 視為 foo,但空輸入將會變成空參數,而不是遺失的參數。
phpman at crustynet dot org dot uk
15 年前
來自 'rmays at castlecomm dot com' 的評論是不正確的:在建構 shell 引數時,單引號內的單引號無法使用反斜線跳脫。此函數的輸出實際上是正確的。它會跳出單引號字串,包含一個帶有反斜線跳脫的單引號,然後繼續單引號字串。請觀察

[shellarg.php]
<?php

system
("echo ' single quote\'d '");
system("echo ' single quote'\''d '");
?>

$ php shellarg.php
sh: -c: 第 0 行:尋找匹配的 `'' 時遇到意外的 EOF
sh: -c: 第 1 行:語法錯誤:意外的檔案結尾
single quote'd
Audun
16 年前
在 Windows 上,% 會被替換成空格的原因是,在 cmd.exe 中無法跳脫或引用它們,以避免環境變數被展開。例如,如果您的引數中有 %path%,它總是會被展開,因此唯一安全的做法是將 % 替換為其他符號。

或者,您可以在呼叫 exec() 之前清除環境,但這會有副作用。
Jannis
2 年前
escapeshellarg() 會根據您的地區設定移除所有無效字元(例如,當 locale/LC_CTYPE 為 UTF-8 時,拉丁文-1 字元會被移除)。

請記住,地區設定的支援取決於您編譯時使用的 C 標準函式庫。這可能會在使用地區設定支援不佳(非 glibc)的標準函式庫的嵌入式系統上導致奇怪的行為。
info at infosoporte dot com
15 年前
如果 escapeshellarg() 函數從給定的字串中移除您的重音符號(例如 á,帶有銳音符號的 a),請確保您的 LC_ALL 變數正確。如果透過網頁使用它,在從您的 shell 使用 export LC_ALL=es_ES.utf8(例如)設定 LC_ALL 之後,您需要重新啟動 Apache 或對應的網頁伺服器。
sblyons+php at gmail dot com
12 年前
如果對序列化的物件使用 escapeshellarg(),請小心。序列化的物件包含空位元組,而 escapeshellarg 會在第一個空位元組停止,因此您將無法收到完整的引數。(我認為這是一個錯誤,但不確定在這種情況下應該怎麼做。也許序列化不應該使用空位元組,但現在為時已晚)。
我發現的在命令列上傳遞序列化物件的解決方法是先將它們 base64_encode(),然後在另一側解碼。
Tony C
2 年前
在使用此函數將檔名傳遞到命令列時,我注意到 shell_exec() 失敗了。經過進一步調查,看來 escapeshellarg() 會從檔名中移除雙空格。

例如

$filename = "my super file.txt"; // 請注意 "my" 後面的雙空格

echo escapeshellarg($filename);

產生
'my super file.txt'

(第二個空格被移除)
crose
4 年前
Ubuntu:想知道為什麼您的系統地區設定(例如 'en_US.UTF-8')沒有繼承到您的 Apache(仍然是 'C')嗎?

檢查 `/etc/apache2/envvars` ... 啟用行 `. /etc/default/locale`
divinity76 at gmail dot com
3 年前
如果您需要在 Windows 上執行時也產生 Linux 引數,請嘗試

<?php

/**
* 無論主機作業系統為何,都使用 linux 跳脫規則來引用引數
* (例如,即使在 Windows 上執行,也會使用 linux 跳脫規則)
*
* @param string $arg
* @throws \InvalidArgumentException 如果引數包含空位元組
* @return string
*/
/*public static*/
function linux_escapeshellarg(string $arg): string
{
if (
false !== strpos($arg, "\x00")) {
throw new
\InvalidArgumentException("引數包含空位元組,無法跳脫空位元組!");
}
return
"'" . strtr($arg, [
"'" => "'\\''"
]) . "'";
}
Anonymous
3 年前
如果您需要在 Windows 上執行時也產生 Linux 引數,請嘗試

<?php

/**
* 無論主機作業系統為何,都使用 linux 跳脫規則來引用引數
* (例如,即使在 Windows 上執行,也會使用 linux 跳脫規則)
*
* @param string $arg
* @throws \InvalidArgumentException 如果引數包含空位元組
* @return string
*/
/*public static*/
function linux_escapeshellarg(string $arg): string
{
if (
false !== strpos($arg, "\x00")) {
throw new
\InvalidArgumentException("引數包含空位元組,無法跳脫空位元組!");
}
return
"'" . strtr($arg, [
"'" => "'\\''"
]) . "'";
}
jon at wroth dot org
10 年前
我為 Windows 想出的 escapeshellarg() 的最佳替代方案是這個
<?php
function w32escapeshellarg($s)
{ return
'"' . addcslashes($s, '\\"') . '"'; }
?>
jrbeaure at uvm dot edu
15 年前
當將包含連字號的 LaTeX 程式碼字串透過此命令逸脫作為 pdflatex 的引數執行時,將會導致失敗。
wijnand at jpresult dot nl
11 年前
如果您需要處理特殊字元,這是此函數的快速而粗略的替代方案。

<?php
/**
* 一個難看的、非 ASCII 字元安全版本的 escapeshellarg() 替代品。
*/
function escapeshellarg_special($file) {
return
"'" . str_replace("'", "'\"'\"'", $file) . "'";
}
?>
vosechu at roman-fleuve dot com
20 年前
如果 escapeshellarg() 在 null 輸入時傳回某些內容,它可能會破壞比它幫助的更多程式。即使是兩個 "'s 或兩個 ''s,此函數也無法按照預期的方式工作(也就是傳回空值)。

但是,大多數人不會在他們的命令中放入 "",但我可以看到在某些時候它可能很有用。
也許命令中會有一個選項,可以傳回我們想要的 null 類型。我可能希望傳回 null 字元,其他人可能想要 '', 而其他人可能根本不想要任何東西。
To Top