环境搭建与基础知识
参考链接
GitHub - Mochazz/ThinkPHP-Vuln: 关于ThinkPHP框架的历史漏洞分析集合
环境搭建
Win20h 、PHP 7.4.3 、VScode
PHPSTORM xdebug 配置一直失败,give up
而VScode(WSL)又不能查上下文,😑😶😡
- Composer
个人感觉比较方便,不用去官网找
1 | composer config -g repo.packgist composer https://mirrors.aliyun.com/composer |
基础知识
天哪,thinkphp的设计的好优秀,膜
ThinkPHP5.0完全开发手册 · 看云 (kancloud.cn)
ThinkPHP6.0完全开发手册 · 看云 (kancloud.cn)
任意文件操作漏洞
thinkphp6
Thinkphp6.0任意文件写入漏洞复现 - 先知社区 (aliyun.com)
影响版本
ThinkPHP6.0.0-6.0.1
漏洞环境
1 | composer create-project topthink/think tp60 |
将 tp60/composer.json 文件的 “topthink/framework”: “^6.0.*“, 改成 6.0.0 版本,并执行如下命令。
1 | composer update |
Debug配置
建议不要php run ./think --host=localhost --port=8000
好像这样的话,PHPSTORM无法debug
当前环境为phpstudy+phpstorm
xdebug由phpstudy下载,同时修改配置
1 | [Xdebug] |
添加服务器

DBGp 代理

配置成功

漏洞复现
修改app/controller/Index.php 文件如下:
1 |
|
修改 app/middleware.php 文件如下(开启Session功能,可参考: https://www.kancloud.cn/manual/thinkphp6_0/1037635 )
1 |
|
写入文件
1 | GET /?c=aaaa<?php+phpinfo();+?> HTTP/1.1 |
注意len(PHPSESSID)要等于32位

访问http://127.0.0.1:8000/aaaaaaaaa.php

漏洞分析
首先,查看session_save
位于vendor/topthink/framework/src/think/session/Store.php

在save函数处,write为写入文件
单步调试,进入vendor/topthink/framework/src/think/session/driver/File.php

writeFile处步入

可以观察到最终执行为file_put_contents($path, $content, LOCK_EX);,$path为$sessionId控制,$content=$sessData=$data
而一开始的$this->handler->write($sessionId, $data)接受两个参数$sessionId, $data
$data为session序列化后的内容,可控
1 |
|
而$sessionId则为

断点步入getId(),vendor\topthink\framework\src\think\session\Store.php

$this->id由setId初始化

由此可见,当len(PHPSESSID) == 32为 PHPSESSID,反之md5(microtime(true) . session_create_id())
**任意代码执行漏洞 **
该漏洞出现的原因在于ThinkPHP5框架底层对控制器名过滤不严,从而让攻击者可以通过url调用到ThinkPHP框架内部的敏感函数,进而导致getshell漏洞
参考链接:
ThinkPHP5漏洞分析之代码执行(十) | Mochazz’s blog
Thinkphp5 远程代码执行漏洞事件分析报告 (seebug.org)
影响版本
- ThinkPHP 5.0.5-5.0.22
- ThinkPHP 5.1.0-5.1.30
payload
因为漏洞触发点和版本的不同,导致payload分为多种,其中一些payload需要取决于debug选项 比如直接访问路由触发的
5.1.x :
1 | ?s=index/\think\Request/input&filter[]=system&data=pwd |
5.0.x :
1 | ?s=index/think\config/get&name=database.username // 获取配置信息 |
还有一种
1 | http://php.local/thinkphp5.0.5/public/index.php?s=index |
可以看到payload分为两种类型,一种是因为Request类的method和__construct方法造成的,另一种是因为Request类在兼容模式下获取的控制器没有进行合法校验,我们下面分两种来讲,然后会将thinkphp5的每个小版本都测试下找下可用的payload。
ThinkPHP 5.0.5-5.0.22
环境搭建
http://www.thinkphp.cn/download/1015.html
个人选择的版本是5.0.10
也可以使用
composer create-project topthink/think=5.0.10 tp5010 --prefer-dist,但需要修改composer.json和composer update
漏洞复现
payload
1 | POST /public/index.php?s=index |

漏洞分析
通过查看手册可以得知tp5支持多种路由定义方式:路由地址 · ThinkPHP5.0完全开发手册 · 看云 (kancloud.cn)
路由地址表示定义的路由表达式最终需要路由到的地址以及一些需要的额外参数,支持下面5种方式定义:
| 定义方式 | 定义格式 |
|---|---|
| 方式1:路由到模块/控制器 | ‘[模块/控制器/操作]?额外参数1=值1&额外参数2=值2…’ |
| 方式2:路由到重定向地址 | ‘外部地址’(默认301重定向) 或者 [‘外部地址’,’重定向代码’] |
| 方式3:路由到控制器的方法 | ‘@[模块/控制器/]操作’ |
| 方式4:路由到类的方法 | ‘\完整的命名空间类::静态方法’ 或者 ‘\完整的命名空间类@动态方法’ |
| 方式5:路由到闭包函数 | 闭包函数定义(支持参数传入) |
这里值得注意的地方有两个,一个是路由定义方式4,tp5可以将请求路由到指定类的指定方法(必须是public方法)中;另一个是即使没有定义路由,tp5默认会按照方式1对URL进行解析调度。
测试的payload为:
1 | POST /public/index.php?s=index HTTP/1.1 |
thinkphp5 的一开始的加载顺序如下:

在app.php中,$dispatch = self::routeCheck($request, $config);为路由检查

具体的代码实现:
由于没有在配置文件定义任何路由,所以默认按照方式1解析调度。如果开启强制路由模式,会直接抛出错误。根据$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);进入
1 | thinkphp/library/think/Route.php |

