参数注入漏洞是指,在执行命令的时候,用户控制了命令中的某个参数,并通过一些危险的参数功能,达成攻击的目的。
我们从gitlist说起,gitlist是一款使用PHP开发的图形化git仓库查看工具。在其0.6.0版本中,存在一处命令参数注入问题,可以导致远程命令执行漏洞。
在用户对仓库中代码进行搜索的时候,gitlist将调用git grep
命令:
<?php
public function searchTree($query, $branch)
{
if (empty($query)) {
return null;
}
$query = escapeshellarg($query);
try {
$results = $this->getClient()->run($this, "grep -i --line-number {$query} $branch");
} catch (\RuntimeException $e) {
return false;
}
其中,$query
是搜索的关键字,$branch
是搜索的分支。
如果用户输入的$query
的值是--open-files-in-pager=id;
,将可以执行id
命令:
导致这个漏洞的原因,有几点:
escapeshellarg
函数的误解,造成参数注入git grep
的参数--open-files-in-pager
的值,将被直接执行理论上,在经过$query = escapeshellarg($query);
处理后,$query
将变成一个由单引号包裹的字符串。但不出漏洞的前提是,这个字符串应该出现在“参数值”的位置,而不是出现在参数选项(option)中。
我们可以试一下如下命令:
git grep -i --line-number -e '--open-files-in-pager=id;' master
如上图,我将$query
放在了-e
参数的值的位置,此时它就仅仅是一个字符串而已,并不会被当成参数--open-files-in-pager
。
这应该作为本漏洞的最佳修复方法,也是git官方对pattern可能是用户输入的情况的一种解决方案(以下说明来自man-page):
-e
The next parameter is the pattern. This option has to be used for patterns starting with - and should be used in scripts passing user input to grep. Multiple patterns are combined by
or.
当然,gitlist的开发者用了另一种修复方案:
<?php
public function searchTree($query, $branch)
{
if (empty($query)) {
return null;
}
$query = preg_replace('/(--?[A-Za-z0-9\-]+)/', '', $query);
$query = escapeshellarg($query);
try {
$results = $this->getClient()->run($this, "grep -i --line-number -- {$query} $branch");
} catch (\RuntimeException $e) {
return false;
}
首先用preg_replace
将-
开头的非法字符移除,然后将$query
拼接在--
的后面。
在命令行解析器中,--
的意思是,此后的部分不会再包含参数选项(option):
A -- signals the end of options and disables further option processing. Any arguments after the -- are treated as filenames and arguments. An argument of - is equivalent to --.
If arguments remain after option processing, and neither the -c nor the -s option has been supplied, the first argument is assumed to be the name of a file containing shell commands. If bash is invoked in this fashion, $0 is set to the name of the file, and the positional parameters are set to the remaining arguments. Bash reads and executes commands from this file, then exits. Bash's exit status is the exit status of the last command executed in the script. If no commands are executed, the exit status is 0. An attempt is first made to open the file in the current directory, and, if no file is found, then the shell searches the directories in PATH for the script.
举个简单的例子,如果我们需要查看一个文件名是--name
的文件,我们就不能用cat --name
来读取,也不能用cat '--name'
,而必须要用cat -- --name
。从这个例子也能看出,单引号并不是区分一个字符串是“参数值”或“选项”的标准。
所以官方这个修复方案也是可以接受的,只不过第一步的preg_replace
有点影响正常搜索功能。
熟悉PHP语言的同学一定对PHP执行命令的方法感受深刻,PHP内置的命令执行函数(如shell_exec
、system
),都只接受一个“字符串”作为参数。而在内核中,这个字符串将被直接作为一条shell命令来调用,这种情况下就极为容易出现命令注入漏洞。
由于这个特点,PHP特别准备了两个过滤函数:
二者分工不同,前者为了防止用户利用shell的一些技巧(如分号、反引号等),执行其他命令;后者是为了防止用户的输入逃逸出“参数值”的位置,变成一个“参数选项”。
但我在0x02中也已经说清楚了,如果开发者在拼接命令的时候,将$query
直接给拼接在“参数选项”的位置上,那用escapeshellarg也就没任何效果了。
Java、Python等语言,执行命令的方法相对来说更加优雅:
import subprocess
query = 'id'
r = subprocess.run(['git', 'grep', '-i', '--line-number', query, 'master'], cwd='/tmp/vulhub')
默认情况下,python的subprocess接受的是一个列表。我们可以将用户输入的query放在列表的一项,这样也就避免了开发者手工转义query的工作,也能从根本上防御命令注入漏洞。但可惜的是,python帮开发者做的操作,也仅仅相当于是PHP中的escapeshellarg。我们可以试试令query等于--open-files-in-pager=id;
:
可见,仍然是存在参数注入漏洞的。原因还是0x02中说的原因,你把query放在了“参数选项”的位置上,无论怎么过滤,或者换成其他语言,都不可能解决问题。
参数注入的例子还比较多,因为大部分的开发者都能理解命令注入的原理,但处理了命令注入后,往往都会忽略参数注入的问题。
最典型是案例是Wordpress PwnScriptum漏洞,PHP mail函数的第五个参数,允许直接注入参数,用户通过注入-X
参数,导致写入任意文件,最终getshell。
另一个典型的例子是php-cgi CVE-2012-1823 ,在cgi模式中,用户传入的querystring将作为cgi的参数传给php-cgi命令。而php-cgi命令可以用-d参数指定配置项,我们通过指定auto_prepend_file=php://input
,最终导致任意代码执行。
客户端上也出现过类似的漏洞,比如Electron CVE-2018-1000006,我们通过注入参数--gpu-launcher=cmd.exe /c start calc
,来让electron内置的chromium执行任意命令。electron的最早给出的缓解措施也是在拼接点前面加上“--”。