点击阅读
漏洞简介
影响版本: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 逐步分析漏洞能更好的捋清漏洞的形成过程,了解新的框架执行流程和开发思想