漏洞简介
漏洞存在版本:5.0.0<=ThinkPHP5<=5.0.23,5.1.0<=ThinkPHP<=5.1.30,在开启debug模式的情况下(默认不开启),传入payload可以修改Request类的成员变量,控制filter,和server[REQUEST_METHOD]最后在filterValue中的call_user_func造成任意代码执行
漏洞复现环境
- php7.4.11
- apache2
- ThinkPHP5.0.23

1 | server%5BREQUEST_METHOD%5D=dir&_method=__construct&filter%5B%5D=system |
部分payload
1 | # ThinkPHP <= 5.0.13 |
漏洞分析
数据包
1 | POST / HTTP/1.1 |
发送数据包后进入start.php的APP::run()->send()方法

然后跟进到thinkphp/library/think/App.php中进行模块/控制器绑定,默认语言加载、系统语言包,在未设置调度信息进行URL路由检测,调用的是self::routeCheck

在Route::check中进行路由获取

在check方法中调用了$request->method()获取$_SERVER[REQUEST],method方法是关键所在

由于$method默认是false,因此进入第二个分支,先从config文件中获取var_method的值,翻看config.php文件

得到var_method对应的是_method,而$this->method获取的是$_POST[_method]的大写,因此是我们传入的__construct的大写,即__CONSTRUCT,由于没有对$method进行过滤,因此调用了Request类的构造方法,传入变量是$_POST变量

在构造方法中,检查Request类中是否含传入的post数组中的变量,存在则存入属性中,即$this->server[REQUEST_METHOD]会赋值为dir即我们要执行的命令,$this->filter会赋值为system,至此赋值结束

回到thinkphp/library/think/App.php的routeCheck逻辑,路由无效时会再调用parseUrl对url进行解析

返回thinkphp/library/think/App.php的run方法逻辑,如果开启了debug模式的话self::debug的值为true,会调用$request->param()方法进行变量的读取

在param方法中如果$this->mergeParam是空值(默认是false,即默认会触发)则会调用method方法,传入变量为true,true意味着会进入$this->server方法

server方法中,$this->server是之前赋值的REQUEST_METHOD=dir,$name是REQUEST_METHOD

在input方法中对$name进行一系列处理后取出$this->server``中的REQUEST_METHOD即我们传入的dir

然后下面就是getFilter方法,在原来filter的基础上加一个null,接着就进入到关键方法filterValue

先用array_pop弹出一个值,剩下的filters进行foreach获取值,判断是否可调用,这里我们的$this->filter是传入的system,$value是dir,进入call_user_func就是system('dir')

至此,漏洞分析完成,总的来说,关键出现问题的地方就是在Request.php的method方法中,使用了用户传入的值进行动态函数调用,修改了Request类的对象的值,因此才有后面的利用
漏洞修复

判断$method是否是数组(‘GET’, ‘POST’, ‘DELETE’, ‘PUT’, ‘PATCH’)中的元素,才会进行调用