本文作者:Z1NG(信安之路荣誉作者)
近来在阅读 ThinkPHP5 源码,在阅读到 Request 类文件的时候想起 ThinkPHP5 之前报过两次 RCE 漏洞,于是试着分析了一下这两枚漏洞。在之前还没有能力分析框架的漏洞,调试漏洞也只是拿着 POC 一路跟进,缺乏思考。如今旧洞重提,我能学到还有很多。之前一个大佬我说,分析漏洞并不只是梳理成因和利用过程,还要思考别人是如何能发现问题,自己有什么不足。于是,就有了下文。
变量覆盖引发 RCE
在 thinkphplibrarythinkRequest.php
的第 1090 行,有个可能够执行RCE的操作。前提是要能控制 $value
和 $fileter
变量。
通过回溯,可以找到 thinkphplibrarythinkRequest.php
的 input 方法在第 1036 和 1038 行调用了该方法。
根据 input 的参数列表,需要继续回溯
在 thinkphplibrarythinkRequest.php
的 param 方法中,看到了 $this->param
的来源是 $_GET
和 $_POST
等传参方式的合并。因此可以控制 $this->param
,相应的控制了 filterValue
中的 $data
, call_user_func
中的 $value
。
但是最大的一个问题是,如何控制 $filter
? 也就是 call_user_function
的 $filter
。在 input
方法中,有如下的调用,跟进 getFilter
看看。
简而言之,当传入的 $filter 值为空,那么 $filter 的值来源于 $this->filter。
到这里可以想到的一个思路,可使用反序列化的方式,注入一个 Request 的对象,并且调用 Param 方法,这样就可以一路到达 RCE 的现场。
如果细细看 Request 的每个方法,不难发现如下代码
在 525 行处,由于我们可以控制 $this->method
的值,以致于可以执行 Request 类中的任意方法,并且传入的参数也是我们可控的。可以通过执行 Request 的 __construct
,显然 137 行处是一个变量覆盖,可以将 Request 的各个属性的值进行控制。这样我们就可以控制到了 $this->filter
。
到这里为止,已经可以控制了 call_user_func 的两个参数,执行任意命令。那么现在要解决的问题就是如何触发?换句话说,要满足什么条件才可以执行到 Request 的 method 方法。
回溯发现 Route.php 中的 check 方法调用了 method 方法,值得注意的是,调用 method 的地方很多,选用此处的原因是因为没有给 $method
传值,这样默认 $method
默认就是 false 了,能够顺利触发 __construct
中的变量覆盖。
在 App.php 中的 routeCheck 调用该方法。
看上述代码,需要满足的条件是开启了路由,而默认是开启了路由,这样顺利就能执行了 Route::check
后续的也能顺利执行。
回顾上面的流程,理一下思路,想办法执行 method 的目的是为了控制 call_user_func
的参数。而触发代码执行并不是 method 方法,而是要有一处执行了 param 方法的地方。
全局搜索,会发现 App::exec 执行了这个方法,前提是 $dispatch['type']
为 method,意味着是要路由到类的方法,才可以执行到分支。
显然和 routeCheck 还有关系,继续回头看,要如何满足这个条件。
thinkPHP5 中自带了一个验证码组件已经注册到了如图之中,并且就是路由到类的方法,显然是满足条件。
而如何找到验证码这个类的路由满足条件呢?一个简单的方法,就是先打印出 self::$rules
,所有已经注册的路由都会在这个静态变量里保存着,将这个变量打印出
如下图,可以看到,在 get 里注册了验证码的路由,这也就是 POC 中的 method 为什么是 get 的原因。
这样,利用验证码这个类的路由,所有条件都满足了。
POC如下:
到这,其实漏洞分析就结束了。先总结一下,首先一处疑似可能 RCE 的利用点,通过层层回溯,发现入口是在 Request 类的 param 方法,此处在反序列化 RCE 中也是可以利用的。而为了控制 param 方法的各项参数,需要透过 Request 类的 method 方法来触发 Request 的任意方法执行,当执行了 Request 类的
__construct
魔术方法时,通过变量覆盖完成了控制 pamra 方法的参数。上述的每个问题点在代码之中都显而易见,但点连成线,却是我没考虑到的。在完成漏洞成因的分析,那么剩下的就是寻找 parama 的触发点和满足一切条件来到触发点。所以寻找到验证码类只是为了满足条件的一环,而不是必须。那么在看到其他地方触发了 param 方法时,也是可以利用的。
比如,当开启了 debug 模式,就会记录路由信息,此时则会触发 param 方法,这时的 POC 就不依赖该类了。
未严格限制控制器引发RCE
这个漏洞流程也很简单,反思了一下自己在阅读源码的时候没思考到的点。ThinkPHP 在获取控制器名的时候没有严格控制,但是自己在阅读源码的时候出于惯性思维,没有想到可以用命名空间的方式实例化非控制器的类。简单的提一下这个漏洞的流程。
首先是在解析 url 路径的过程中,仅仅是使用了 / 做分隔符,来切割出一个路径数组。比如
index/think/App/action
。这样切割出来的模块是 index,而控制器则是thinkApp
,操作是 action。而在 PHP 中是支持了命名空间的方式引用某个类。所以在这里自然而然的可将控制器定为thinkApp
类。当然,不一定是要 App 类,也可以是其他的,但是该类可以执行 RCE。
而后续,并没有对控制器名进行处理,于是在如下代码中,就会实例化出一个 App 类的对象。
接着,后续就可以根据如下代码,执行 App 类中的任意方法,此时的
$method
是一个数组,含有两个元素,一个是 App 类对象,一个是 App 类定义的方法。
那选择 App 类中的哪个方法呢?
在 App 类中翻一番,可以看到 invokeFunction 可以通过反射的方式执行任意函数,并且因为参数绑定,参数可控,显然已经可以 RCE 了。
于是构造 POC
总结
这两个漏洞已经公开一年多了,最近在阅读 ThinkPHP 的源码,在阅读到 Request 类的时候,发现这处疑似可利用的 RCE 的点,就想着进而分析一下这个历史漏洞。我将审计过程分为了两步:
第一分析漏洞点和漏洞成因,第二是如何满足条件,到达漏洞现场。
第一步很顺利,也能够轻易的分析出来。但是对于 ThinkPHP 路由不熟悉,无法理解如何寻找到验证码这个类并加以利用。当然如果利用已有的 POC 和动态调试,一路跟进自然能发现。那么如何在没有 POC 的基础下,如何分析出呢?此时似乎有些钻牛角尖,想在代码逻辑上强行分析,阅读代码许久无果,然后才意识到打印出一些变量来发现些蛛丝马迹(打印出了已注册的路由信息可以看到验证码类的路由)。而后理清整个漏洞流程的时候,才意识到验证码类只是利用的一个方式,还有其他不同的方式,当然需要环境的支持。再加深理解了 Request 类的作用(一次请求的所有信息都保存在一个 Request 对象之中)以及该类产生RCE的条件(控制 Request 的 $this->param 和 $this->filter),这个地方在反序列化的利用链中也是关键的一环。
再谈谈第二个漏洞。在阅读 ThinkPHP 源码的时候出于惯性思维,在获取控制器的代码处,第一反应到的 URL 是 indexindexindex,没考虑到在 PHP 中可使用命名空间。如果可以思考到这一点,后续则会相对容易些,但如何寻找到合适的类和合适的方法又是要细细分析。其中的一些坑,还需要动手调试才能体会,而且在不断调试的过程中,对框架机制逐渐熟悉,当然也意识到薄弱之处,比如还是无法完全理解 ThinkPHP 的自定义路由等等。
本文始发于微信公众号(信安之路):旧洞重提,我能学到什么
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论