Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

  • A+
所属分类:代码审计

本文作者:1x2Bytes(信安之路红蓝对抗小组成员)

6.0.0 中有两个版本存在该漏洞, dev 版本只能覆盖任意位置的文件,6.0.0-1 则可以在特定的情况下控制写入的内容实现 getshell,看到一些师傅的 blog 的文章使用 composer 下载的源码, Thinkphp6 也确实开始使用 composer 的方式进行安装但是我使用 composer 方式下载的源码无法复现,猜测进行了修复,于是在网上找一键安装包,找了半天找到一个 11 月份的版本遂复现成功.`

具体漏洞位置:

vendortopthinkframeworksrcthinksessionStore.php 文件254行开始

public function save(): void{        $this->clearFlashData();        $sessionId = $this->getId();        if (!empty($this->data)) {            $data = $this->serialize($this->data);            $this->handler->write($sessionId, $data);        } else {            $this->handler->delete($sessionId);        }        $this->init = false;    }

这里 $this->handler->write($sessionId, $data)是漏洞的关键位置,handler的值我们从文件开头 53 行的__construct方法中可以看到 handler 是 SessionHandlerInterface 接口

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

我们搜索 SessionHandlerInterface

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

分别发现 File 类与 Cache 类都实现了该接口, 查看了 Cache 的 write 方法,并没有进行文件写入的操作,于是分析 File 中的 write 方法,看注释应该是跟 Session 操作相关,在文件vendortopthinkframeworksrcthinksessiondriverFile.php 的 210 行

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

$filename变量是从 getFileName 方法中获取,传入的值为 $sessID, 跟进该方法,在 File 文件的 117 行

 protected function getFileName(string $name, bool $auto = false): string{        if ($this->config['prefix']) {            // 使用子目录            $name = $this->config['prefix'] . DIRECTORY_SEPARATOR . 'sess_' . $name;        } else {            $name = 'sess_' . $name;        }        $filename = $this->config['path'] . $name;        $dir      = dirname($filename);        if ($auto && !is_dir($dir)) {            try {                mkdir($dir, 0755, true);            } catch (Exception $e) {                // 创建失败            }        }        return $filename;    }

这里判断是否有配置 session 文件的前缀,配置文件在config/session.php,如果存在配置则拼接到路径的最后并在 $name 前加上字符串sess_,不存在则直接拼接sess_前缀后返回文件名,最后 write 方法进行了 writeFile 操作,跟进 writeFile 方法,在文件 170 行进入 file_put_contents 操作,其中的文件名和内容我们都可控,我们下一步要查看如何控制我们写入的值和文件名

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

回到前面的 save 方法,传入的$sessionId变量是 getId 方法获取的,查看 getId 方法

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

该方法返回 id 的值,该值已经在 setId 方法中进行设置,于是查看 119 行的 setld 方法 

 public function setId($id = null): void{        $this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());    }

这里对 $id 的值进行了判断长度是否为 32 位,所以构造 payload 的时候要注意长度为 32

查找使用 setId 方法的文件,在vendortopthinkframeworksrcthinkmiddlewareSessionInit.php46 行

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

$varSessionId 变量的值从配置中获取session.var_session_id的值,因为 session.var_session_id默认是空 ,所以进入另一分支$sessionId变量的值由$request->cookie($cookieName)获取, $cookieName 由 $this->session->getName() 获取,查看 getName 方法

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

返回的值为 name,查看 name 变量的值在 Store 文件 36 行已经赋值,为 PHPSESSID

复现的时候要在 app/middleware.php 文件中开启即去除注释 thinkmiddlewareSessionInit::class然后在控制器中使用 Thinkphp 的 session 方法设定值,在 Index 控制器中修改 index 方法

   public function index(){    if($_GET['code']){        session('test', $_GET['code']);        return 'ThinkPHP V6.0.0';    }    }

搭建好后使用以下 Payload:

../../../../testgetshellvuln.php //在根目录下写入文件../../../../public/shellvuln.php //写入public

成功 getshell

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

本文始发于微信公众号(信安之路):Thinkphp 6 小于 6.0.2 任意文件创建覆盖漏洞分析

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: