1. 项目概述从一次内部渗透测试说起前段时间公司内部组织了一次针对自研和第三方系统的渗透测试竞赛。我抽到的目标之一是一个基于YZMCMS搭建的企业门户网站。在常规的端口扫描、目录爆破、弱口令尝试都收效甚微后我把目光转向了代码审计。毕竟对于这类使用广泛但可能疏于维护的CMS系统从源码层面寻找突破口往往是更高效的路径。而SSRFServer-Side Request Forgery服务端请求伪造漏洞由于其危害性大、利用场景多一直是我在代码审计时的重点排查对象。最终我确实在YZMCMS的某个功能模块中找到了一个典型的SSRF漏洞并成功利用它探测到了内网环境。这篇文章我就来详细拆解这次审计的全过程从漏洞原理、定位思路、到代码分析和利用构造希望能为同样关注应用安全的同行提供一个清晰的参考案例。简单来说SSRF漏洞就是攻击者能够诱使服务器应用程序向攻击者指定的任意地址发起HTTP请求。一个存在SSRF漏洞的服务器就像是一个被蒙上眼睛的壮汉攻击者可以指挥他去敲打任何一扇门内网服务甚至把门里的东西敏感数据搬出来。对于使用YZMCMS这类CMS的系统管理员和开发人员理解其代码中可能潜藏此类风险的点并掌握审计方法是加固系统安全的重要一环。2. SSRF漏洞核心原理与在CMS中的常见藏身之处在深入YZMCMS的代码之前我们必须先夯实理论基础明白要在代码的海洋里捞哪根“针”。2.1 SSRF漏洞的本质与危害链条SSRF的本质是“信任边界”的混淆。服务器端的代码本应只访问其信任的后端服务如数据库、缓存服务器、文件存储但当它接收了一个来自外部的URL参数并且未经充分校验和限制就直接用于发起网络请求时信任边界就被打破了。其危害主要沿着以下链条展开攻击内网服务这是最直接的危害。互联网服务器通常位于DMZ区其后方是庞大的、默认缺乏防火墙防护的内网。通过SSRF攻击者可以扫描内网的Web服务如192.168.1.1:8080、数据库服务如10.0.0.2:3306、Redis服务如172.16.0.5:6379等获取敏感信息或直接攻击脆弱的内部系统。本地文件读取许多网络请求库如PHP的file_get_contents()、curl支持file://协议。如果URL参数未过滤该协议攻击者可以构造file:///etc/passwd来读取服务器本地的敏感文件。协议滥用与回环攻击攻击者可能利用dict://、gopher://等协议与服务器本地的其他端口服务进行交互甚至构造特定Payload攻击本机上的Redis、Memcached等服务可能导致远程代码执行。绕过访问控制如果服务器配置了某些IP白名单例如管理后台只允许127.0.0.1访问攻击者可以通过SSRF以服务器本地的身份127.0.0.1去访问这些受保护的管理接口。注意在PHP环境中file_get_contents()对http://和file://协议的处理是透明的这使得它成为SSRF漏洞的“重灾区”。而curl库功能更强大支持的协议更多潜在风险也更大但同时也提供了更多的安全控制选项。2.2 CMS中SSRF的常见触发点审计清单根据经验在YZMCMS这类内容管理系统中SSRF漏洞通常潜伏在以下几个功能模块的代码里。审计时可以按图索骥数据采集/远程内容获取这是最高发区域。例如文章采集功能CMS允许管理员从其他网站采集文章通常会有一个输入框用于填写目标文章的URL。头像/图片远程设置用户设置头像时支持输入一个远程图片URL由服务器下载后保存到本地。网址二维码生成输入一个网址生成对应的二维码图片后端需要去请求这个网址。第三方API调用例如天气插件、内容校验插件等需要请求外部API但请求地址可能部分由用户控制。文件处理与导入Office文档在线预览服务器需要下载用户上传的、存放在远程的Word/Excel文档进行解析。XML解析如果系统有导入XML数据的功能并且XML内容中可以包含外部实体XXE这常常与SSRF相伴相生。网络诊断与管理员工具“Ping”或“路由追踪”工具提供给管理员测试网络连通性的功能可能直接执行系统命令或调用网络请求。网站可用性检查定时任务或手动功能检查一批网址是否可访问。审计时我们的核心策略就是全局搜索那些用于发起网络请求的关键函数然后回溯其参数是否用户可控以及可控参数是否经过了有效的过滤。PHP中需要重点搜索的函数/方法file_get_contents()fsockopen()curl_exec()/ 所有curl_*函数fopen()当用于打开URL时readfile()include()/require()在特定配置下如allow_url_includeOn时可用于远程文件包含这也是一种SSRF类方法如GuzzleHttp、Requests等第三方HTTP客户端库的请求方法。3. 对YZMCMS的代码审计实战定位与剖析我的审计环境是YZMCMS的一个较新版本。我首先从后台功能入手因为后台功能通常权限更高潜在的漏洞危害更大。3.1 第一步全局搜索与初步筛选我使用代码编辑器或命令行工具在YZMCMS的源码目录中对上述关键函数进行了全局搜索。# 例如使用 grep 进行搜索 grep -r file_get_contents --include*.php . grep -r curl_exec --include*.php .搜索file_get_contents的结果非常多大部分是用于读取本地模板文件、缓存文件等这些需要逐一排除。我重点关注的是参数中明显包含http或变量名称为$url、$link的调用。很快在/admin/目录下的一个名为collect.php的文件引起了我的注意。顾名思义这是“采集”功能模块。打开文件我直接搜索file_get_contents在其附近发现了如下代码段// 文件路径: /admin/collect.php function get_remote_content($url) { $ctx stream_context_create(array( http array( timeout 10, header User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\n ) )); $content file_get_contents($url, false, $ctx); if ($content FALSE) { return array(code0, msg获取远程内容失败); } return array(code1, data$content); }这是一个非常典型的“远程内容获取”函数。函数接收一个参数$url然后直接将其传递给file_get_contents()。虽然设置了超时和User-Agent但对$url协议、主机、端口没有任何校验和限制。这就是一个赤裸裸的SSRF风险点。3.2 第二步回溯参数传递链数据流跟踪找到风险函数只是第一步关键是看这个$url参数从何而来是否完全由用户控制。我向上追溯调用get_remote_content函数的地方。在同一文件中我找到了一个名为test_collect的函数或一个表单处理逻辑具体函数名因版本而异但逻辑类似// 假设的调用处 if ($action test) { $test_url $_POST[test_url]; // 直接来自用户POST输入 $result get_remote_content($test_url); // ... 将 $result 展示给用户 ... }或者在采集配置保存和测试时$collect_url trim($_POST[collect_url]); // ... 可能有一些简单的过滤比如去除空格 $html get_remote_content($collect_url);关键发现$url参数直接来源于$_POST[test_url]或$_POST[collect_url]在传入get_remote_content前代码仅做了trim()操作没有进行任何有效的URL安全校验。3.3 第三步漏洞确认与利用链分析至此漏洞已经基本确认。攻击者可以在后台的“采集测试”或“采集配置”页面输入一个恶意的URL服务器就会去请求它。构造利用Payload探测内网存活主机假设我们知道内网网段是192.168.1.0/24可以尝试http://192.168.1.1:8080。通过返回内容的差异错误信息、超时、页面内容来判断端口是否开放和服务类型。读取本地文件使用file://协议。例如file:///etc/passwd来读取Linux系统用户列表。也可以尝试file:///C:/Windows/System32/drivers/etc/hostsWindows。攻击元数据服务在云服务器环境中可以尝试访问云厂商的元数据服务地址如AWS的http://169.254.169.254/阿里云的http://100.100.100.200/以窃取实例的临时密钥等敏感信息。利用协议攻击其他服务构造dict://127.0.0.1:6379/info来探测本机Redis服务如果Redis未授权访问甚至可以进一步利用。实操心得在实际测试时我通常会先尝试http://127.0.0.1:80来验证漏洞是否存在请求本机Web服务。如果返回了本机Web服务器的默认页或错误页说明漏洞确实可利用。然后再系统性地进行内网探测。切记要在授权范围内进行测试。4. 漏洞修复方案与深度防御建议找到漏洞只是开始更重要的是如何修复和预防。对于这个具体的YZMCMS漏洞修复方案需要从代码层和架构层同时考虑。4.1 代码层即时修复修复的核心原则是白名单优于黑名单解析与校验优于简单过滤。方案一严格校验URL主机推荐修改get_remote_content函数在发起请求前对URL进行解析并校验主机名。function get_remote_content($url) { // 1. 解析URL $parsed_url parse_url($url); if (!$parsed_url || !isset($parsed_url[host])) { return array(code0, msg无效的URL); } $host $parsed_url[host]; $port isset($parsed_url[port]) ? $parsed_url[port] : 80; // 2. 定义允许访问的主机白名单根据业务需要 $allowed_hosts array( www.example.com, news.sina.com.cn, // ... 其他允许采集的源站 ); // 3. 校验主机和端口 if (!in_array($host, $allowed_hosts)) { return array(code0, msg不允许采集该站点的内容); } // 可选限制端口只允许80和443 if (!in_array($port, array(80, 443))) { return array(code0, msg仅支持HTTP/HTTPS标准端口); } // 4. 禁止非HTTP/HTTPS协议 $scheme isset($parsed_url[scheme]) ? strtolower($parsed_url[scheme]) : http; if (!in_array($scheme, array(http, https))) { return array(code0, msg仅支持HTTP和HTTPS协议); } // 5. 重建一个“安全”的URL防止通过畸形URL绕过 $safe_url $scheme . :// . $host . (($port80||$port443)?::.$port) . (isset($parsed_url[path])?$parsed_url[path]:) . (isset($parsed_url[query])??.$parsed_url[query]:); // 6. 使用安全的请求方式如CURL并设置更多限制 $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $safe_url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); // 禁止跟随重定向防止通过重定向跳转到内网 curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); // 限制协议 curl_setopt($ch, CURLOPT_RESOLVE, array()); // 防止DNS重绑定攻击高级防御 // 设置CURLOPT_DNS_USE_GLOBAL_CACHE为false也有助于缓解DNS重绑定 $content curl_exec($ch); $errno curl_errno($ch); if ($errno) { curl_close($ch); return array(code0, msg获取远程内容失败 . curl_error($ch)); } curl_close($ch); return array(code1, data$content); }方案二使用中间代理服务如果业务上需要采集的源站不固定无法使用白名单可以考虑引入一个安全的代理服务。用户提交URL后由后端一个独立的、网络受限的代理服务去获取内容。这个代理服务部署在独立的Docker容器或网络命名空间中其网络访问被严格限制例如只能访问外网或只能访问特定的几个IP。这样即使代理服务存在SSRF其危害范围也被限制在“监狱”内。4.2 架构与运维层深度防御网络隔离将Web服务器部署在独立的分区或VPC中严格限制其出站连接。通过安全组或防火墙策略只允许Web服务器访问其真正需要的后端服务如数据库、缓存和少数几个必要的外部API地址禁止访问整个内网段。使用虚拟化或容器隔离将存在风险的应用如CMS运行在容器或虚拟机中并配置其网络为“无特权”模式限制其访问宿主机网络或元数据服务的能力。统一HTTP客户端与安全中间件在项目架构中封装一个统一的、安全的HTTP客户端类。所有需要发起外部网络请求的地方都必须通过这个客户端。在这个客户端内部集中实现URL校验、DNS解析控制、协议限制、请求超时、重定向控制等安全逻辑。这样比在每个业务点修复更彻底。定期安全扫描与代码审计将SSRF检测纳入SAST静态应用安全测试和DAST动态应用安全测试的扫描规则中。定期对代码进行人工审计尤其是在新增涉及网络请求的功能时。5. 审计延伸其他潜在风险点与排查技巧在YZMCMS中除了collect.php我还顺藤摸瓜检查了其他可能存在类似问题的模块并总结出一些高效的排查技巧。5.1 其他可疑模块快速检查头像上传/远程设置查找处理用户头像的代码看是否存在通过URL下载头像的功能。二维码生成模块搜索“qrcode”、“二维码”等关键词看生成二维码时是否请求了用户提供的URL。第三方登录回调某些社交登录回调时可能会根据state或code参数去请求一个URL需要确认这个URL是否可控。管理员工具中的“网址检测”在后台管理面板中寻找类似“死链检测”、“友链检查”的功能。5.2 高效排查技巧实录技巧一关注“用户输入”与“网络函数”的交汇点。在代码审计工具或IDE中可以尝试追踪用户输入如$_GET$_POST$_REQUEST的变量传播路径看最终是否流入了file_get_contents、curl等函数。技巧二利用正则表达式进行精准搜索。单纯搜函数名噪音太大。可以尝试搜索模式如grep -r file_get_contents.*\$_ . --include*.php grep -r curl_init.*\$_ . --include*.php技巧三黑盒测试辅助定位。在授权的情况下可以先对目标系统进行黑盒测试。使用Burp Suite等工具拦截所有请求将参数值替换为http://your-burp-collaborator-domain或类似的SSRF测试域名观察是否有请求发出。如果发现请求再根据请求特征如User-Agent、路径去反查代码能极大提高效率。技巧四注意“二次跳转”型SSRF。有时用户输入的URL不是直接被请求而是先被保存到数据库然后由另一个后台任务如定时任务去读取并请求。这种漏洞更隐蔽需要审计整个数据处理链条。6. 总结与个人体会这次对YZMCMS的SSRF漏洞审计是一次非常典型的“由功能点切入追溯数据流定位风险函数”的代码审计过程。它再次印证了一个观点很多安全漏洞的根源不在于使用了某个“危险函数”而在于对不可信数据缺乏足够的校验和边界控制。对于开发者而言在处理任何来自用户输入的、将要用于发起网络请求的URL时必须抱有最大的不信任感。白名单机制是首选如果业务上无法实现则必须进行严格的解析、协议限制、端口限制并考虑在网络架构上进行隔离。对于安全研究人员和渗透测试人员CMS系统由于其功能复杂、代码量庞大且很多是基于开源项目二次开发往往遗留了已知漏洞或引入新的安全风险。以SSRF为例将其作为审计的常规检查项在测试采集、文件处理、远程下载、第三方集成等功能时多留一个心眼很可能会发现意想不到的突破口。最后修复漏洞不是终点。将这次审计中发现的问题模式化补充到团队的《安全编码规范》和《代码审计 Checklist》中才能在未来的开发中避免同类问题反复出现。安全是一个持续的过程需要开发、运维、安全团队的共同关注和努力。