ThinkPHP 6.0.0-6.0.1 任意文件操作漏洞分析和复现
次访问
ThinkPHP是中国顶想信息科技公司的一套基于PHP的、开源的、轻量级Web应用程序开发框架,在国内被广泛应用。
ThinkPHP 6.0.0-6.0.1中存在一个任意文件操作漏洞。由于对SessionId的处理逻辑存在错误,导致攻击者在目标环境启用session的条件下,可以通过发送特定构造的Session Cookie值,实现任意文件创建和删除操作,在特定情况下还可以实现getshell。
环境搭建
环境依赖
- PHP:ThinkPHP V6的运行环境要求PHP7.1+版本
- Composer:ThinkPHP V6版本开始仅支持Composer安装及更新
安装Composer
- Linux/Unix/macOS 环境
官方提供了快速安装脚本,可以执行如下命令:
1 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" |
命令执行完成后会生成composer.phar文件,将其移动到/usr/local/bin下,就可以对composer进行全局调用:
1 | sudo mv composer.phar /usr/local/bin/composer |
验证composer安装情况:
1 | composer --version |
- Windows环境
官方提供了EXE安装程序https://getcomposer.org/Composer-Setup.exe,安装程序会自动安装最新版本的Composer并设置PATH环境变量,安装完毕后即可在任意路径下以命令行方式调用Composer。
安装ThinkPHP 6.0.0
用Composer安装thinkphp
1
composer create-project topthink/think tp60 #tp60为自定义名称
将thinkphp版本切换为受漏洞影响的6.0.0版本
修改tp/composer.json文件,将”topthink/framework”: “^6.0.0”修改为6.0.0,如下图:
执行命令
composer update
将thinkphp代码更新到6.0.0版本。启动thinkphp:
1
./think run --host 0.0.0.0 --port 8080
漏洞分析
代码分析
根据漏洞通告内容和时间节点及版本,遍历ThinkPHP源代码commit记录,将漏洞修复代码定位到commit 1bbe75019ce6c8e0101a6ef73706217e406439f2:
由上述代码看出,针对漏洞的修复主要是对sessionId的值添加了限制,要求id中的所有字符必须为字母或数字。结合漏洞描述,推测漏洞成因可能是sessionId中的特殊字符导致session存储时产生了目录遍历等情况。因此重点关注session的存储实现。
首先,通过代码看到session name为PHPSESSID:
查看实现session存储的save()
方法代码如下:
在save()
方法中,当$data
变量不为空时,将$data
内容序列化后调用了$this->handler->write()
函数;当$data
为空时,调用了$this->handler->delete()
函数。跟踪write()
和delete()
函数,跟踪到vendor/topthink/framework/src/think/session/driver/File.php中:
可以看到在write()
和delete()
函数中,都调用getFileName()
函数,根据sessionId获取文件名,write()
函数紧接着根据参数配置判断是否对数据进行gz压缩,最后调用writeFile()
将数据写入指定路径文件。而delete()
函数直接调用unlink()
函数删除文件。继续跟踪getFileName()
函数,发现其实现内容就是以sessionId作为文件名,拼接上配置中的存储路径,形成写入文件的路径:
而writeFile()
函数,则直接调用了file_put_contents()
函数执行写文件操作:
从上述调用链条来看,当存储session文件时,写入文件名由cookie中PHPSESSID的值控制,而代码中对PHPSESSID值的校验,只判断其满足长度为32的字符串即可。因此,可以使用../等字符,构造长度为32的文件名,实现目录穿越,当$data
不为空时,可以实现任意文件创建/写入,当$data
为空时,可以实现任意文件删除。
从代码来看,$data
的值默认为空,可以通过setData()
或set()
函数进行赋值:

在框架代码中进行搜索,发现原始代码中并没有对session进行赋值的操作。因此,默认环境下,只能利用此漏洞进行文件删除操作。如果要想实现任意文件创建写入,需要配合实际业务开发代码中有session赋值操作才能实现。
利用复现
启用session
ThinkPHP 6.0默认不启用session,如果想要触发此漏洞,需要首先启用session。修改app/middleware.php,将
\think\middleware\SessionInit::class
前的注释去掉即可。启用session后,session默认存储路径为runtime/session
下文件删除
如上述分析所说,文件删除可以在默认配置环境下触发,只需要在32位长度的PHPSESSID值中,利用
../
等特殊字符,将目标文件路径从runtime/session
穿越到指定目录下即可。利用效果如下:文件上传/GetShell
触发文件上传/GetShell,需要ThinkPHP业务开发代码中,有session赋值操作。为了复现漏洞,这里简单在
app/controller/Index.php
中添加一个Session:set()
操作,然后再启动ThinkPHP服务:构造请求如下:
1
2
3
4GET /?username=%3C?php%20phpinfo()%20?%3E HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Cookie: PHPSESSID=../../../../public/sayers123.php;提交请求后,即可看到ThinkPHP路径下
public
路径中被创建了sayers123.php
文件,其内容如下:1
a:1:{s:4:"user";s:18:"<?php phpinfo() ?>";}
访问
http://127.0.0.1:8000/sayers123.php
,可以看到phpinfo()被执行:
同理,将phpinfo()替换为一句话木马,即可实现GetShell。