影响版本
ThinkPHP 6.0.0-6.0.1,这里使用 thinkphp 6.0.0 做漏洞复现、分析
环境搭建
php 集成环境使用 phpstudy,php 版本为 7.3.4 nts
thinkphp 6.x 使用 composer 进行安装,下载 https://getcomposer.org/Composer-Setup.exe 进行安装即可
安装完成后,打开 cmd,输入命令composer --version
设置 composer 镜像
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
进入 phpstudy 的 www 目录下,运行命令,其中 thinkphp6.0.0 是目录名称,后面的 6.0.0 是版本号
composer create-project topthink/think thinkphp6.0.0 6.0.0
在 thinkphp 目录下使用php think version
命令来查看当前 thinkphp 版本,当前 thinkphp 版本不是 6.0.0,需要调整 thinkphp 版本号
切换 thinkphp 版本到 6.0.0 composer require topthink/framework:6.0.0
,然后再次查看 thinkphp 版本号
漏洞复现
thinkphp6 默认不开启 session,需要手动进行开启,代码位置在 app\middleware.php,取消掉 session 初始化
下面代码的注释即可
任意文件写入
来到 app\controller\Index.php#index(),增加如下代码,目的就是为了增加一个可控的 session
session('test', $_GET['f']);
默认写入 runtime/session 目录下
使用 burp 发送如下数据包,这里需要注意两个点
- get 请求中的空格需要使用 url 编码即 20% 替换
- http 请求头中的 PHPSESSID 的值为 32 位,除去后缀
.php
后前面的 aaaa 是 28 位,加起来总共是 32 位
然后可以看到 runtime/session 下会生成一个 php 文件,sess_ 不计算长度,后面的 aaa.php 长度为 32位
访问该链接 http://127.0.0.1/thinkphp6.0.0/runtime/session/sess_aaaaaaaaaaaaaaaaaaaaaaaaaaaa.php
写入 public 目录下
或者可以这样子,直接写入 public 目录下
访问连接:http://127.0.0.1/thinkphp6.0.0/public/aaaaaaaaaaa.php
任意文件删除
在 app\controller\Index.php#index 中增加这样一个代码,同样,需要增加一个可控的 session
session(null, $_GET['d']);
在 burp 下创建 bbbbbbbbbbb.txt 文件,使用 burp 尝试进行删除
成功删除文件
需要注意的是,这里的 PHPSESSID 的值也得是 32位
漏洞分析
thinkphp 官方 修正 sessionid 检查的一处隐患 的 commit 连接
该漏洞是存储 session 时进行了写入文件操作,从而导致的任意文件写入
文件写入/删除操作
vendor\topthink\framework\src\think\session\Store.php#save 函数,在 data 不为空后会进入 write 操作,在 data 为空后会进入 delete 操作,这里追踪 write 函数,先看任意文件写入漏洞
跟进 write 函数,在 vendor\topthink\framework\src\think\session\driver\File.php#write
继续跟进 writeFile 函数,最后在 vendor\topthink\framework\src\think\session\driver\File.php#writeFile 函数进行文件写入操作,file_put_contents 函数在 php 中用于进行文件写入操作
delete 函数同理,会调用 vendor\topthink\framework\src\think\session\driver\File.php 的 delete 函数和 unlink 函数进行删除文件操作
写入文件名可控
通过分析可以知道,sessionId 其实就是最终写入/删除的文件名,那么 sessionId 是怎么来的呢?
首先在 vendor\topthink\framework\src\think\session\Store.php#save 中调用了 getId 函数
查看 getId 函数,在 vendor\topthink\framework\src\think\session\Store.php#getId
这里的 id 又是在上面的 setId 函数中赋值的
在 vendor\topthink\framework\src\think\middleware\SessionInit.php#handle 处调用了 setId 方法
这里的 varSessionId 变量会从 config\session.php 文件中读取,默认为空,因此 if 判断会走到下面的 else 中
sessionId 的值是 cookie 中 PHPSESSID 的值,为攻击者可控的,从而导致写入文件名可控
写入的内容
写入的内容其实就是 vendor\topthink\framework\src\think\session\Store.php#save 中的 data 变量,先来看看 data 变量是如何获取的
data 变量预先定于在了 vendor\topthink\framework\src\think\session\Store.php 中并且默认为空
因此,data 内容关键在于对 session 的操作方法,session 不为空的话,会进行 session 文件写入操作,导致任意文件写入漏洞;session 为空的话,会造成任意文件删除漏洞,在复现过程中,我们对 session 的操作代码如下