0x00 前言
抽空分析一波
0x01 5.0.x漏洞分析
POC集合
- extend(需要
think-captcha
)- POC 1(覆盖get变量)
- POC 2 (覆盖server变量)
1 | /public/index.php?s=captcha |
1 | /public/index.php?s=captcha |
- 开启debug模式下POC(extend or core)
1 | /public/index.php |
环境
PHP5.5.38+Thinkphp5.0.22完整版+PHPStorm+Apache
POC 1 Of Analyse
程序从入口函数APP::run()
开始,会实例化一个Request对象,自动调用构造方法初始化所有参数。
根据URL路由检测解析调度,将返回值赋给$dispatch
,然后会判断debug状态选择逻辑,这里变量覆盖的调用链为 App::routeCheck() ==> Route::check() ==> Request::method()
,这里跟进routeCheck()
进行路由检测,跟进check()
这里会先进入method()
方法中,然后,跟进method()
这里动态调用类,将取得的$_POST['_method']
值赋给$method
,可以调用Request类中任意方法,选择动态调用__construct()
方法来达到变量覆盖。
这里的$this->filter
、$this->get
、$this->method
经过foreach循环后,被POST传入的值重新覆盖了,紧接着回到routeCheck()中,完成路由规则的解析及缓存机制的处理,得到的$result
然后将该值赋予$this->dispatch
,由于未开启debug模式,所以程序就进入了exec()
中,继续跟进
会根据传入的$dispath['type']
来选择case的流程,URL中确定了s=captcha,所以该处Type为method;倘若按照默认的s=index,Type为module,会在module()
中重新初始化$filter的值,将会覆盖已经覆盖的值。但是若开启了Debug却不用在意这些,因为执行的是另一条链,此为后话,继续跟进param()
获取参数,然后调用method(),此时$method === true,所以跳转至input()
,继续跟进
此时$filter为null,然后进入getFilter()
,跟进
当filter为null时,就会把Request::$this->filter
赋给它,这里是system,然后回到input()中,此时data为POST,所以进入filterValue()
,跟进
这里就直接调用了call_user_func(),传入了之前POST覆盖的$filter
和$value
参数,而value是之前$this->param
通过array_merge
将当前请求参数和URL地址中的参数合并 ,正好$this->get
被全局覆盖为whoami,在此处被调用,俩者均为可控所以造成了RCE,调用栈为
POC 2 Of Analyse
此处与前者的调用链几乎一致,只是在处理input数据时,利用了另一处入口,也就是method()
此时$method为True,所以将REQUEST_METHOD
传入server()中,跟进
这里$name
的值就是REQUEST_METHOD
,而且传入input()的$this->server
就是之前通过构造方法变量覆盖的server变量,所以接下来在进入input()中的$data
就变成我们可控的点,然后经过call_user_func(),成功RCE,调用栈为
关于Debug
application/config.php
下存放着系统配置文件config.php
,开启debug,然后进入前面处理判断debug状态的run()
开启了debug后,就直接进入param()中,就不用进入之前的exec()再跳转回param(),然后后面的处理逻辑流程与之前一致,取得filter和server,然后call_user_function()
此外在之前的routeCheck()中,没有进入paserUrl()中调度并且路由注册,直接Type为module,所以并不需要s=xxxx控制器,也就是核心版也适用
至此,5.0.x的全部分析完成了,完整利用链如下(盗绿盟的图:sweat_smile:):
5.0.x补丁
对$method()
进行了校验,只允许为[‘GET’, ‘POST’, ‘DELETE’, ‘PUT’, ‘PATCH’] 五种方式(默认为POST),不再允许调用任意类方法
可利用版本问题
通过composer create-project topthink/think
然后修改composer.json中TP的版本,即可下载任意版本的TP,然后批量Fuzz该版本的POC,得出结论
- 没有利用captcha等组件可以利用的版本
覆盖get变量的POC
1 | 5.0.2~5.0.12 |
- 利用captcha组件可以利用的版本
覆盖get变量的POC
1 | 5.0.2~5.0.23 |
覆盖server变量的POC
1 | 5.0.22/23 |
0x02 5.1.x漏洞分析
POC
1 | POST / |
环境
PHP5.6.27+Thinkphp5.1.17(with think-install)+PHPStorm+Apache
composer create-project topthink/think 5117
修改json中TP版本为5.1.17
composer update
Tip:TP5.1.x需要PHP>=5.6
Analyse
TP5.1之后加载的流程开始出现了比较大变化,所以这里在App::run()
击中断点。
进入routeCheck()函数,
这里调用了若干个check()函数,然后在RuleGroup::checke()
中调用了漏洞方法method()
这里不像之前的$this->{$this->method}($_POST);
去构造__construct()
方法覆盖变量,而是直接地覆盖,将$this->filter
覆盖为POST传入的数组,$this->method
覆盖为FILTER
,然后返回$method
回到check()中,进入getMethodRules()
进行路由分发
这里将$rules['filter']
与$rules[*]
进行合并,但是数组中并没有filter的键名,所以这里会报错,所以这里必须要在入口函数处加error_reporting(0)
,逻辑才能继续,否则直接跳转至error中
这个位置必须得在ruqire之后才有效,这个条件正是这个POC鸡肋之处。然后回到run()中,跟进param()
这里又调用了method(),返回的$method
为POST,所以进入case 'POST'
流程,跟进post()
直接返回POST的数组,然后回到param()中,将参数合并为$this->param
,然后传至input()
中,跟进
这里跟之前版本的类似,先是调用了getFilter()
将$filter
取出,然后在array_walk_recursive()中对$param
递归调用回调函数filterValue()
,而且传入的$data
和$filter
都是POST的三个值,继续跟进
foreach中遍历$filter,然后传入至call_user_func(),成功RCE!!!
至此,5.1.x的全部分析完成了,完整利用链如下(又盗绿盟的图😅):
5.1.x补丁
对$method()进行了校验,只允许为[‘GET’, ‘POST’, ‘DELETE’, ‘PUT’, ‘PATCH’] 五种方式(默认为POST),不再允许再调用任意类方法或者直接覆盖原有变量了
可利用版本问题
同样通过composer下载全部版本的TP,然后批量在入口文件中加入error_reporting(0);
FUZZ测试POC可以得出可利用版本:
5.1.0
5.1.14
5.1.16
5.1.17
5.1.19
5.1.20
5.1.21
5.1.22
5.1.23
5.1.24
5.1.25
5.1.26
5.1.27
5.1.28
5.1.29
5.1.31
5.1.32
此外,更为鸡肋的一点就是在/vendor/topthink/
中不能存在除think-installer外的其他依赖…..
0x03 结尾
这个漏洞处的Request::method()方法,导致影响版本数量之多,这个着实比较令人头大,而且利用条件也比较鸡肋(运气好),但是利用链非常的巧妙,膜一波Orz