La vulnerabilità emersa all’interno di Log4j, l’utility di Apache per il logging scritta in linguaggio Java (CVE-2021-44228), è stata definita come “la vulnerabilità più critica dell’ultimo decennio”: conosciuta anche come Log4Shell, questa criticità ha costretto gli sviluppatori di numerosi prodotti software a rilasciare aggiornamenti o mitigazioni ai loro clienti. E da quando è stata scoperta la vulnerabilità, i maintainer di Log4j hanno pubblicato due nuove versioni – la seconda della quale ha completamente rimosso la funzione che aveva originariamente reso possibile l’exploit.
Come è già stato fatto notare, Log4Shell è un exploit legato alla funzione di “sostituzione messaggi” di Log4j, che permette(va) di modificare programmaticamente il log degli eventi inserendo stringhe formattate in modo da richiamare contenuti esterni. Il codice alla base di questa funzione consentiva anche di effettuare ricerche o “lookup” usando URL JNDI (Java Naming and Directory Interface).
Questa funzione ha tuttavia dato inavvertitamente la possibilità a un attaccante di inserire testo contenente URL JNDI pericolosi all’interno delle richieste inviate al software che utilizza Log4j, con la conseguenza di far caricare ed eseguire il codice remoto dal logger. Per renderci meglio conto della pericolosità degli exploit di questa funzione analizzeremo ora il codice che li rende possibili.
Indice degli argomenti
Come funziona Log4j
Log4j produce gli eventi di logging usando TTCCLayout: orario, thread, categoria e informazioni di contesto. Per default utilizza il seguente pattern:
%r [%t] %-5p %c %x – %m%n
In questo caso %r stampa il tempo in millisecondi trascorso dal momento dell’avvio del programma; %t indica il thread, %p la priorità dell’evento, %c la categoria, %x il contesto diagnostico associato al thread che ha generato l’evento e %m è riservato al messaggio associato all’evento, fornito dall’applicazione.
È proprio in quest’ultimo campo che entra in gioco la vulnerabilità.
La vulnerabilità può essere sfruttata quando la funzione logger.error() viene chiamata passando come parametro un messaggio comprendente un URL JNDI (jndi:dns://, jndi:ldap:// o una qualunque delle altre interfacce JNDI discusse nel nostro post precedente). Nel momento in cui viene passato l’URL, il software esegue un “lookup” JNDI che può provocare l’esecuzione di codice remoto.
Per replicare questa vulnerabilità possiamo vedere uno dei numerosi PoC (Proof of Concept) pubblicati, che dimostra come molte applicazioni interagiscono con Log4j. Nel codice logger/src/main/java/logger/App.java usato in questo PoC, notiamo come logger.error() venga chiamato con un parametro di tipo messaggio:
package logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.logger;
public class App {
private static final Logger logger = LogManager.getLogger(App.class);
public static void main(String[] args) {
String msg = (args.length > 0 ? args [0] : “”);
logger.error(msg);
}
}
A scopo di debugging abbiamo cambiato il messaggio con un URL di test (creato con il tool Interactsh) che utilizza DNS con JNDI per passarlo come parametro alla funzione logger.error(), seguendo quindi lo svolgere passo passo del programma:
Possiamo notare come, dopo aver chiamato il metodo logger.error() della classe AbstractLogger con l’URL creato apposta, venga chiamato un altro metodo, logMessage:
Il metodo log.message crea un oggetto messaggio con l’URL che gli è stato passato:
Dopodiché chiama processLogEvent dalla classe LoggerConfig per registrare l’evento:
La chiamata successiva riguarda il metodo append della classe AbstractOutputStreamAppender, che aggiunge il messaggio al log:
Ecco dove si verifica il problema
A sua volta, questo metodo chiama il metodo directEncodeEvent:
E il metodo directEncodeEvent chiama il metodo getLayout().Encode, che formatta il messaggio per il log aggiungendovi il parametro che gli è stato passato – che, in questo caso, non è altro che l’URL che avevamo creato per sfruttare la vulnerabilità:
Viene quindi creato un nuovo oggetto StringBuilder:
StringBuilder chiama il metodo format della classe MessagePatternConvert ed esegue il parsing dell’URL fornito alla ricerca dei caratteri ‘$’ e ‘{’ per identificare l’URL effettivo:
Dopodiché prova a identificare svariati nomi e valori separati da ‘:’ o ‘-’:
La successiva chiamata al metodo resolveVariable della classe StrSubstitutor identifica le variabili, che possono essere una o più delle seguenti:
{date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j}
A questo punto, il codice chiama il metodo lookup della classe Interpolator per controllare il servizio associato alla variabile (che in questo caso è jndi):
Avendo trovato jndi, il codice chiama il metodo lookup della classe jndiManager, che valuta quanto contenuto nella risorse JNDI:
Dopodiché viene chiamato il metodo getURLOrDefaultInitCtx della classe IntialContext. Qui viene creata la richiesta che sarà successivamente inviata all’interfaccia JNDI per recuperare le informazioni di contesto, a seconda dell’URL che è stato passato. In questo esatto punto inizia a prendere forma l’exploit. Nel nostro caso, l’URL riguarda il servizio DNS:
Specificando un URL del genere, con Wireshark possiamo vedere che viene inviata una query DNS all’URL che avevamo fornito (i tratta di un URL di test, non pericoloso):
Qualora l’URL sia jndi:ldap://, viene chiamato un altro metodo della classe ldapURLConext per verificare la possibilità che l’URL abbia queryComponents:
Dopo aver chiamato il metodo lookup della classe ldapURLContext, la variabile name contiene l’URL ldap:
Per connettersi quindi all’URL ldap fornito:
Viene chiamato il metodo flushBuffer della classe OutputStreamManager, qui buf contiene i dati restituiti dal server LDAP, in questo caso la stringa mmm…. che possiamo osservare qui sotto:
Osservando i pacchetti catturati con Wireshark, possiamo constatare come la richiesta sia composta dai seguenti bytes:
Si tratta dei dati serializzati che verranno visualizzati dal client, come possiamo vedere qui sotto dove la vulnerabilità è stata messa a frutto: si noti la stringa [main] ERROR logger.App all’interno del messaggio seguita da dati:
Problema risolto
Tutto questo è stato possibile perché in tutte le versioni di Log4j 2 fino alla 2.14 (escludendo la release di sicurezza 2.12.2), il supporto di JNDI non era limitato in termini di nomi che potevano essere risolti. Alcuni protocolli non erano sicuri o rendevano possibile l’esecuzione di codice remoto. Log4j 2.15.0 limita JNDI ai soli lookup LDAP, e tali ricerche sono ulteriormente limitate per default a connettersi agli oggetti primitivi Java residenti sull’host locale.
La versione 2.15.0 ha tuttavia lasciato parzialmente irrisolta la vulnerabilità, perché per le implementazioni dotate di “certi layout pattern non di default” per Log4j, come quelli con lookup di contesto (come “$${ctx:loginId}”) o con un pattern Thread Context Map (“%X”, “%mdc” o “%MDC”), era ancora possibile definire dati di input mediante un pattern JNDI Lookup tale da provocare un attacco Denial of Service (DoS).
Nelle ultime release tutti i lookup sono stati disabilitati per default. In questo modo la funzione JNDI è stata interamente rimossa, ma ciò evita che Log4j possa essere utilizzato per exploit remoti.
In conclusione
Log4j è un framework per logging molto diffuso e utilizzato da numerosi prodotti software, servizi cloud e altre applicazioni.
Le vulnerabilità presenti nelle versioni precedenti la 2.15.0 permettono a un malintenzionato di recuperare i dati da un’applicazione o dal relativo sistema operativo sottostante, piuttosto che eseguire codice Java che gira con lo stesso livello di autorizzazioni attribuito al runtime Java stesso (Java.exe sui sistemi Windows).
Questo codice può eseguire comandi e script sul sistema operativo locale scaricando quindi ulteriore codice pericoloso e spianando la strada all’elevazione dei privilegi e ad accessi remoti persistenti.
Sebbene la versione 2.15.0 di Log4j, rilasciata nel momento in cui la vulnerabilità è divenuta di pubblico dominio, risolva questi problemi, essa lascia tuttavia aperta la porta ad exploit e attacchi Denial of Service (situazione risolta almeno parzialmente dalla versione 2.16.0).
Il 18 dicembre è stata rilasciata una terza versione, la 2.17.0, che previene possibili attacchi ricorsivi che potrebbero provocare un Denial of Service.
Le aziende dovrebbero verificare le versioni di Log4j presenti nelle applicazioni sviluppate internamente e provvedere a passare alle versioni più recenti (2.12.2 per Java 7 e 2.17.0 per Java 8), nonché applicare le patch software non appena vengono rilasciate dai rispettivi vendor.