0x00 漏洞简述

对于Struts2的RCE漏洞来说,有三个关键点,一个是找到传入ognl的表达式的点,一个是绕过ognl里面的沙盒,一个是尝试理解那些“这tm都调用ognl的函数”。

在struts-default.xml中,Struts2默认处理multipart报文的解析器是jakarta。因此使用默认的jakarta的版本,便会存在该漏洞。

0x01 漏洞触发原理

在Strust2中,StrutsPrepareAndExecuteFilter类会默认处理content-type字段。(Jakarta为Struts2上传默认使用库,对上传数据进行解析。)

S2-045 RCE漏洞利用的正是上面这一点。

在Dispatcher.class中,只要content-type含有multipart/form-data便会进入检查流程。

public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException {
    // don't wrap more than once
    if (request instanceof StrutsRequestWrapper) {
        return request;
    }

    String content_type = request.getContentType();
    if (content_type != null && content_type.contains("multipart/form-data")) {
        MultiPartRequest mpr = getMultiPartRequest();
        LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);
        request = new MultiPartRequestWrapper(mpr, request, getSaveDir(), provider, disableRequestAttributeValueStackLookup);
    } else {
        request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);
    }

    return request;
}

在FileUploadBase.class类中的FileItemIteratorImpl函数

FileItemIteratorImpl(RequestContext ctx)
            throws FileUploadException, IOException {
        if (ctx == null) {
            throw new NullPointerException("ctx parameter");
        }

        String contentType = ctx.getContentType();
        if ((null == contentType)
                || (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {
            throw new InvalidContentTypeException(
                    format("the request doesn't contain a %s or %s stream, content type header is %s",
                           MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));
        }
        ...

由于ctx.getContentType()并没有获取到contentType,导致丢出一个异常,而这个异常就是拼接了恶意代码的异常。

而JakartaMultiPartRequest.class类的parse方法的buildErrorMessage函数会调用这个e。

public void parse(HttpServletRequest request, String saveDir) throws IOException {
    try {
        setLocale(request);
        processUpload(request, saveDir);
    } catch (FileUploadBase.SizeLimitExceededException e) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("Request exceeded size limit!", e);
        }
        String errorMessage = buildErrorMessage(e, new Object[]{e.getPermittedSize(), e.getActualSize()});
        if (!errors.contains(errorMessage)) {
            errors.add(errorMessage);
        }
    } catch (Exception e) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("Unable to parse request", e);
        }
        String errorMessage = buildErrorMessage(e, new Object[]{});
        if (!errors.contains(errorMessage)) {
            errors.add(errorMessage);

接下来的调用过程为

buildErrorMessage函数把e带入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);
}

LocalizedTextUtil类的findText方法会把e带入到getDefaultMessage函数内,然后进入buildMessageFormat函数。

if (message != null) {
           MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);

           String msg = formatWithNullDetection(mf, args);
           result = new GetDefaultMessageReturnArg(msg, found);
       }

先执行buildMessageFormat的TextParseUtil.translateVariables函数

public static String translateVariables(String expression, ValueStack stack) {
    return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, null).toString();
}

然后对 $ 和 % 开头的内容进行搜索,然后都搜索出的内容带入 findValue。

接下来的内容就是绕过ognl的沙盒和拼接更好的利用输出了。

##0x02 Bypass Struts2 2.3.31 ognl沙盒

细心的同学肯定已经发现,此次bypass沙盒的方式显然跟以前不一样,利用中给出现了两次三元表达式,其中第一个三元表达式是进行bypass ognl沙盒的方式。其中运用了两处特性。

  • ognl上下文中存在com.opensymphony.xwork2.ActionContext.container对象。
  • com.opensymphony.xwork2.ognl.OgnlUtil的实例提供了过于强大的方法。

ActionContext.container实例存在getInstance方法。Container,之后便可以利用getInstance方法建立OgnlUtil对象。OgnlUtil类提供了获取和设置的Ognl API。Class OgnlUtil 而其中的 getExcludedClasses() 和 getExcludedPackageNamePatterns() 分别可以获取ognl的禁用的类和禁用的包,这俩方法获取的对象是的数据结构是set,之后调用clear方法便可以清除这些限制内容,导致沙盒失效。

0x03 防护方法

1,升级Struts至最新版

2,检查content-type内容