
1. 从一次审计中的“意外”说起为什么原生类反序列化值得单独拎出来讲很多做代码审计或者CTF的同行第一次接触PHP反序列化时注意力几乎都放在__destruct、__wakeup、__toString这些魔术方法上觉得只要把POP链拼出来就万事大吉。但真正在实战里摸爬滚打过一段时间之后你会发现有一类利用方式根本不需要你自己去构造POP链——PHP内置的那些原生类本身就带着可以被串联起来的魔术方法。这就是“PHP原生类反序列化”这个主题的核心价值所在。简单来说PHP原生类反序列化指的是利用PHP语言自带的内置类比如SoapClient、DirectoryIterator、FilesystemIterator、SplFileObject、SimpleXMLElement、Error、Exception等在反序列化场景下触发它们内部的魔术方法从而实现SSRF、任意文件读取、XSS、甚至代码执行等效果。它解决的核心问题是当目标代码里找不到用户自定义类的POP链时我们依然有牌可打。适合谁看适合已经理解PHP反序列化基础原理、想进一步拓宽利用面的人也适合做CTF Web方向、想补全原生类利用知识盲区的选手。我个人的体会是原生类反序列化的门槛其实比自定义POP链要低因为不需要你去读大量业务代码找链但它对PHP内置类行为的熟悉程度要求更高。你得清楚哪个类在__destruct时会做什么、哪个类在__toString时会输出什么、哪个类在__call时会发起网络请求。这篇文章就把我这些年积累的原生类利用思路、踩过的坑、以及实际测试中验证过的细节系统地梳理一遍。2. 原生类反序列化的底层逻辑魔术方法才是真正的入口2.1 反序列化触发的本质是魔术方法的自动调用要理解原生类反序列化得先把反序列化的触发机制说透。unserialize()函数在还原对象时并不是简单地把属性填回去就完事了它会根据对象的状态自动调用一系列魔术方法。比如对象被销毁时调用__destruct对象被当成字符串使用时调用__toString对象被当成函数调用时调用__invoke访问不可访问属性时调用__get等等。原生类之所以能被利用就是因为PHP内置的这些类在实现时内部也定义了这些魔术方法而且它们的实现逻辑往往带有“副作用”——比如发起网络连接、读取文件内容、输出数据。这些副作用在正常业务里是功能在攻击者手里就是利用点。我经常用一个类比来解释自定义类的POP链像是你自己组装的一串多米诺骨牌每一张牌都得你自己摆而原生类反序列化像是直接推倒一摞厂家已经摆好的骨牌你只需要找到第一张牌在哪就行。2.2 为什么原生类利用在实战中经常被忽略原因有几个。第一很多教程讲反序列化时默认用自定义类举例导致初学者形成了“反序列化找POP链”的思维定式。第二原生类的魔术方法行为在PHP手册里写得比较分散需要跨多个页面查阅。第三部分原生类的利用条件比较苛刻比如需要开启特定扩展SOAP扩展、需要特定PHP版本。但恰恰是因为被忽略原生类利用在某些场景下反而成了“奇兵”。我在一次内部代码审计演练中就遇到过这样的情况目标应用里几乎没有任何自定义类反序列化入口却真实存在最后就是靠SoapClient打出了一条SSRF链路。2.3 利用原生类前必须确认的三个前提在动手之前有三个前提条件必须逐一确认否则后面全是白费功夫。反序列化入口是否可控也就是unserialize()的参数是否部分或完全来自用户输入。常见入口包括Cookie、POST参数、缓存数据、Session内容等。目标PHP版本与扩展不同PHP版本对原生类的行为有差异某些类在特定版本才引入或修改了魔术方法实现。SOAP类需要php_soap扩展SimpleXMLElement需要php-xml扩展。魔术方法是否会被触发即使你构造了对象如果代码流程中没有触发对应魔术方法的操作利用也不会生效。比如__destruct在脚本结束时会触发但__toString需要对象被当成字符串使用。提示在实际测试中建议先用一个最简单的phpinfo()页面或者本地环境确认目标PHP版本和已加载扩展这比盲目构造payload要高效得多。3. SoapClient原生类SSRF利用的“头号选手”3.1 SoapClient的__call机制与SSRF的关联SoapClient类在反序列化利用中的地位大概相当于自定义POP链里的“万能入口”。它的核心利用点在于__call魔术方法当你调用一个SoapClient对象上不存在的方法时__call会被触发而SoapClient的__call实现会尝试向location属性指定的URL发起SOAP请求。这意味着什么意味着如果你能控制反序列化后的SoapClient对象的location属性就能让它向任意地址发起HTTP请求。这就是一条典型的SSRF链路。构造思路大致是这样的先创建一个正常的SoapClient对象然后用序列化字符串手动修改其location属性最后把修改后的payload传给目标的unserialize()。当目标代码后续调用了这个对象上任意一个不存在的方法时__call触发请求就发出去了。3.2 构造SoapClient payload时最容易踩的坑这里有几个细节我在实际测试中反复踩过。第一个坑是user_agent属性。SoapClient在发起请求时会带上user_agent如果这个字段为空或者格式不对某些服务端可能会拒绝请求。更关键的是在CTF场景下user_agent经常被用来绕过一些简单的WAF规则因为你可以把payload藏在User-Agent头里。第二个坑是location的协议。SoapClient默认使用SOAP协议但你可以通过修改location为http://或https://来让它发起普通HTTP请求。不过要注意如果目标环境禁用了http流包装器这条路就走不通。第三个坑是CRLF注入。SoapClient的user_agent属性如果包含换行符可以实现HTTP请求头的注入这在某些场景下能构造出更复杂的请求。但PHP不同版本对换行符的处理不一样需要实测。?php // 构造SoapClient反序列化payload的基本思路 $target http://example.com/; $client new SoapClient(null, array( location $target, uri http://test.com, user_agent test\r\nX-Custom: injected )); // 序列化后手动调整属性或直接使用序列化字符串 echo serialize($client); ?3.3 一个完整的SSRF验证流程假设你已经确认目标存在反序列化入口并且php_soap扩展已开启。完整的验证流程是这样的在本地构造SoapClient对象设置location为你控制的监听地址。序列化该对象得到payload字符串。将payload通过目标的反序列化入口传入。在监听端观察是否收到请求。如果收到请求说明SSRF成立可以进一步尝试访问内网地址。我通常会在监听端用nc -lvnp 8080或者起一个简单的HTTP服务来观察请求。如果目标环境有出网限制可以尝试访问内网地址如127.0.0.1、192.168.x.x等观察响应时间差异来判断端口开放情况。注意SoapClient的利用需要目标代码在反序列化之后调用了该对象上的某个方法。如果代码只是反序列化但没有后续调用__call不会触发。这一点和__destruct不同需要特别留意。4. 文件操作类原生类DirectoryIterator与SplFileObject的读取能力4.1 DirectoryIterator的__toString为何能泄露目录内容DirectoryIterator这个类在反序列化利用中的价值主要体现在它的__toString方法上。当你把一个DirectoryIterator对象当成字符串使用时它会返回当前指向的文件名。如果配合__destruct或者循环遍历就能列出目录内容。但这里有个关键限制DirectoryIterator的构造函数需要一个路径参数而反序列化时构造函数不会被调用。所以你不能直接通过反序列化创建一个指向任意目录的DirectoryIterator。不过在某些场景下如果代码在反序列化后对对象进行了操作比如遍历仍然可能触发信息泄露。实际测试中DirectoryIterator更多是作为POP链中的一环来使用而不是单独利用。它的__toString可以返回文件名这在构造文件包含或者路径拼接的场景下很有用。4.2 SplFileObject读取文件内容的实际效果SplFileObject是另一个值得关注的原生类。它继承自SplFileInfo并且实现了__toString方法返回的是当前读取的行内容。这意味着如果你能控制一个SplFileObject对象指向某个文件并且在反序列化后触发了__toString就能读取文件内容。但同样的问题构造函数不会被调用所以你不能直接通过反序列化指定文件路径。不过SplFileObject的序列化格式中包含了文件名信息如果你能手动构造序列化字符串理论上可以设置任意路径。?php // SplFileObject的序列化格式示例 $file new SplFileObject(/etc/passwd); echo serialize($file); // 输出类似O:13:SplFileObject:1:{s:12:\0*\0filename;s:11:/etc/passwd;} ?注意那个\0*\0前缀这是PHP序列化中表示protected属性的格式。手动构造时如果格式不对反序列化会失败。4.3 文件类原生类利用的边界条件我在实际测试中总结了几条经验DirectoryIterator和SplFileObject的利用高度依赖后续代码是否触发了__toString。如果代码只是反序列化后存起来不进行字符串操作就不会触发。这两个类在PHP 7和PHP 8中的行为基本一致但PHP 8对序列化格式的校验更严格手动构造的payload更容易失败。如果目标开启了open_basedir限制文件读取的范围会被限制在指定目录内利用价值大打折扣。5. 报错与异常类Error和Exception的信息泄露与XSS5.1 Error类的__toString输出机制Error类是PHP 7引入的它实现了__toString方法当对象被当成字符串时会输出错误信息包括错误消息、文件路径、行号、调用栈等。在反序列化场景下如果你能控制Error对象的message属性就能让__toString输出任意内容。这个特性在CTF中经常被用来构造XSS。比如如果目标应用把反序列化后的对象直接输出到页面上而对象是Error类型那么__toString返回的内容就会被浏览器解析。如果message属性里包含script标签就形成了XSS。?php // 构造Error对象进行XSS $error new Error(scriptalert(1)/script); echo serialize($error); // 反序列化后如果被输出脚本就会执行 ?5.2 Exception类的类似利用方式Exception类和Error类在这一点上非常相似都实现了__toString都会输出消息内容。区别在于Exception是PHP 5就有的而Error是PHP 7才引入的。在实际利用中两者可以互相替代。但要注意Exception的序列化格式中包含了trace、file、line等属性手动构造时需要确保这些属性的格式正确否则反序列化可能失败。我通常的做法是先在本地用PHP生成一个标准的序列化字符串然后只修改message部分。5.3 信息泄露类原生类的实战价值评估说实话Error和Exception的利用价值取决于具体场景。如果目标只是把反序列化结果存到数据库或者日志里不输出到页面那XSS就打不出来。但如果目标有输出点这就是一个非常直接的利用方式。信息泄露方面Error的__toString会输出文件路径和行号这在信息收集阶段很有用。你可以通过构造特定的Error对象让目标泄露Web根目录、框架路径等敏感信息。提示在PHP 8中Error和Exception的__toString输出格式有细微变化比如调用栈的展示方式不同。测试时最好在相同版本的环境下验证。6. SimpleXMLElement与XXE原生类里的XML利用面6.1 SimpleXMLElement反序列化触发XXE的条件SimpleXMLElement这个类在反序列化利用中比较特殊。它的构造函数接受一个XML字符串并解析它。如果XML中包含外部实体引用且目标环境没有禁用外部实体加载就会触发XXE。但问题在于反序列化时构造函数不会被调用。所以你不能直接通过反序列化让SimpleXMLElement去解析恶意XML。不过SimpleXMLElement的序列化格式中包含了XML数据如果反序列化后代码对该对象进行了某些操作比如asXML()可能会触发解析。实际测试中SimpleXMLElement的利用条件比较苛刻需要目标代码在反序列化后主动调用相关方法。但一旦条件满足XXE的危害是很大的可以读取文件、发起SSRF等。6.2 与其他XML类原生类的配合思路除了SimpleXMLElementPHP还提供了DOMDocument、XMLReader等XML处理类。这些类在反序列化场景下的利用思路类似都是通过控制XML内容来触发解析行为。我个人的经验是XML类原生类的利用在实战中比较少见因为需要目标代码配合。但在CTF中如果题目明确涉及XML处理这就是一个值得尝试的方向。7. 原生类利用的实战排查链路从入口到验证7.1 第一步确认反序列化入口和可控程度排查的第一步永远是找入口。常见的反序列化入口包括Cookie中的序列化数据POST参数中的序列化字符串Session文件内容缓存数据如Redis、Memcached中的值数据库中的序列化字段找到入口后要确认可控程度。是完全可控还是只能控制部分字段这决定了你能构造什么样的payload。7.2 第二步探测PHP版本和可用扩展这一步很关键。不同PHP版本对原生类的支持不一样。比如Error类需要PHP 7SoapClient需要php_soap扩展。你可以通过报错信息、phpinfo()页面、或者构造特定payload观察响应来推断。我常用的一个技巧是构造一个引用不存在类的序列化字符串观察报错信息。报错信息中通常会包含PHP版本号。7.3 第三步逐个尝试原生类payload确认环境后就可以逐个尝试原生类payload了。建议的顺序是SoapClient如果php_soap开启优先尝试SSRF。Error/Exception尝试XSS或信息泄露。SplFileObject/DirectoryIterator尝试文件读取。SimpleXMLElement尝试XXE。每尝试一个都要观察响应差异。有时候payload没有生效不是因为类不可用而是因为魔术方法没被触发。7.4 第四步根据响应调整payload如果第一次没成功不要急着放弃。检查以下几点序列化字符串的格式是否正确特别是protected和private属性的前缀。魔术方法是否真的被触发了可以在本地模拟目标代码的流程。是否有WAF拦截尝试编码绕过或分块传输。我在一次测试中SoapClient的payload一直不生效后来发现是目标代码在反序列化后对对象进行了类型检查非预期类型的对象会被丢弃。这种情况下就需要结合其他漏洞点来绕过。8. 那些年我踩过的原生类反序列化坑8.1 序列化字符串中属性前缀写错导致反序列化失败这是最常见的坑。PHP序列化中public属性直接写属性名protected属性前面加\0*\0private属性前面加\0类名\0。手动构造payload时如果前缀写错反序列化会失败或者属性设置不上去。我建议的做法是先在本地用PHP生成标准序列化字符串然后用文本编辑器修改需要改的部分不要从头手写。8.2 魔术方法触发时机与预期不符另一个常见的坑是魔术方法没触发。比如你构造了一个SoapClient对象但目标代码只是$obj unserialize($input);然后就没有后续操作了。这种情况下__call永远不会触发SSRF也就打不出来。解决办法是仔细阅读目标代码找到反序列化后对对象的操作。如果确实没有操作可以尝试寻找其他入口或者结合其他漏洞。8.3 PHP版本差异导致的payload失效PHP 7和PHP 8在序列化格式上有一些差异。比如PHP 8对类型的要求更严格某些在PHP 7下能用的payload在PHP 8下会报错。另外PHP 8.1引入了readonly属性对序列化也有影响。我的建议是尽量在目标相同版本的环境下测试payload。如果无法确定版本准备多个版本的payload备用。8.4 扩展未开启导致类不存在SoapClient需要php_soap扩展SimpleXMLElement需要php-xml扩展。如果目标环境没有开启这些扩展对应的类就不存在payload会直接报错。排查方法是构造一个引用该类的序列化字符串观察是否报“Class not found”错误。如果是说明扩展未开启换其他类尝试。9. 原生类反序列化的防御视角开发侧应该注意什么9.1 避免直接反序列化用户输入最根本的防御措施是不要对用户输入直接调用unserialize()。如果业务确实需要序列化考虑使用JSON等更安全的格式。JSON不支持对象反序列化天然避免了这类问题。如果必须使用PHP序列化可以考虑使用hash_hmac对序列化数据进行签名反序列化前先验证签名。这样即使攻击者构造了恶意payload也无法通过签名验证。9.2 限制unserialize的类白名单PHP 7引入了unserialize()的第二个参数$options其中allowed_classes可以指定允许反序列化的类白名单。设置allowed_classes为false可以禁止所有类的反序列化设置为数组则只允许指定类。?php // 只允许反序列化MyClass $data unserialize($input, [allowed_classes [MyClass]]); ?这个措施能有效阻止原生类反序列化利用因为原生类不在白名单里。9.3 禁用危险扩展和函数如果业务不需要SOAP可以禁用php_soap扩展。同样如果不需要XML处理可以禁用相关扩展。此外可以通过disable_functions禁用unserialize等危险函数但要注意这会影响正常业务。注意防御措施需要根据实际业务权衡。过度限制可能导致业务功能受损建议在测试环境充分验证后再上线。10. 关于原生类反序列化我个人的几点体会写了这么多最后分享几点个人体会。第一原生类反序列化不是银弹它的利用条件比自定义POP链更依赖环境。在实战中我通常把它作为“备选方案”当自定义链走不通时再尝试。第二熟悉PHP内置类的行为需要时间积累建议平时多翻PHP手册特别是那些实现了魔术方法的类。第三本地环境测试非常重要不要直接在目标上试payload容易打草惊蛇。另外随着PHP版本的更新一些原生类的行为在变化。比如PHP 8对序列化的校验更严格某些老payload可能失效。保持学习及时更新知识库是这个领域的基本功。如果你也在做代码审计或者CTF建议把原生类反序列化作为一个专项来练习。找一些经典的CTF题目手动构造payload观察每一步的响应。练得多了遇到实际场景时就能快速反应出该用哪个类、怎么构造。这个技能一旦掌握在反序列化相关的场景里你的利用面会比别人宽很多。