thinkphp5.0.23rce
漏洞代码分析
函数调用
先从应用入口处分析
跟进start.php
跟进App::run()
run()下调用了该类下的exec方法
exec()中通过$dispatch的type键来决定执行何种case,这里进入method
array_merge将一个或多个数组的单元合并起来,Request::instance()只获得一个实例化对象,而获取post内容、get内容、参数内容、表单中上传的文件的内容则从param()方法中获取,跟进param()
该方法开头调用了method方法,跟进method方法,参数为true
见到可能存在漏洞点,$this>{$this->method}($_POST)可能存在变量覆盖,但还需接下来寻找能否进入elseif方法和这些参数是否可控才可以判断此处是否是漏洞存在点。
跟进$this->server()
跟进input()方法
首先$filter通过getFilter方法获取到一个实例对象,然后进入is_array()的if判断,该函数形参中的data默认为数组类型,所以直接进入if
其中 array_walk_recursive
简单来说该函数可以对1号参数中的数组的每一个元素应用参数2的自定义函数,此处数组中的键和值都作为参数传入参数2的自定义参数中
在该代码中,就是将$data的键和值都传入filterValue方法中
又见到敏感函数call_user_func
所以问题就是如何从请求中传入$this->filter和$this->value这两个值,构造call_user_func()
触发漏洞
参数控制和变量覆盖
首先要进入上面的方法,必须要在最开头使$dispatch=method,进入method对应的case
回到App::run()
$dispatch由routeCheck方法确定,跟进routeCheck
该方法返回result,而result由Route::check决定
跟进check方法
此处由于对method方法进行操作,先跟进request中的method方法,注意此时的参数为false,恰好满足条件进入elseif
一行行分析:
1 | if (isset($_POST[Config::get('var_method')])) { |
thinkphp中Config::get('参数')
方法可以得到相应的配置信息,在config.php中找到var_method:
也就是说 此处可以任意控制$this->method的值,也就表明可以实现该类的任意函数的调用。
事实上理完思绪审到此处才发现,事实上出现问题的原因在于$this->method的值可以简单的通过传入_method的值改变。
考虑利用__construct
property_exists
该函数判断$this即Request类中是否含有$name
当POST一个_method=__construct&filter[]=system
就会执行__construct(filter[]=system)
,进入foreach循环,如果$this有filter这个成员变量,就会把system的值传给$filter,所以我们也能根据这个实现任意变量覆盖
考虑控制Request->$filter
跟进getFilter方法
发现$filter=$this->filter 也就是说可以通过之前的$this->{$this->method}($_POST)调用__construct控制$filter
此时形成漏洞条件满足其一,接下来考虑如何控制Request->value
$value事实上就是$data
$data是$this->server
$this->server是在method中被调用的,调用带参’REQUEST_METHOD’
所以传过去的$this->server就带了一个参数REQUEST_METHOD,也就是data数组中多了一个键为REQUEST_METHOD,payload中为server[REQUEST_METHOD]=whoami
,实现变量覆盖
另外payload中method=get是因为
我们的路由是s=captcha
在vender/topthink/think-captcha/src/helper.php
中可以看到路由的设置,而且路由方法是get
而method方法本来就是获取请求类型的,并且返回$this->method,为了命令执行,我们传入了_method参数实现变量覆盖,但这也导致了method方法获取不到正确的请求类型,所以不妨我们在变量覆盖一个method,使其=get
最后在给出总体的调用流程:
1.变量覆盖:
App::run()->
App::routeCheck()->
Route::check()->
method()->
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami
2.函数调用:
App::run()->
App::exec()->
Request::param()->
Request::method(true)->
Request::server(‘REQUEST_METHOD’)->
Request::input->
经过array_walk_recursived->
filterValue()->
call_user_func($filter, $value)
payload和poc
payload:
1 | http://localhost/?s=captcha |
poc:
1 | import requests |
总结
由于时间问题还有一个遗留的小问题还未解决,就是POST传参server[REQUEST_METHOD]可以直接覆盖$server[‘REQUEST_METHOD’],还没有找到具体实现覆盖的语句。而且跟着别人的思路走总感觉很别扭,日后再回来看。
- Post link: http://example.com/2021/11/24/thinkphp5.0.23rce/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.
若没有本文 Issue,您可以使用 Comment 模版新建。