5.2 代码执行漏洞
代码执行漏洞是指应用程序本身过滤不严,用户可以通过请求将代码注入到应用中执行。说得好理解一点,类似于SQL注入漏洞,可以把SQL语句注入到SQL服务执行,而PHP代码执行漏洞则是可以把代码注入应用中最终到WebServer去执行。这样的漏洞如果没有特殊的过滤,相当于直接有一个Web后门存在,该漏洞主要由eval()、assert()、preg_replace()、call_user_func()、call_user_func_array()、array_map()等函数的参数过滤不严格导致,另外还有PHP的动态函数($a($b))也是目前出现比较多的。
5.2.1 挖掘经验
eval()和assert()函数导致的代码执行漏洞大多是因为载入缓存或者模板以及对变量的处理不严格导致,比如直接把一个外部可控的参数拼接到模板里面,然后调用这两个函数去当成PHP代码执行。
preg_replace()函数的代码执行需要存在/e参数,这个函数原本是用来处理字符串的,因此漏洞出现最多的是在对字符串的处理,比如URL、HTML标签以及文章内容等过滤功能。
call_user_func()和call_user_func_array()函数的功能是调用函数,多用在框架里面动态调用函数,所以一般比较小的程序出现这种方式的代码执行会比较少。array_map()函数的作用是调用函数并且除第一个参数外其他参数为数组,通常会写死第一个参数,即调用的函数,类似这三个函数功能的函数还有很多。
除了上面这些函数导致的代码执行漏洞,还有一类非常常见的是动态函数的代码执行,比如下面这样的写法:
$_GET ( $_POST["xx"] )
基于这种写法变形出来的各种异形,经常被用来当作Web后门使用,可以看到这里的PHP函数是从$_GET变量当做字符串传入进来的,这是PHP的一个特性。
5.2.1.1 代码执行函数
PHP代码执行有多种利用方式,但目前见得最多的还是由于函数的使用不当导致的,这类函数还不少,有eval()、assert()、preg_replace()、call_user_func()、call_user_func_array()以及array_map()等,下面我们来详细看看各自产生漏洞的原理和利用方式吧。
1.eval和assert函数
这两个函数原本的作用就是用来动态执行代码,所以它们的参数直接就是PHP代码,我们来看看是怎么使用的,测试代码如下:
< ? php
$a='aaa' ;
$b='bbb' ;
eval ( '$a=$b ; ' );
var_dump ( $a );
测试截图如图5-7所示。
图 5-7
2.preg_replace函数
preg_replace函数的作用是对字符串进行正则处理,我们在上面的挖掘经验已经介绍了,它经常会出现漏洞的位置,下面我们来看看它在什么情况下才会出现代码执行漏洞。
它的参数和返回如下:
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [ , int $limit = -1 [ , int &$count ]] )
这段代码的含义是搜索$subject中匹配$pattern的部分,以$replacement进行替换,而当$pattern处即第一个参数存在e修饰符时,$replacement的值会被当成PHP代码来执行,我们来看一个简单的例子(1.php)。
< ? php
preg_replace ( "/\[ ( .* ) \]/e" , '\\1' , $_GET['str'] );? >
正则的意思是从$_GET['str']变量里搜索中括号[]中间的内容作为第一组结果,preg_replace()函数第二个参数为'\\1'代表这里用第一组结果填充,这里是可以直接执行代码的,所以当我们请求/1.php?str=[phpinfo()]时,则执行代码phpinfo(),结果如图5-8所示。
图 5-8
3.调用函数过滤不严
call_user_func()和array_map()等数十个函数有调用其他函数的功能,其中的一个参数作为要调用的函数名,那如果这个传入的函数名可控,那就可以调用意外的函数来执行我们想知道的代码,也就是存在代码执行漏洞。
我们用call_user_func()函数来举例,函数的作用是调用函数并且第二个参数作为要调用的函数的参数,官方说明如下:
mixed call_user_func ( callable $callback [ , mixed $parameter [ , mixed $... ]] )
该函数第一个参数为回调函数,后面的参数为回调函数的参数,测试代码如下:
< ? php
$b="phpinfo () " ;
call_user_func ( $_GET['a'] , $b );? >
当请求1.php?a=assert的时候,则调用了assert函数,并且将phpinfo()作为参数传入,如图5-9所示。
图 5-9
同类的函数还有如下这些:
call_user_func ()、 call_user_func_array ()、 array_map ()
usort ()、 uasort ()、 uksort ()、 array_filter ()
array_reduce ()、 array_diff_uassoc ()、 array_diff_ukey ()
array_udiff ()、 array_udiff_assoc ()、 array_udiff_uassoc ()
array_intersect_assoc ()、 array_intersect_uassoc ()
array_uintersect ()、 array_uintersect_assoc ()
array_uintersect_uassoc ()、 array_walk ()、 array_walk_recursive ()
xml_set_character_data_handler ()、 xml_set_default_handler ()
xml_set_element_handler ()、 xml_set_end_namespace_decl_handler ()
xml_set_external_entity_ref_handler ()、 xml_set_notation_decl_handler ()
xml_set_processing_instruction_handler ()
xml_set_start_namespace_decl_handler ()
xml_set_unparsed_entity_decl_handler ()、 stream_filter_register ()
set_error_handler ()、 register_shutdown_function ()、 register_tick_function ()
5.2.1.2 动态函数执行
由于PHP的特性原因,PHP的函数可以直接由字符串拼接,这导致了PHP在安全上的控制又加大了难度,比如增加了漏洞数量和提高了PHP后门的查杀难度。要找漏洞就要先理解为什么程序代码要这么写,不少知名程序中也用到了动态函数的写法,这种写法跟使用call_user_func的初衷是一样的,大多用在框架里,用来更简单更方便地调用函数,但是一旦过滤不严格就会造成代码执行漏洞。
PHP动态函数写法为“变量(参数)”,我们来看一个动态函数后门的写法:
< ? php
$_GET['a'] ( $_GET['b'] );? >
代码的意思是接收GET请求的a参数,作为函数,b参数作为函数的参数。当请求a参数值为assert,b参数值为phpinfo()的时候打印出phpinfo信息,请求如下:
http : //127.0.0.1/test/1.php ? a=assert&b=phpinfo ()
执行结果如图5-10所示。
图 5-10
要挖掘这种形式的代码执行漏洞,需要找可控的动态函数名。
5.2.1.3 Thinkphp代码执行漏洞分析
要分析代码执行的案例,在Java界来说就是Struts2的代码执行了,不过在PHP领域,国内影响比较大的代码执行漏洞非thinkphp框架URL解析的代码执行漏洞莫属,这个漏洞的影响力,做渗透测试的安全人员应该比较清楚,在国内还是会经常遇到这个漏洞的。
下面我们来分析这个漏洞的原理,thinkphp框架的GET参数以index.php/a/b/c的形式传递,程序在获取参数之前需要先解析URL,漏洞就发生在解析URL的地方,官方补丁对比地址如下:
https://code.google.com/p/thinkphp/source/diff?spec=svn2904&r=2838&format=side&path=/trunk/ThinkPHP/Lib/Core/Dispatcher.class.php。
漏洞出现在/ThinkPHP/Lib/Core/Dispatcher.class.php文件的dispatch()函数,为了节省篇幅,这里只贴出关键代码:
$depr = C ( 'URL_PATHINFO_DEPR' );
if (! empty ( $_SERVER['PATH_INFO'] )) {
tag ( 'path_info' );
if ( C ( 'URL_HTML_SUFFIX' )) {
$_SERVER['PATH_INFO'] = preg_replace ( '/\.'.trim ( C ( 'URL_HTML_SUFFIX' ), '.' ) .'$/i' , '' , $_SERVER['PATH_INFO'] );
}
if (! self :: routerCheck ()) { // 检测路由规则 如果没有则按默认规则调度 URL
$paths = explode ( $depr , trim ( $_SERVER['PATH_INFO'] , '/' ));
/***** 省略 ****/
$var[C ( 'VAR_ACTION' ) ] = array_shift ( $paths );
// 解析剩余的 URL 参数
$res = preg_replace ( '@ ( \w+ ) '.$depr.' ( [^'.$depr.'\/]+ ) @e' , '$var [\'\\1\']="\\2" ; ' , implode ( $depr , $paths ));
$_GET = array_merge ( $var , $_GET );
}
可以看到这里使用preg_replace()函数,我们在前面已经介绍了关于这个函数的代码执行漏洞,这个函数里面的变量为$depr和$paths,代码中的这句话:
$depr = C ( 'URL_PATHINFO_DEPR' );
是取得配置中的参数分隔符,下面这句话:
$paths = explode ( $depr , trim ( $_SERVER['PATH_INFO'] , '/' ));
则是从$_SERVER['PATH_INFO']中以$depr为分隔符分割后的数组,而后面又用implode()函数还原成字符串才带入preg_replace()函数,关键在于:
'$var[\'\\1\']="\\2" ; '
代码的意思是,把正则匹配出来的参数1初始化到$var变量中,并且赋值为参数2的值,问题是这段代码在赋值的时候使用的是双引号("),在PHP中,如果字符串使用双引号括起来,中间的变量是会正常解析的,如:
< ? php
$a=1 ;
echo "$a" ;? >
会输出1,而不是$a,利用这个特性,再结合PHP可变变量即可执行任意代码,最终EXP为:
/index.php/module/action/param1/${@phpinfo () }
5.2.2 漏洞防范
采用参数白名单过滤,在可预测满足正常业务的参数情况下,这是非常实用的方式,这里的白名单并不是说完全固定为参数,因为在eval()、assert()和preg_replace()函数的参数中大部分是不可预测一字不差的,我们可以结合正则表达式来进行白名单限制,用上面的thinkphp来举例,如果我们事先已经知道这个URL里面的第二个参数值由数字构成即可满足业务需求,则可以在正则里用\d+来限制第二个参数内容,这样相对更加安全,用代码举例更加清晰易懂,代码如下:
< ? php
preg_replace ( '/ ( \w+ ) \| ( .* ) /ie' , '$\\1="\\2" ; ' , $_GET['a'] );? >
这段代码是有问题的,只要提交/1.php?a=b|${@phpinfo()}即可执行phpinfo()函数,这时候如果我们知道\\2的值范围为纯数字,只要正则改成(\w+)\|(\d+)即可解决执行代码的问题,这只是一种修复方案,最好的方法是:在$\\1="\\2"这里不要用双引号。