可以看到tp5在解析URL的时候只是将URL按分割符分割,并没有进行安全检测。继续往后跟,我们回到App.run().
在self::exec()为路由表达式的具体实现

具体代码大致如下:

接着进入模块/控制器/操作,并对module进行步入,而module的功能是将result依次转为模块/控制器/操作,最后会进入invokeMethod

invokeMethod会调用反射执行类的方法 、支持参数绑定,并进入binParams
从module开始,形式感觉有点像Java的反射

bindParams,进行绑定参数,if判断后,进入$vars = Request::instance()->param();

先$vars = Request::instance()

后param,而param中有一个$this->input函数,且会将post参数赋值给vars数组

之后步入input函数

在input中,先getFilter

1 | $filter = $this->filter #['system'] |

再步入进去到filterValue
array_walk_recursive — 对数组中的每个成员递归地应用用户函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 >
>$sweet = array('a' => 'apple', 'b' => 'banana');
>$fruits = array('sweet' => $sweet, 'sour' => 'lemon');
>function test_print($item, $key)
>{
>echo "$key holds $item\n";
>}
>array_walk_recursive($fruits, 'test_print');
>
>a holds apple
>b holds banana
>sour holds lemon


所以value为whomai
最终call_user_func回调函数执行payload

到这里还有一个问题,$this->filter 是什么时候发生变化的.
个人认为,这里的构造和利用链比上面的
call_user_func更有意思。
首先需要关注的是$this指的是Request(thinkphp/library/think/Request.php)对象
主要部分如下:
当应用启动时,会进行URL路由检测

路由检测(根据路由定义返回不同的URL调度)


Method部分最为重要。

如图中断点处,
1 | $this->method = strtoupper($_POST[Config::get('var_method')]); |
Config::get('var_method')=_method

$_POST['_method'] = __construct
$this->{'__construct'}($_POST);调用构造函数,且$_POST为其值

property_exists — 检查对象或类是否具有该属性
此时this->method = '',this->filter=['system'],其中method最后return(其作用应该是消除对流程的影响)。
再回到此处,可以发现method后面的作用是对$rules取值,对流程无影响

最后放七月火师傅一张流程图

其他分析:
可以见y4er师傅的分析[强烈推荐]Thinkphp5 RCE总结 | Y4er的博客
y4er yyds
反序列化漏洞 [🕊鸽了]
thinkphp5
TP5.0.xRCE&5.0.24反序列化分析 - 先知社区 (aliyun.com)
ThinkPHP v5.0.x 反序列化利用链挖掘 - 安全客,安全资讯平台 (anquanke.com)
thinkphp6
ThinkPHP v6.0.x 反序列化漏洞利用 - ctrl_TT豆 - 博客园 (cnblogs.com)
[ThinkPHP V6.0.x 反序列化漏洞 | WHOAMI’s Blog (whoamianony.top)](https://whoamianony.top/2020/12/31/漏洞复现/thinkphp/ThinkPHP V6.0.x 反序列化漏洞/)