ThinkPHP6 任意文件操作漏洞分析

释放双眼,带上耳机,听听看~!
本文是一篇关于 ThinkPHP6 任意文件操作漏洞的代码审计复现文章,也是加入 bugfor 社区的投稿文章。这是笔者先前所写,由于水平有限,如有错误请指正。

漏洞介绍 

2020年1月10日,ThinkPHP 团队发布一个补丁更新,修复了一处由不安全的 SessionId 导致的任意文件操作漏洞。该漏洞允许攻击者在目标环境启用 session 的条件下创建任意文件以及删除任意文件,并在实际环境中还可能 getshell。具体受影响版本为ThinkPHP 6.0.0-6.0.1 。 

环境搭建 

环境要求: PHP >= 7.1.0,ThinkPHP6.0.0-6.0.1

ThinkPHP6.0.x 必须通过 composer 方式安装和更新,无法通过 git 下载安装 

安装 composer(Linux 或者 macOS) 

curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composercomposer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ //稳定版
composer create-project topthink/think=6.0.1 tp6 

国内镜像 

composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ 

安装 ThinkPHP6.0.1 

//稳定版
composer create-project topthink/think=6.0.1 tp6 

验证安装 

切换到 tp6 目录下执行命令:php think run 

在浏览器中打开网址:127.0.0.1:8000/ 

看到欢迎界面说明成功安装 

漏洞分析

访问 ThinkPHP 官方 GitHub 找到相关的 commit 信息: 

https://github.com/top-think/framework/commit/1bbe75019ce6c8e0101a6ef73706217e406439f2 

修正代码位于 src/think/session/Store.php 文件的121 行处,仅仅在原来的基础上多加了一个 ctype_alnum 函数,该函数的用法如下:

我们定位到本地 web 目录下的文件:vendor/topthink/framework/src/think/session/Store.php,该文件主要是 session 相关的操作,包括 session 的设置、获取和保存等。 

定位到 setId 函数,逻辑是判断参数 id 是否是长度为32的字符串,是则将该参数作为 session id,否则将利用 session_create_id 函数来生成唯一的 session id。 

而修正后的代码多了 ctype_alnum 函数检查参数 id 是否为字母和数字的组合。 根据 Store.php 文件和添加的 ctype_alnum 函数猜测可能是在存储 session 时触发了相关的文件操作漏洞。 

在 Store.php 文件中查找 setId 发现在 save 函数中调用了 setId 函数,代码中还存在 write 和 delete 函数涉及到文件的写入和删除。 

我们跟进 write 函数,跳转到 session 接口驱动文件:vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php,查找实现了这个接口的类文件一共有两个 —— Cache 和 File,而 Cache 类文件中的 write 函数无法利用,所以跳转到 File 类文件:vendor/topthink/framework/src/think/session/driver/File.php 

继续跟进 writeFile 函数,File.php 的 170 行:

file_put_contents 函数涉及到文件写入,危险函数找到了,下一步需要回溯寻找可控点。 

我把涉及到的函数放到一起便于回溯可控点: 

$this->id 是 session_id,由 setId 函数设置;$this->data 是 session 数据,默认为空,由 set 函数设置。 我们查找调用了 setId 函数的地方:vendor/topthink/framework/src/think/middleware/SessionInit.php 

$cookieName 的值是 PHPSESSID,因为默认环境配置中没有 session.var_session_id 参数,因此进入 else 分支,调用了 cookie 函数,易知 $sessionId 是 cookie 中名为 PHPSESSID 的值。

分析到这里,file_put_contents 函数的文件名已经可控,那我们看下能不能实现在任意位置写入文件。由于在 write 函数中调用了 getFilename 函数,使得文件名带上了 sess_ 的前缀,并且如果目录不存在会自动创建,因此我们可以利用目录回退符(../)来实现任意文件写入。而写入文件的内容取决于 $this->data 的值,默认为空,需要由实际的后端业务逻辑来决定,只有当后端业务中存在 session 值可控的代码时方可 getshell。

再来看一下 delete 函数,如果目录不存在并不会创建目录,如果开发环境是 Windows 系统的话,我们可以利用 Windows 系统的一个特性 —— 如果目录不存在就会跳过当前目录,从而实现任意文件删除。如果是Linux 或者mac OS 系统则没有这一特性,但我们可以结合任意文件写入创建目录,从而根据已知目录实现任意文件删除。

漏洞复现

根据以上的漏洞分析,我们可以总结出文件写入和删除的利用条件: 

要实现任意文件写入,需要目标环境开启 session,session 值可控且不为空;

要实现任意文件删除,需要目标环境开启 session,session 值为空。

构造场景

本文在 MacOS 10.14.6 系统中进行复现,由于默认环境中没有开启 session,且没有 session 设置的代码,所以我们需要构造相应的场景。 

1. 开启 session:

在 app/middleware.php 文件中删除最后一行注释 

2. 添加设置 session 值的代码:

由于默认环境中 session 为空,所以我们需要添加代码来测试任意文件写入漏洞。 

在 app/controller/Index.php 文件中添加方框中代码。 

任意文件写入

使用 burp 作为中间代理构造如下 GET 请求,并使得 cookie 参数中名为 PHPSESSID 的值为 32 位的字符串。

可以发现默认在 runtime/session/ 目录下新增了一个 session 文件,session 值经过了序列化。

我们还可以实现 getshell,因为 ThinkPHP6.0.x 的控制器文件在 app/controller/ 目录下,所以我们在该目录下写入文件。

成功写入文件:

访问控制器验证成功 getshell:

任意文件删除

要实现任意文件删除首先要利用任意文件写入构造已知路径,从而利用目录回退符(../)跳转到任意目录实现文件删除。 构造如下请求: 

可以发现创建了新的目录 sess_ :

再次构造如下请求删除 14.log 文件,注意因为要使 session 数据为空,所以不能添加 GET 参数。

验证成功删除文件:

总结

这个漏洞是由于一处不安全的 session id 造成的任意文件操作,最终找到了危险函数 file_get_contents,通过回溯参数寻找可控点发现并没有过滤处理,因此整个分析利用也相对容易。官方给出的补丁中在检查 session id 时添加了 ctype_alnum 函数,很好地过滤了非法构造 session id 的情形。

参考 

https://paper.seebug.org/1114/#_2

https://hacpai.com/article/1579965339516

https://www.smi1e.top/thinkphp6-0-%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E5%86%99%E5%85%A5%E6%BC%8F%E6%B4%9E/

https://woj.app/6032.html

BUGFOR课程安全独秀圈质量好文

BUGFOR第一期网安助力计划 开课通知

2020-4-12 17:01:05

WEB安全安全独秀圈质量好文

盘点那些可以绕过的验证码

2020-4-13 18:54:35

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
有新私信 私信列表
搜索