Thinkphp 5.0.x 变量覆盖导致的RCE 漏洞分析
Thinkphp5.X的RCE分为两大版本:
ThinkPHP 5.0-5.0.24
ThinkPHP 5.1.0-5.1.30
tp5.0.x
代码执行漏洞:
URL:http://tp5019.com/index.php
POST请求
_method=__construct&method=GET&filter[]=system&s=whoami
_method=__construct&method=GET&filter[]=system&get[]=whoami
自己搭建的环境是 thinkphp5.0.5 php 5.4.x
payload:_method=__construct&filter[]=system&method=GET&get[]=whoami
测试,成功执行命令!
漏洞分析:
这条payload不受debug影响,因为触发了两次request->param()
首先是来到 Route::RouteCheck方法中
接着进行 Route::check 方法中
继续跟中method方法中,这里是关键,因为这里存在变量覆盖漏洞,最后导致了命令的执行
这里只是单纯的调用了__construct方法,我们去__construct中看看如何进行调用的,发现是一个 foreach循环将每个post的参数进行变量覆盖
接着回到了App->run()的方法中
调试有一处触发了param,这里我们不跟,跟另外一处(在self::module中)
此时走到self::module这里,已经是走过了 initCommon 初始化配置参数,routeCheck 路由检测,来到了如下的地方,此时的post数据有如下
继续跟进去,这个module方法主要就是进行 相关的控制器 动作 的初始化
来到最后app->module方法中的最后 进行调用了路由所指向的动作
继续跟进去return self::invokeMethod($call, $vars)
,来到了参数绑定的方法中,继续跟进去
参数绑定的方法实质就是接收请求过来的方法的参数,它最后又继续进去了 request->param方法中 ,继续跟
开始获取各种请求方法请求过来的数据
此时我们是post请求,所以进post中
接接着将$_POST的数据带进了input方法中,在这里$name为空所以直接退出来
然后将数据合并 再一次进去 input方法中
此时的$name不为false 所以往下走,此时来到了array_walk_recursive,这个方法 就是 进filterValue方法中 $data作为第一个参数 $filter作为第二个参数,用$filter函数进行处理$data每个一个值
但是这里观察,$filter 为什么能被我们控制呢?往上看一下,当$filter不是字符串的时候,则进行转化为array赋值
再继续走,处理完之后 $data其中的值 就为如下
最后通过渲染,展现在页面上!
同样的为什么会显示处两个一样的值呢?
因为$_POST中有个get[]=whoami,当变量覆盖的时候$this->get[]=whoami,导致Request类中又多了一个whoami
总结:
第一个重点: $this->method 可控导致可以调用 __contruct() 覆盖Request类的filter字段
然后App::run()执行判断 debug 来决定是否执行 $request->param(),
并且还有$dispatch['type'] 等于controller或者 method 时也会执行$request->param()
第二个重点: 最终 $request->param()会进入到input()方法,在这个方法中将被覆盖的filter回调call_user_func(),造成rce。
关系图如下:
最后还需要知道的是: 5.0.13版本之后需要开启debug才能RCE
URL:http://tp5019.com/index.php?s=captcha
POST请求:
payload:_method=__construct&method=GET&filter[]=system&s=whoami
发现没有响应,但是开启debug模式之后就可以进行命令执行
但是开启 captcha 路由之后,在debug关闭了之后也可以进行了
没开DEBUG,不能RCE的原因:在进入module方法中对$request->filter中的变量进行了清空
开启debug之后能够进行RCE的原因:走的就不是module分支,而是method的分支