Headline
SimpleRmiDiscoverer 0.1
SimpleRmiDiscoverer is a JMX RMI scanning tool for unsecured (without enabled authentication) instances of JAVA JMX. It does not use standard Java RMI/JMX classes like other available tools but rather communicates directly over TCP. The tool is written in Java and is very useful in red teaming operations because JVM is still ubiquitous in corporate environments. It can be executed by unprivileged (non-admin) users.
// Simple JMX scanner extracting data (IP:Port) from RMI Registry and verifying RCE (using MLet) exploitability without credentials.// Version: 0.1// Sample use of: https://rabobank.jobs/en/techblog/security-risk-infrastructure/jmx-the-best-buddy-of-every-red-teamer-marcin-wolak/// Author: Marcin Wolak// LinkedIn: https://www.linkedin.com/in/marcinwolak/// Medium: https://marcin-wolak.medium.com/// Github: https://github.com/marcin-wolak/// Tested successfuly against JMX enabled on Java x64:// Java(TM) SE Runtime Environment (build 1.7.0_80-b15) on Windows 10// Java(TM) SE Runtime Environment (build 1.8.0_202-b08) on Windows 10// Java(TM) SE Runtime Environment (build 9.0.4+11) on Windows 10// OpenJDK Runtime Environment (build 11.0.17+8-post-Ubuntu-1ubuntu222.04)// OpenJDK Runtime Environment (build 17.0.5+8-Debian-2)import java.net.*;import java.io.InputStream;import java.io.OutputStream;import java.lang.System;import java.util.*;import java.io.DataInputStream;import java.io.ByteArrayInputStream;import org.apache.commons.cli.CommandLine;import org.apache.commons.cli.Option;import org.apache.commons.cli.Options;import org.apache.commons.cli.OptionBuilder;import org.apache.commons.cli.CommandLineParser;import org.apache.commons.cli.DefaultParser;import org.apache.commons.cli.ParseException;import org.apache.commons.cli.HelpFormatter;public class SimpleRmiDiscoverer {// IP Address of the RMI Registry to query (CMD Parameter)private String regIP;// TCP Port of the RMI Registry to query (CMD Parameter)private int regPort;// Ignore endpoint from RMI Registry to verify exploitability without credentials. Use instead IP of the registry itself and dynamic TCP port from the registry dump (CMD Parameter)private boolean ignoreEndpoint;// Extract Host:Port from the RMI Registry without checking RCE exploitability without credentials (CMD Parameter)private boolean dumpOnly;// IP Address and TCP port for the endpoint exposed via RMI Registryprivate String endPIP;private int endPPort;// Sockets for communication with RMI Registry and exposed endpointprivate Socket regS;private Socket endPS;//Streams for communication via above socketsprivate InputStream in, inEndP;private OutputStream out, outEndP;// RMI Handshake dataprivate byte[] hShake1 ={0x4A,0x52,0x4D,0x49,0x00,0x02,0x4B};private byte[] hShake2 ={0x00,0x09,0x31,0x32,0x37,0x2E,0x30,0x2E,0x30,0x2E,0x31,0x00,0x00,0x00,0x00};// Calling Objprivate byte[] hObj ={0x50,(byte)0xac,(byte)0xed,0x00,0x05,0x77,0x22,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x44,0x15,0x4d,(byte)0xc9,(byte)0xd4,(byte)0xe6,0x3b,(byte)0xdf,0x74,0x00,0x06,0x6a,0x6d,0x78,0x72,0x6d,0x69};//Pattern to find in dumped registry (UnicastRef)private byte[] ucRef = {0x55,0x6e,0x69,0x63,0x61,0x73,0x74,0x52,0x65,0x66};//Pattern to find in exception dump (Credentials required)private byte[] credReq = {(byte)0x43,(byte)0x72,(byte)0x65,(byte)0x64,(byte)0x65,(byte)0x6e,(byte)0x74,(byte)0x69,(byte)0x61,(byte)0x6c,(byte)0x73,(byte)0x20,(byte)0x72,(byte)0x65,(byte)0x71,(byte)0x75,(byte)0x69,(byte)0x72,(byte)0x65,(byte)0x64};// Calling Obj (after modification) on the dynamic port private byte[] hDObj ={(byte)0x50,(byte)0xac,(byte)0xed,(byte)0x00,(byte)0x05,(byte)0x77,(byte)0x22,(byte)0x6d,(byte)0xf9,(byte)0xa5,(byte)0x39,(byte)0xb4,(byte)0xfa,(byte)0x06,(byte)0xc3,(byte)0xea,(byte)0x8e,(byte)0xa7,(byte)0x0e,(byte)0x00,(byte)0x00,(byte)0x01,(byte)0x85,(byte)0x06,(byte)0x13,(byte)0xa1,(byte)0x4d,(byte)0x80,(byte)0x01,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xff,(byte)0xf0,(byte)0xe0,(byte)0x74,(byte)0xea,(byte)0xad,(byte)0x0c,(byte)0xae,(byte)0xa8,(byte)0x70};private String endpIP;private short enpPort;//javax.management.remote.rmi.RMIServerImpl_Stub object details collected from RMI registry:private byte[] rmiStub;int rmiStubLength;int eIPlength;byte [] eIP;byte [] ePort;// Constructor. Parameter dumps determines if only dynamic endpoint should be extracted from RMI Registry without checking if JMX is username/password protected// Ignore detrmines if the IP of endpoint extracted from registry should be ignored and the IP of RMI Registry used insteadpublic SimpleRmiDiscoverer(String rIP, int rPort, boolean ignore, boolean dump) { regPort = rPort; regIP = rIP; rmiStub = new byte[4096]; ignoreEndpoint = ignore; dumpOnly = dump;}// findArray taken from: https://stackoverflow.com/questions/3940194/find-an-array-inside-another-larger-array public int findArray(byte[] largeArray, byte[] subArray) { /* If any of the arrays is empty then not found */ if (largeArray.length == 0 || subArray.length == 0) { return -1; } /* If subarray is larger than large array then not found */ if (subArray.length > largeArray.length) { return -1; } for (int i = 0; i < largeArray.length; i++) { /* Check if the next element of large array is the same as the first element of subarray */ if (largeArray[i] == subArray[0]) { boolean subArrayFound = true; for (int j = 0; j < subArray.length; j++) { /* If outside of large array or elements not equal then leave the loop */ if (largeArray.length <= i+j || subArray[j] != largeArray[i+j]) { subArrayFound = false; break; } } /* Sub array found - return its index */ if (subArrayFound) { return i; } } } /* Return default value */ return -1; }// Connecting to RMI Registry using IP/TCP pair from CLI private int connectRegistry(){ try { //System.out.println("IP: " + regIP); regS = new Socket(regIP, regPort); in = this.regS.getInputStream(); out = this.regS.getOutputStream(); } catch (Exception e) { System.out.println("[!] Cannot connect to RMI Registry."); //e.printStackTrace(); return 0; } return 1;}// RMI handshake. Common function for handshaking with RMI Registry as well as the endpoint extracted from it private int rmiHandShake(InputStream inP, OutputStream outP){ byte [] buffer = new byte[4096]; try { outP.write(hShake1, 0, hShake1.length); int count = inP.read(buffer); if(count==0) { System.out.println("[!] Handshake failed, no date received. Likely not RMI."); return -1; } else { if(buffer[0]!=0x4E) { System.out.println("[!] Handshake failed, bad response. Likely not RMI."); return -2; } outP.write(hShake2, 0, hShake2.length); } } catch (Exception e) { System.out.println("[!] Handshake failed, Error in RMI Communication."); //e.printStackTrace(); return 0; } return 1;}// Invoking RMIServerImpl_Stub over RMI Registryprivate int invokeObject(){ try { out.write(hObj, 0, hObj.length); int rmiStubLength = in.read(rmiStub); if(rmiStubLength==0) { System.out.println("[-] RMIServerImpl_Stub invocation against RMI Registry failed, no response!"); return -1; } //String s = new String(rmiStub); //System.out.println(s); //Findinf "UnicastRef" in the response from RMI Registry: int unipos = findArray(rmiStub, ucRef); if(unipos<1) { System.out.println("[-] RMIServerImpl_Stub invocation against RMI Registry failed, Malformed registry dump."); return -2; } //else // System.out.println("\nThe position is: " + unipos); eIPlength = (int) rmiStub[unipos+11]; //System.out.println("\nLength of eIP is: " + eIPlength); eIP = Arrays.copyOfRange(rmiStub,unipos+12,unipos+12+eIPlength); endPIP = new String(eIP); //System.out.println("\nThe eIP is: " + endPIP); ePort = Arrays.copyOfRange(rmiStub,unipos+12+eIPlength,unipos+16+eIPlength); DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(ePort)); endPPort = inputStream.readInt(); //System.out.println(endPPort); //System.out.println("\nThe ePort is: " + Integer.valueOf(new String(ePort))); } catch (Exception e) { System.out.println("[-] RMIServerImpl_Stub invocation against RMI Registry failed, Other Error."); // e.printStackTrace(); return 0; } return 1;}// Connecting to the endpoint extracted from RMI registry or (when ignore option active) to RMI Registry IP and TCP port extracted from this Registry.private int connectEndPoint(boolean ignoreEndPoint){ try { //System.out.println("End Point IP: " + endPIP); //System.out.println("End Point Port: " + endPPort); System.out.println("[+] Connecting to the dynamic Endpoint ... "); if(!ignoreEndPoint){ endPS = new Socket(endPIP, endPPort); } else { System.out.println("[+] Ignoring the endpoint exposed on: " + endPIP); System.out.println("[+] Connecting instead to: " + regIP + " on TCP Port:" + Integer.toString(endPPort)); endPS = new Socket(regIP, endPPort); } inEndP = this.endPS.getInputStream(); outEndP = this.endPS.getOutputStream(); } catch (Exception e) { // e.printStackTrace(); System.out.println("[-] Connection to the dynamic Endpoint failed!"); return 0; } System.out.println("[+] Dynamic Endpoint connected successfuly!"); return 1;}// Invoking RMIServerImpl_Stub over the endpoint extracted from RMI registry or (when ignore option active) over RMI Registry IP and TCP port extracted from this Registry.private int invokeDObject(){ int unipos = findArray(rmiStub, ucRef); System.arraycopy(rmiStub, unipos + 16 + endPIP.length(), hDObj, 7, 8); System.arraycopy(rmiStub, 8, hDObj, 15, 4); System.arraycopy(rmiStub, 16, hDObj, 23, 4); byte [] buffer = new byte[4096]; try { outEndP.write(hDObj, 0, hDObj.length); int dStubLength = inEndP.read(buffer); if(dStubLength==0) { System.out.println("[-] Invoking RMIServerImpl_Stub on the dynamic Endpoint failed, no response."); return -1; } String s = new String(buffer); if(s.contains("Credentials required")){ System.out.println("[-] Dynamic Enpoint requires credentials (username and password)."); } else if(s.contains("UnicastRef")){ System.out.println("[+] Dynamic Enpoint is VULNERABLE to RCE, NO username and password required."); } else { System.out.println("[-] Unknown outcome from invoking RMIServerImpl_Stub on the dynamic Endpoint. See below:"); System.out.println(s); } } catch (Exception e) { e.printStackTrace(); return 0; } return 1;}private void closeSocket(Socket inSock){ try{ inSock.close();} catch (Exception e) { //e.printStackTrace(); }}public static void main(String[] args) throws Exception{ boolean ignore=false; boolean donly=false; String IP = ""; long Port = 1099; Options options = new Options(); options.addOption("h", "help", false, "Prints help for the tool."); options.addOption("i", "ignore", false, "Uses RMI registry IP for methods invocations. Ignores endpoint from the Registry dump."); options.addOption("d", "dumponly", false, "Extracting endpoint <host:port> from RMI Registry without checking exploitabilty, which is by default checked."); options.addOption(OptionBuilder.withLongOpt("host").withDescription("IP of RMI Registry to query.").hasArg().withArgName("RMI HOST IP ").isRequired().create("H")); options.addOption(OptionBuilder.withLongOpt("port").withDescription("TCP port of RMI Registry to query.").hasArg().withArgName("RMI HOST TCP PORT ").isRequired().create("P")); //options.addOption("H", "--host", false, "IP of RMI Registry to query."); //options.addOption("P", "--port", false, "TCP port of RMI Registry."); CommandLineParser parser = new DefaultParser(); String header = "SimpleRmiDiscoverer extracts JMX host:port endpoint from RMI registry and checks if is exploitable without credentials using MLet.\n\n"; String footer = "\nPlease check my Blog for more details: https://marcin-wolak.medium.com/"; HelpFormatter formatter = new HelpFormatter(); try { CommandLine cmd = parser.parse(options, args); if (cmd.hasOption("h")) { formatter.printHelp("SimpleRmiDiscoverer", header, options, footer, true); System.exit(0); } if (cmd.hasOption("i")) { ignore = true; } if (cmd.hasOption("d")) { donly = true; } if (cmd.hasOption("H")) { IP = cmd.getOptionValue("H"); } if (cmd.hasOption("P")) { try { Port = Long.valueOf(cmd.getOptionValue("P")); } catch (Exception e){ System.out.println(e.getMessage()); System.out.println("[!] Parameter -P --port must be a number between 1 and 65535!\n\n"); formatter.printHelp("SimpleRmiDiscoverer", header, options, footer, true); System.exit(1); } if(Port<0 || Port>65535){ System.out.println("[!] Parameter -P --port out of range!\n\n"); formatter.printHelp("SimpleRmiDiscoverer", header, options, footer, true); System.exit(0); } } } catch (ParseException e){ System.out.println(e.getMessage()); formatter.printHelp("SimpleRmiDiscoverer", header, options, footer, true); System.exit(1); } System.out.println("[+] Approached RMI Registry IP: " + IP); System.out.println("[+] Approached RMI Registry TCP Port: " + Port); SimpleRmiDiscoverer sRD = new SimpleRmiDiscoverer(IP, (int) Port, ignore, donly); sRD.start(); } public int start() { // can now access non-static fields int isConnected = connectRegistry(); if(isConnected == 0){ System.out.println("[-] Aborting Connection to RMI Registry! Quiting!"); closeSocket(regS); return -1; } int isHShaked = rmiHandShake(in,out); if(isHShaked != 1){ System.out.println("[-] Aborting handShaking with RMI Registry! Quiting!"); closeSocket(regS); return -2; } int isInvoked = invokeObject(); if(isInvoked != 1){ System.out.println("[-] Object Calling Error, RMI Registry! Quiting"); closeSocket(regS); return -2; } System.out.println("[+] RMI Registry contacted successfuly!"); System.out.println("[+] Extracted Endpoint Name / IP Address: " + endPIP); System.out.println("[+] Extracted Endpoint Dynamic TCP Port: " + endPPort); closeSocket(regS); if(dumpOnly) { System.out.println("[+] RMI Registry Dump Completed Successfuly!"); System.exit(0); } int isEConnected = connectEndPoint(ignoreEndpoint); if(isEConnected != 1){ System.out.println("[-] Aborting!"); closeSocket(endPS); return -3; } isHShaked = rmiHandShake(inEndP, outEndP); if(isHShaked != 1){ System.out.println("[-] Cannot handShake with the dynamic Endpoint!"); closeSocket(endPS); return -2; } int isDInvoked = invokeDObject(); if(isDInvoked != 1){ System.out.println("[-] Cannot Invoke RMIServerImpl_Stub on the dynamic Endpoint!"); closeSocket(endPS); return -4; } closeSocket(endPS); return 1; } }