点击阅读
漏洞简介
影响版本:ThinkPHP 6.0.0 ~ThinkPHP 6.0.1
漏洞危害:任意文件操作,getshell
官方补丁:https://github.com/top-think/framework/commit/1bbe75019ce6c8e0101a6ef73706217e406439f2
漏洞分析
1. 搭建环境
安装漏洞版本的 ThinkPHP
测试使用 composer create-project topthink/think=6.0.0 tp6.0.0 命令安装的时候会自动安装 6.0.2 版本的 framework

可以使用 composer create-project topthink/framework=6.0.0 tpframework 命令下载指定版本的 framework,再替换过去即可
下载若出现 SSL: Handshake timed out 错误的可以尝试换源并在 php.ini 中重新设置超时时间
composer config -g repo.packagist composer https://packagist.phpcomposer.com
default_socket_timeout = 360
2. 复现
开启 Session,在全局的中间件定义文件中删除 Session 初始化注释
1 |
|
自定义一个漏洞方法
1 | public function vuln() |
发送以下请求
1 | GET /index/vuln?para=%3C?php%20phpinfo();?%3E HTTP/1.1 |
即在 web 根目录生成 000000000000000000.php (如果有写入权限的话)

3. 分析
看下官方 github 的 commit

对 $id 使用 ctype_alnum 检测其是否全部为字母和(或)数字字符
下面我就 debug 跟一下调用看看是如何通过 session 写入文件的
在开启 Session 初始化的全局中间件之后,ThinkPHP 会调用反射执行类的实例化,加载 \think\middleware\SessionInit

之后通过中间件调度管道,调用 SessionInit 的 handle 类方法进行 session初始化

跟进 handle 函数,来到 /vendor/topthink/framework/src/think/middleware/SessionInit.php,首先获取的 $varSessionId值为空,下面跟进 $this->session->getName()

来到 /vendor/topthink/framework/src/think/session/Store.php,Store 类构造函数会调用其 setId 方法,来到漏洞代码处,但此时为 Store 类的初始化阶段,并没有 id 参数传入

接着通过 Store 类的 getName 方法获取 sessionName,之后会赋值给 $cookieName

而 sessionName 的值在 Store 类中定义好了的,即 "PHPSESSID"

返回到 SessionInit,所以此处的 $cookieName 的值为"PHPSESSID"

接着往下开始获取 $sessionId ,关键一步出现了,由于 $varSessionId 的值为空,所以 if 判断之后,$sessionId 的值就是名称为 PHPSESSID 的 cookie 值,也是后来写入的文件名,接下来将 $sessionId 直接传入 setId 函数进行判断

又来到漏洞代码处,就是在个时候在这里未对 $id 值做除长度之外的限制,从而可以直接写入任意文件

在上面将 PHPSESSID 的值赋值给 id后一路跟进,再次来到 /vendor/topthink/framework/src/think/session/Store.php,终于开始保存 session 数据,获取 $sessionId,即 PHPSESSID 的值,这里测试使用的是 c465f41ee715df8c726dcc4742a6.php

将 data 序列化之后通过 write 函数将序列化的数据保存在 c465f41ee715df8c726dcc4742a6.php 文件中

跟进 write 函数,在文件名前加上了 sess_ 前缀,再调用 writeFile 函数写入

跟进 writeFile,最终 file_put_contents 完成写入

成功写入 WebShell

在 /vendor/topthink/framework/src/think/session/Store.php:254 中不难看出还有个 delete 函数,用于删除 session
1 | public function save(): void |
这里我也进行了测试,看能否删掉在 web 根目录下的 000000000000000000.php,跟进 delete,

显然是通过 getFileName 获取文件名后直接进行删除操作了,但是 getFileName 函数会自动对文件名加上 sess_ 前缀

这就直接导致在最后 unlink 删除操作之前的 is_file 判断过不了

这也算是比较经典的问题了,因为 php 在读写文件时使用的是 php_stream_open_wrapper_ex 进行流处理,其最后会使用 tsrm_realpath 函数将文件名标准化成一个绝对路径, 通过处理 ../ 等特殊符号,文件路径中间有不存在的目录时也不会影响,而判断文件存在、重命名、删除文件等操作无需打开文件流,也就不会进行这种处理,导致报错

漏洞总结
该漏洞主要危害还是文件写入,而文件删除实际测试来看还是比较有限制的,可操作性不强
v6 版本的 ThinkPHP 较之前版本还是有不少变化的,通过 debug 逐步分析漏洞能更好的捋清漏洞的形成过程,了解新的框架执行流程和开发思想