Struts2的OGNL表达式注入漏洞,本质上是一种利用框架对用户输入处理不当,导致攻击者可以绕过应用逻辑,直接在服务端执行任意代码的高危风险。OGNL全称是Object-Graph Navigation Language,它原本设计用来在Struts2标签和Java对象之间方便地传递和操作数据。问题在于,当框架把HTTP请求参数当作OGNL表达式的一部分进行解析时,如果这些参数未经严格过滤,攻击者就能构造恶意的OGNL语句,从而访问或修改服务器上的对象,甚至执行系统命令。
漏洞的根源:值栈与参数拦截机制要理解这个漏洞,必须先搞清楚Struts2的核心数据流转机制。Struts2通过一个叫“值栈”的对象来存储和传递Action中的属性值,OGNL表达式可以直接访问值栈中的对象。框架默认的拦截器栈里有一个叫“params”的拦截器,它会自动把HTTP请求中的参数名和参数值,通过OGNL表达式赋值给Action的属性。比如,一个请求参数名为user.name,值栈里有一个user对象,那么它的name属性就会被设置为参数值。这种自动绑定机制,在带来便利的同时,也打开了潘多拉魔盒。如果参数名本身就是一个可执行的OGNL语句,比如一个方法调用,那么赋值过程就会触发恶意代码的执行。这就是漏洞的核心成因。
历史漏洞演变:从S2-001到S2-062的攻防对抗Struts2的OGNL注入漏洞不是单一事件,而是一个持续了十多年的系列问题,每一次官方的修复,几乎都伴随着新的绕过技术出现。最早的S2-001漏洞,是因为在表单验证失败后,返回用户输入时,对OGNL表达式进行了二次解析,导致代码执行。真正让这个漏洞类型变得臭名昭著的是S2-003和S2-005,攻击者发现可以通过在参数名中使用Unicode编码或特殊字符,绕过XWork的安全过滤,直接执行OGNL语句。随后S2-007、S2-008、S2-009等漏洞接连爆发,每次都是对之前沙箱限制的突破。最严重的一次是S2-045,它利用了Jakarta Multipart解析器在处理文件上传时,对Content-Type头部的异常处理,将错误信息中的用户输入直接带入OGNL上下文执行,这个漏洞影响范围极广,利用方式极其简单,只需在HTTP头中插入恶意代码即可。后续的S2-046、S2-048、S2-057、S2-059、S2-061一直到S2-062,攻击手法愈发精妙,从利用重定向结果、到利用命名空间、再到利用标签的各种属性,每一次都是对OGNL沙箱规则的一次精准打击。这场攻防战的本质,是攻击者在不断寻找那些被开发者忽略的、能够将用户可控数据带入OGNL解析过程的隐蔽入口。
典型的漏洞利用代码剖析一个经典的S2-045漏洞利用请求,其HTTP头可能如下所示:
Content-Type: %{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())
这段代码的逻辑非常清晰:首先,它获取了OGNL上下文的默认成员访问权限,然后通过反射清空了框架禁止访问的包名和类名列表,也就是所谓的“沙箱逃逸”。接着,它根据操作系统类型,构造了执行命令的ProcessBuilder对象,最后将命令执行的结果输出到HTTP响应流中。这个利用链展示了一个核心思路:只要找到一个能把用户数据带入OGNL解析的入口点,攻击者就能通过反射和OGNL的强大对象操作能力,逐步突破所有限制,最终控制服务器。
沙箱机制与逃逸技术的猫鼠游戏Struts2官方为了应对OGNL注入,引入了沙箱机制。这个沙箱本质上是一系列黑名单,包括禁止访问的包名、类名和方法名。比如,默认禁止访问java.lang.ProcessBuilder、java.lang.Runtime等危险类。但问题在于,OGNL的表达能力太强了,攻击者可以通过很多方式绕过这些黑名单。常见的手法包括:利用OGNL的链式表达式和变量赋值功能,通过反射API动态获取和调用黑名单中的类;利用一些看似无害的类作为跳板,比如org.apache.commons.io.IOUtils、ognl.MemberAccess等;使用字符串拼接、编码等方式混淆类名和方法名,躲过静态检测;甚至利用JDK自身的类加载机制,动态加载字节码来执行任意代码。每一次沙箱的更新,都是对已知利用链的封堵,但攻击者总能找到新的、未被封堵的类和方法来构造利用链。这揭示了一个根本问题:基于黑名单的防御,在面对OGNL这种动态语言时,总是滞后和被动的。
深入理解漏洞触发点:不止是参数名很多开发者以为,只要对请求参数名进行过滤就够了,这是极其危险的误解。OGNL注入的触发点远不止GET或POST参数名。在Struts2中,任何被框架自动解析并带入OGNL上下文的数据都可能成为攻击向量。这包括但不限于:HTTP请求头,如Content-Type、Cookie等;文件上传时的文件名和Content-Type;表单中的参数值,当它们被用在某些返回页面的标签中时;URL中的路径和命名空间,特别是当框架使用了通配符映射或动态方法调用时;甚至是一些看似安全的拦截器或标签的内部处理逻辑。例如,S2-052漏洞就是因为Struts2的REST插件在处理XML数据时,将外部实体注入了OGNL上下文。因此,防御不能只盯着一两个点,必须理解整个数据流的每一个环节。
规避方案:从临时修补到架构级防御解决这个问题的终极方案,不是打补丁,而是从架构层面消除风险。最直接有效的做法是升级到最新的、官方明确声明修复了已知OGNL注入漏洞的Struts2版本。但仅仅升级是不够的,因为新的绕过方法可能随时被公开。更稳固的策略包括:
第一,彻底禁用动态方法调用。在struts.xml中设置struts.enable.DynamicMethodInvocation为false。这能堵住一大类利用URL映射的注入攻击。
第二,严格限制OGNL表达式解析的范围。如果你不需要在JSP页面之外的地方使用OGNL,可以考虑通过配置或自定义拦截器,禁止在参数名中使用OGNL语法。可以编写一个全局拦截器,检查所有请求参数名是否包含OGNL特殊字符,如括号、井号、问号等,一旦发现立即拦截请求。
第三,使用白名单而非黑名单。对于所有用户输入,无论是参数名、参数值还是HTTP头,都应该进行严格的输入验证。最好的方式是采用白名单策略,只允许已知安全的字符通过,拒绝一切其他字符。比如,参数名通常只应该包含字母、数字和下划线。
第四,最小化服务端对象的暴露。OGNL注入之所以危害巨大,是因为通过它可以访问到很多危险的对象。如果应用不需要直接访问Servlet上下文、会话等对象,可以通过配置拦截器栈,移除那些会自动将这些对象放入值栈的拦截器,如servletConfig拦截器。这能有效缩小攻击面。
第五,实施运行时应用自我保护。可以在应用层部署RASP技术,监控OGNL表达式的实际执行行为。当检测到表达式试图调用getRuntime、exec、ProcessBuilder等危险方法时,直接在运行时阻断操作并告警。这比静态黑名单更有效,因为它关注的是行为本身。
代码层面的安全实践在编写Struts2应用时,开发者必须时刻保持警惕。绝对不要在任何地方将用户输入直接拼接到OGNL表达式中。如果你在JSP中使用了<s:property value="%{...}" />这样的标签,并且括号内的内容有任何部分来自用户输入,那你就亲手制造了一个漏洞。所有需要在页面展示的用户数据,都应该通过普通的属性访问方式,而不是动态表达式。对于不得不使用动态表达式的场景,必须对用户输入进行极其严格的净化,移除所有可能改变表达式语义的特殊字符。另外,保持所有依赖库的更新也至关重要,不仅仅是Struts2核心库,还包括ognl.jar等底层依赖,因为这些库本身也可能存在漏洞。
检测与监控:你如何知道已被入侵对于线上系统,建立有效的检测机制至关重要。OGNL注入攻击通常会在HTTP请求中留下明显的痕迹。可以在WAF或应用日志中搜索特征字符串,如“ognl.OgnlContext”、“DEFAULT_MEMBER_ACCESS”、“java.lang.ProcessBuilder”、“getRuntime()”、“exec(”等。同时,监控服务器是否出现异常的对外连接或进程,因为攻击者执行命令后往往会下载恶意软件或进行反弹shell。在日志记录方面,确保记录完整的请求头信息,而不仅仅是URL和参数,因为很多攻击都隐藏在Content-Type或Cookie中。建立一个自动化的告警系统,当匹配到这些特征时立即通知安全团队。
总结:一个需要持续警惕的架构缺陷Struts2的OGNL表达式注入漏洞,本质上是其强大数据绑定功能带来的副作用。只要这个自动将HTTP请求映射到OGNL表达式的机制还存在,并且OGNL语言本身具备操作Java对象的能力,新的绕过方式就可能被发现。这不是一个可以通过一两个补丁彻底解决的问题,而是一个需要从开发规范、架构设计、部署运维等多个层面共同应对的持续性风险。对于仍在使用Struts2的企业,最负责任的做法是制定一个明确的迁移计划,逐步将应用转移到更现代、更安全的框架上。在迁移完成之前,必须建立一套纵深防御体系,从输入验证、沙箱配置、运行时监控到应急响应,层层设防,才能将风险控制在可接受的范围内。
