从无到有分析thinkphp6 session文件操作漏洞

漏洞介绍:

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

该漏洞已经有分析文章:https://mp.weixin.qq.com/s/UPu6cE20l24T6fkYOlSUJw  但分析的不够详细,本文是对该漏洞点进行一个更详细的分析。

 

搭环境:

 1.安装composer,ubuntu下 apt install composer

 2.使用composer安装thinkphp

 3.解决composer下载速度慢问题

       composer config -g repo.packagist composer https://packagist.phpcomposer.com

 4.使用composer安装指定版本的thinkphp

       composer create-project topthink/think=6.0.1  tp6 --prefer-dist

       composer require topthink/framework:6.0.1

 5.进入tp目录运行 php  think  run

    

 

注意点:

   1.上述下载的代码是修复后的,可以手工修改会原先漏洞点。

   2.不能直接使用git clone 的源码。

 

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

 

漏洞代码点:

   ./vendor/topthink/framework/src/think/session/Store.php 此文件中定义了Store类,class Store{}。

public function setId($id = null): void

{

  //$this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());  //修复前

  $this->id = is_string($id) && strlen($id) === 32 && ctype_alnum($id) ? $id : md5(microtime(true) . session_create_id());

}

区别:

  修复前 检查$id的内容是32位的字符串即可。

  修复后 增加了$id必须是数字字母组合的检查。

 

解疑:

    这里用到了三目运算符 ?:  

   microtime() 函数返回当前 Unix 时间戳的微秒数。

   ctype_alnum() 检查提供的string,text 是否全部为字母和(或)数字字符。

 

查看前后的代码,发现存在save函数有文件操作,进行分析,调用了write和delete方法。

 

由于是write是属于handler的方法,在此Store类中找一下hander在哪,发在咋构造函数中SessionHandlerInterface $handler

   public function __construct($name, SessionHandlerInterface $handler, array $serialize = null)

    {
        $this->name    = $name;

        $this->handler = $handler;

        if (!empty($serialize)) {

            $this->serialize = $serialize;

        }

        $this->setId();
    }

解疑:

    SessionHandlerInterface是一个 接口,用于创建自定义会话。

    参考:https://www.php.net/manual/zh/class.sessionhandlerinterface.php

               https://blog.csdn.net/zyddj123/article/details/78906530

 

除此以外:

     拉到Store.php最上面可以看到看到

     use think\contract\SessionHandlerInterface;

    其内容如下,声明了接口,我们可以看一下,当然和追踪漏洞关系不大。

/**
 * Session驱动接口 
*/

interface SessionHandlerInterface
{
    public function read(string $sessionId): string;

    public function delete(string $sessionId): bool;

    public function write(string $sessionId, string $data): bool;

}

 

接下来我们继续追踪write函数,因为接口SessionHandlerInterface是需要被类继承使用的,因此可以全局搜索implements SessionHandlerInterface,定位到相关的文件及代码。(当然了,最简便的方法是直接在Stroe.php所在的session目录下发现File.php,里面有很多注释,表明是对session的相关操作,可以顺利找到相关代码)

 

    public function write(string $sessID, string $sessData): bool
    {
        $filename = $this->getFileName($sessID, true);
        $data     = $sessData;

        if ($this->config['data_compress'] && function_exists('gzcompress')) {
            $data = gzcompress($data, 3);  //数据压缩
        }
        return $this->writeFile($filename, $data);
    }

 

可以看到write函数调用了writeFile函数,继续看writeFile函数,注意到的$filename变成了$path

    protected function writeFile($path, $content): bool
    {
        //写文件加锁, LOCK_EX 标记可以防止多人同时写入
        return (bool) file_put_contents($path, $content, LOCK_EX);
    }

因此反向看调用关系,去找看看$path是否可控,最开始就是由原先的Store类中的setid方法产生,未对其做严格过滤。但在这一过程中发现与原文章内容不符,原文未考虑getFileName()对传入参数的影响。

 

我将整个调用的过程,简化成了下面这种形式:

调用关系:

    class Store -->setid()-->getId()-->save() --> class File --> write() -->getFileName()-->writeFile()

简明代码:

class Store{   

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


   public function getId(): string
    {
        return $this->id;
    }


   public function save(): void
    {
        $sessionId = $this->getId();
        $this->handler->write($sessionId, $data);
    }

}



class File implements SessionHandlerInterface {

   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 ($e) {}
        }

        return $filename;
    }


   public function write(string $sessID, string $sessData): bool
    {
        $filename = $this->getFileName($sessID, true);
        return $this->writeFile($filename, $data);
    }



   protected function writeFile($path, $content): bool
    {
        return (bool) file_put_contents($path, $content, LOCK_EX);
    }
}

 

        显然 ,这里存在getFileName会处理sessid,会在文件名前面添上“sess_”前缀,且该文件默认情况会被保存在./runtime/session目录下,该目录一般是访问不到的。可以构造/../../xxx这样的参数绕过限制。从而导致写入的文件名可控,实际的参数名称为PHPSESSID.

PHPSESSID=/runtime/session/sess_/../../xxx

 

扩展知识:

  PHP默认的session配置就能做到控制部分session名称,发送PHPSESSID=xxx内容,就会在session缓存目录创建sess_xxx 。

测试代码:

<?php

    session_start();

?>

请求:

在服务器上查看session文件,成功创建:

 

那能否通过插入/../../xxx来穿越目录并构造任意文件呢? 答案是否,会提示存在非法字符。

 

那么目前为止,可以穿越目录创建任意文件了,接下来再看$DATA从哪来。发现其内容依赖于后端代码的具体实现。

    public function setData(array $data): void
    {
        $this->data = $data;
    }

 

至此分析结束。

欢迎批评和交流。

 

------后来又看到了别人的分析文章,可以互相借鉴----

https://blog.csdn.net/zhangchensong168/article/details/104106869

https://blog.csdn.net/god_zzZ/article/details/104275241

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值