ThinkPHP6漏洞复现

1.漏洞描述

2020年1月10日,ThinkPHP团队发布一个补丁更新,修复了一处由不安全的SessionId导致的任意文件操作漏洞。该漏洞允许攻击者在目标环境启用session的条件下创建任意文件以及删除任意文件,在特定情况下还可以getshell。

2. 受影响版本

具体受影响版本为ThinkPHP6.0.0-6.0.1。

3. 漏洞利用

利用方式:PHPSESSID为32位字符可触发漏洞利用
利用示例:删除数据库配置文件
img

4. 复现环境

Windows:phpstudy + Thinkphp(6.0.0) + php7.2.1
先安装composer工具,composer(windows版本)下载路径https://getcomposer.org/Composer-Setup.exe,php版本使用7以上。
thinkphp6.0.0环境搭建运行
composer create-project topthink/think tp 6.0.0
tp是项目文件夹名称 6.0.0是版本号
cd tp && php think run
#会开启一个临时的开发环境的服务器,默认运行在localhost:8000

5. 漏洞缺陷代码位置

src/think/session/Store.php
img

6. 审计分析

6.1. 流程分析

前提条件:session功能已打开
img

6.2. 代码分析

第一步:通过官方的修复可以推出来漏洞的成因,主要原因应该是出现在Session中,那下面就来分析一下Session的相关代码.
/vendor/topthink/framework/src/think/session/Store.php #TP6存储Session文件的目录
漏洞介绍中说到该漏洞允许攻击者在目标环境启用session的条件下可以删除任意文件和创建任意文件,那就先定位一下关键词delete,既然是任意文件删除,那么参数就一定是用户可控的。
含delete关键字的一共有三处,可以推测在函数265行的位置很可能是该漏洞的形成位置,因为涉及到了SessionId。而且漏洞介绍中也说了可以任意创建文件,在delete关键字上面就有一个write函数且包含有SessionId。
img
跟踪一下这个write函数,找到write()和delete()函数的详细定义
img
img
img
第二步:这里只分析write函数,跟进write的getFileName和writeFile函数。

  1. getFilename函数功能是根据name构建了一个新的文件名,如果文件不存在则新建,这里可以看到只要name可控就可以控制最后的文件名

img

  1. WriteFile函数功能将content内容写入到path路径中

img
在返回最开始处梳理代码的整理逻辑,如果sessionId构造的文件中,data数据为空则执行删除操作
第三步:回到write函数调用处继续追踪session的赋值

  1. /**
  2. * session_id设置
  3. * @access public
  4. * @param string $id session_id
  5. * @return void
  6. */
  7. public function setId($id = null): void
  8. {
  9. //漏洞未修复前的代码
  10. $this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());
  11. }

当传入的参数this->id,观察这个值是否可控,继续追踪setId被调用的地方直到setId,查看id赋值来源于$sessionId。
img
img
第四步::继续追踪查看下cookieName的值,发现$varSessionId为空,cookieName值为PHPSESSID。
img
img
再回到setid调用的地方,可以发现PHPSESSID来源于cookie,而cookie可控,结合最开始的分析得出结论只要控制了cookie中PHPSESSID值就会产生创建任意文件漏洞,如果sesion的值可控甚至会造成getshell。任意文件删除漏洞分析过程类似。

6.3. 漏洞复现

tp6默认未开启session,修改源代码主动打开session功能。
删除/app/middleware.php最后一行的注释即可

  1. <?php
  2. // 全局中间件定义文件
  3. return [
  4. // 全局请求缓存
  5. // \think\middleware\CheckRequestCache::class,
  6. // 多语言加载
  7. // \think\middleware\LoadLangPack::class,
  8. // Session初始化
  9. \think\middleware\SessionInit::class
  10. ];

在首页app\controller\index.php使用session

  1. <?php
  2. namespace app\controller;
  3. use think\facade\Session;
  4. use app\BaseController;
  5. class Index extends BaseController
  6. {
  7. public function index()
  8. {
  9. Session::set('tp','thinkphp6');
  10. return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V6<br/><span style="font-size:30px">13载初心不改 - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';
  11. }

源代码修改好后抓包测试
img
执行后查看生成session文件,通过PHPSESSID可控制文件类型为php文件,如果反序列化内容包含一句话木马,就可以getshell。
img