
CVE-2020-2551 POC zur Verwendung im Internet
Test-POC (für externe Netztests)
python CVE-2020-2551.py [HOST] [IP]
Versuch, codebase zu ändern, um Klassen remote zu laden (fehlgeschlagen)
python CVE-2020-TEST.py [HOST] [IP]
(Erstveröffentlichung auf Anquanke, Originalartikel)
Um diese Schwachstelle zu verstehen, benötigt man einiges an Vorwissen, z. B. über CORBA und RMI.
Ein kurzer Überblick:
CORBA ist ein von der OMG festgelegter technischer Standard für verteilte Anwendungen. Dabei wird IDL für die sprachübergreifende Unterstützung verwendet; zwischen Client und Server wird über das IIOP-Protokoll kommuniziert.
RMI ist eine weitere Technologie für verteilte Anwendungen. In Java kann sie mit JNDI vereinfacht genutzt werden; Client und Server kommunizieren über das JRMP-Protokoll. In WebLogic verwendet RMI jedoch das T3-Protokoll, zu dem es bereits einige Schwachstellen gab.
RMI-IIOP vereint die Vorteile von RMI und CORBA und ermöglicht die Bereitstellung von RMI-Anwendungen über das IIOP-Protokoll.
Die offizielle Dokumentation erwähnt ebenfalls:
RMI-Serverobjekte können das IIOP-Protokoll verwenden und mit in beliebiger Sprache geschriebenen CORBA-Clientobjekten kommunizieren.
Lassen wir WebLogic zunächst beiseite und schauen wir uns an, wie man ein RMI-IIOP-Beispiel schreibt:
Der Client-Code orientiert sich an dem Testprojekt aus RMI, JNDI, LDAP, JRMP, JMX, JMS in Java (Teil 1). Man kann HelloClient und HelloServer selbst kompilieren oder die bereits kompilierten Klassen aus dem Testprojekt verwenden.
Starte den Namensserver in der Befehlszeile (in Java enthalten):
start orbd -ORBInitialPort 1050
Starte HelloServer in der Befehlszeile und konfiguriere Remote-Debugging. Wie man mit IDEA Remote-Debugging durchführt, kann man in der zu Beginn von diesem Artikel erwähnten Methode nachlesen.
Natürlich kann man auch ohne Remote-Debugging einfach das Ergebnis ansehen und direkt starten:
java HelloServer
Client in der Befehlszeile starten:
Java HelloClient
Daraufhin öffnet sich der Taschenrechner. Ist das Remote-Debugging erfolgreich, sieht man den folgenden Aufrufstapel:
In EvilMessage.readObejct() wird der Befehl ausgeführt.

