ThinkPHP未开启强制路由导致的RCE
简介
此漏洞是因为框架对传入的路由参数过滤不严格,导致攻击者可以操作非预期的控制器类来远程执行代码。
漏洞的核心在于通过反射调用
影响版本
5.0.5<=ThinkPHP<=5.0.22
5.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.php
pathinfo(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前面的反斜线即可