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

2020-02-24 13:01:41 浏览数 (1)

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

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

具体漏洞位置:

vendortopthinkframeworksrcthinksessionStore.php 文件254行开始

代码语言:javascript复制
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 接口

我们搜索 SessionHandlerInterface

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

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

代码语言:javascript复制
 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 操作,其中的文件名和内容我们都可控,我们下一步要查看如何控制我们写入的值和文件名

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

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

代码语言:javascript复制
 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 行

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

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

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

代码语言:javascript复制
   public function index()
{
    if($_GET['code']){
        session('test', $_GET['code']);
        return 'ThinkPHP V6.0.0';
    }
    }

搭建好后使用以下 Payload:

代码语言:javascript复制
../../../../testgetshellvuln.php //在根目录下写入文件
../../../../public/shellvuln.php //写入public

成功 getshell

0 人点赞