Nebenbei: Artikel zur Installation und zum Debugging von WebLogic.
Wie sieht es nun mit RMI-IIOP in WebLogic aus? In dem Artikel Über RMI-IIOP in Java wird die Ausnutzung von WebLogic RMI-IIOP erwähnt. Auf dieser Grundlage habe ich einige Untersuchungen angestellt. Using WebLogic’s RMI over IIOP beschreibt mehrere Möglichkeiten, wie WebLogic RMI-IIOP-Clients verwendet, darunter:
Der Unterschied zwischen den ersten beiden Ansätzen scheint nur in der JNDI_FACTORY-Einstellung zu liegen.
Als ich mich zuvor mit der T3-Deserialisierung in WebLogic beschäftigt habe, habe ich auf WebLogic die Anwendung Helloserver bereitgestellt, deren sayhello()-Methode sich ausnutzen ließ. Ich habe beide JNDI_FACTORY-Einstellungen versucht: Mit der zweiten JNDI_FACTORY konnte sayhello() erfolgreich aufgerufen werden.
Daraufhin habe ich den POC für das WebLogic-T3-Protokoll angepasst. Im Grunde wurde nur RMI durch IIOP ersetzt. Dabei zeigte sich, dass die jtaTransactionManager-Nutzungskette erfolgreich ausgeführt wurde und eine JRMP-Anfrage an das lokale jrmplisten gesendet wurde.
Schaut man sich den Datenverkehr an, wird beim Aufruf der Methode remove() eine remove__java_lang_Object-Anfrage gesendet. Der Datenverkehr enthält bösartige Daten, aber es wurde kein aced-Magic-Header gefunden.
Vermutlich werden die Daten auf dem Server erst speziell geparst und dann deserialisiert. Ein Blick auf den Aufrufstapel zeigt, dass die zweite Hälfte der Ausführungskette der zuvor gezeigten nativen RMI-IIOP-Kette sehr ähnlich ist. Dort wurde die Kette durch CDRInputStream.read_value() ausgelöst, hier durch IIOPInputStream.read_value() in WebLogic. (Auch in der Präsentation von 2019 wurde auf diesen Punkt read_value hingewiesen.)
Die Anfrage wird hier zunächst von clusterableServerRef.invoke() verarbeitet, das je nach Invoker this.invoker.invoke() aufruft. Anschließend wird Mejb_dj5nps_HomeImpl_WLSkel.invoke() aufgerufen. Da es sich um „remove“ handelt, wird der Zweig case 6 betreten und IIOPInputStream.readObject() aufgerufen. In der Methode read_value() werden die IIOPInputStream-Daten geparst und die Deserialisierung ausgelöst. Das ist der POC zur Ausnutzung der Methode remove().
Lucifaers [Analyseartikel](https://lucifaer.com/2020/02/25/WebLogic WLS核心组件RCE分析(CVE-2020-2551)/?from=timeline&isappinstalled=0#2-2-Weblogic解析流程) erwähnt die Ausnutzung über die Methode bind() – das ist auch die im Internet gängigste Methode. Verfolgen wir den Aufrufstapel.

Wie zuvor wird die Anfrage zuerst von clusterableServerRef.invoke() verarbeitet, das je nach Invoker this.invoker.invoke() aufruft. Hier wird CobraServerRef.invoke() aufgerufen. In _NamingContextAnyImplBase._invoke() wird – da va1 den Wert „bind_any“ hat – der Zweig case 0 betreten und die Methode IIOPInputStream.read_any() aufgerufen. Anschließend wird weiterhin IIOPInputStream.read_value() aufgerufen, um die Deserialisierung auszulösen. Dass zuvor im Datenverkehr kein aced-Magic-Header zu sehen war, liegt daran, dass IIOPInputStream über eine eigene Parsemethode verfügt. Die Hex-Value-Form von IIOPInputStream sieht folgendermaßen aus und enthält Klassennamen und Feldinformationen:

Letztendlich wird die Methode readObejct() der bösartigen Klasse aufgerufen.

Ein Blick auf den Patch zeigt, dass er sich an derselben Stelle befindet wie der Patch für die T3-Deserialisierungsausnutzung von 2015.

In den Tests der WebLogic-CVE-2020-2551-Schwachstellenanalyse ist die Position der von CVE-2020-2551 gefilterten Klasse ebenfalls in der Klasse weblogic.iiop.Utils zu sehen.

Bei lokalen Tests mit WebLogic 10.3.6 und dem Patch von 2015 wurde die Funktion isBlacklisted() jedoch nicht ausgelöst. (In MsgAbbrevInputStream und InboundMsgAbbrev wird isBlacklisted() allerdings jeweils zur Blacklist-Prüfung aufgerufen – seltsam …)

Der Patch für CVE-2020-2551 fügt in weblogic.iiop.Utils.LoadClass() die Filterfunktion verifyclassermitted() hinzu.

Die Blacklist filtert bösartige Klassen, darunter die Oberklasse von JtaTransactionManager, com.bea.core.repackaged.springframework.transaction.support.AbstractPlatformTransactionManager. Diese Klasse ist in WebLogic enthalten und sehr gefährlich. Beim Ansehen des Patches kam mir ein Gedanke: Da die Prüfung in Zeile 606 erst nach LoadClass() erfolgt – wäre die Verteidigung nicht umgangen, wenn bereits beim Laden des Klassennamens eine Klasse geladen und ein bösartiger statischer Initialisierungsblock ausgeführt würde? Dazu später mehr.
Der in Java geschriebene POC hat Netzwerkprobleme: Er funktioniert direkt gegen einen lokalen WebLogic-Dienst, aber nicht gegen Docker-Container oder externe Maschinen. Analyseartikel zu diesem Problem:
Hands-on-Anleitung zur Lösung der Netzwerkprobleme des WebLogic-CVE-2020-2551-POCs
Eine Betrachtung von WebLogic-CVE-2020-2551
Im Folgenden wird der POC debuggt. Man kann sich an dem zuvor erwähnten remove()-POC orientieren oder an dem von Y4er. Die beiden Artikel nennen zwei Lösungsansätze:
Ich habe beide Ansätze ausprobiert. Nach dem Neupacken von WebLogic tritt der Fehler java.lang.NoSuchMethodError:weblogic.security.subject.SubjectManager.installCESubjectManager auf, aber ich habe keine Lösung gefunden.
Also habe ich versucht, das IIOP-Protokoll zu simulieren, und zunächst im POC einen Haltepunkt gesetzt, um ihn zu debuggen.

Es zeigt sich, dass beim Aufruf von new InitialContext(env) in EndPointImpl.sendReceive() zwei Pakete gesendet und empfangen werden.

LocateReply enthält IOR-Informationen. Hier muss man verstehen, was ein IOR ist. Seine Aufgabe besteht darin, dem RMI-IIOP-Client bei der Interaktion mit Serverobjekten über das IIOP-Protokoll den für die IIOP-Kommunikation benötigten Host und Port zu liefern. Zusätzlich dient der rot umrandete Teil Object_key dazu, verschiedene Objekte auf dem Server zu unterscheiden.

Beim Simulieren des IIOP-Protokolls ist Object_key besonders wichtig. Host und IP sind dagegen nicht wirklich relevant. Als ich zu Beginn der Tests einfach alle Pakete erneut abspielte, gab resolve_any ein location forward zurück.

In der offiziellen GIOP-Dokumentation heißt es, dass sich bei location forward der Object_key ändern kann; bei verschiedenen Anfragen können unterschiedliche Object_key-Werte zurückgegeben werden. (Der hier genannte Object_key ist die key address im Datenpaket.) Dieser Object_key – wie bereits erwähnt – dient bei der Kommunikation über das IIOP-Protokoll zur Unterscheidung, mit welchem Objekt kommuniziert wird. Dieser Wert muss dynamisch aus LocateReply ermittelt werden.

Letztlich habe ich mich hier nicht für die Simulation von remove() entschieden, sondern für die Simulation der IIOP-Anfrage der Methode bind(), da diese weniger Anfragen umfasst. Schauen wir uns die Datenpakete der normalen lokalen Ausnutzung an.

LocateRequest senden, data empfangen und per regulärem Ausdruck die key address aus LocateReply ermitteln.

Manuell die Adresse des bösartigen JRMP-Servers (rmi://...) setzen und im 1-Sekunden-Takt das bind_any-Paket senden. Da die JRMP-Anfrage aus dieser Nutzungskette nicht über DGCClient läuft, ist sie nicht von JEP290 betroffen und kann über jrmplisten ausgenutzt werden.

Der POC wurde in einer Docker-Umgebung erfolgreich getestet. Man kann die vulhub-SSRF-Umgebung verwenden und die IP auf den Host setzen; der Docker-Container empfängt daraufhin erfolgreich die JRMP-Anfrage des Hosts. Der konkrete Code liegt auf Github.

Weiter oben wurde die Idee erwähnt, die Erkennung zu umgehen, indem man über codebase entfernten Code lädt. Wer sich mit JNDI-Angriffen beschäftigt hat, weiß: codebase kann verwendet werden, um den Speicherort entfernter Klassen anzugeben. Wenn codebase kontrollierbar ist und das Programm das Laden entfernter Klassen erlaubt, kann man eine entfernte bösartige Klasse laden und den bösartigen Code in ihrem statischen Initialisierungsblock ausführen.
Beim Lesen des Codes wird deutlich: Der zweite Parameter von weblogic.iiop.Utils.lodaClass() repräsentiert codebase. Dieser Parameter wird in IIOPInputStream.read_value() als Parameter var8 gelesen. In Zeile 1659 wird readIndirectingRepositoryId(var8) aufgerufen, was schließlich weblogic.iiop.Utils.lodaClass() aufruft. Damit die Zeilen 1644 und 1659 ausgeführt werden, müssen (va4r & 1)=1 und (va4 & 6)=2 gelten, also ist var4 gleich 3.
Der Aufrufstapel von readIndirectingRepositoryId bis getClassFromId endet damit, dass in Zeile 304 loadclass() ausgeführt wird.

Schaut man sich das bind_any-Datenpaket an, besteht es im Grunde aus einem GIOP-Header und einem GIOP-Request. Der GIOP-Request enthält außerdem die key address (übereinstimmend mit LocateteReply), ServiceContextList und stub_data. Der Wert von var4 ist das \x7f\xff\xff\x02 in stub_data; daher gilt (va4r & 1)=0 und (va4 & 6)=2, und die Zeile 1644 zum Setzen von codebase wird nicht ausgeführt.

Wir ändern das \x7f\xff\xff\x02 im ersten markierten Bereich zu \x00\x00\x00\x03 und fügen im zweiten markierten Bereich die Länge und den Wert von codebase hinzu. Der konkrete Code liegt auf Github. Wie man sieht, wird dabei auch eine Ausrichtung (Alignment) durchgeführt – das ist eine Stolperfalle. Denn bevor die nachfolgenden Klasseninformationen gelesen werden, wird geprüft, ob die Position des nächsten Bytes ein Vielfaches von 4 ist. Ist das nicht der Fall, werden einige Bytes ignoriert. Ist die Position des nächsten Bytes beispielsweise 1, werden 3 Bytes ignoriert und es wird direkt ab Position 4 gelesen. Diese Position bezieht sich auf das gesamte bind_any-Paket. Ist die Byteposition kein Vielfaches von 4, wird mit Nullen aufgefüllt.

Es gibt noch ein Problem: Schaut man sich den zuvor genannten Aufrufstapel von readIndirectingRepositoryId bis getClassFromId an, führt er über die Funktion findClassInfo(). Wenn eine Klasse bereits geladen wurde, werden die Klassen-ID-Informationen dort gespeichert, und findClassInfo() gibt die Klasseninformationen direkt zurück, ohne in die Funktion weblogic.iiop.Utils.getClassFromID() zu gelangen.

Deshalb muss bei jedem Test der Klassenname geändert werden.

Wie auch immer, letztlich ist es gelungen, das IIOP-Protokoll zu simulieren, den codebase-Wert zu ändern und die Funktion weblogic.iiop.Utils.getClassFromId() auszuführen.
Unglücklicherweise wird beim Abrufen von RMIURLClassFinder NULL zurückgegeben, und die Funktion RMIEnvironment.getEnvironment().isNetworkClassLoadingEnabled() gibt false zurück.

Der Grund ist, dass der Parameter _NetworkClassLoadingEnable in ServerMBeanImpl den Wert False hat.

Ich wollte herausfinden, in welcher WebLogic-Konfigurationsdatei dieser Parameter gesetzt wird, habe aber keine gefunden …
Beim Lernen über diese Schwachstelle habe ich festgestellt, dass man viel Vorwissen benötigt – z. B. über Java-Deserialisierung, RMI, JNDI und ähnliche Themen. Zum Weiterlernen kann man diese Artikelserie ansehen. Obwohl der Versuch, codebase für die Ausnutzung zu modifizieren, am Ende fehlgeschlagen ist, habe ich dennoch viel gelernt.