Introduzione

Oggi vi vogliamo parlare della scoperta, da parte di un membro del team di Soter (Riccardo ‘p4w’ Krauter), del CVE-2021-40961 relativo ad una SQL-injection su CMS Made Simple sfruttabile da un utente autenticato con bassi privilegi. L’articolo ha l’obiettivo di mostrare come, anche una vulnerabilità apparentemente limitata, possa essere concatenta ad altre in modo da ottenere un forte impatto. Per ottenere questo risultato è stato fondamentale l’aiuto di Daniele ‘sk4’ Scanu che ha già scoperto, negli anni passati, numerose vulnerabilità su CMS Made Simple.

Discovering the SQL-injection

L’analisi è partita dai moduli di CMS MS, in partcolare è stato analizzato il modulo news. Questo modulo viene utlizzato per la pubblicazione di articoli sul portale, la cosa importante è che questo modulo può essere utlizzato anche da utenze con bassi privilegi (non amministrative). Attraverso l’analisi statica del codice sorgenete è stato possibile individuare una vulnerabilità di SQL-injection all’interno del file modules/News/function.admin_articlestab.php. Di seguito la porzione di codice interessata.

if( isset($params['submitfilter']) ) {
    //die("i'm here!");
    if( isset( $params['category']) ) {
        $this->SetPreference('article_category',trim($params['category']));
    }
    if( isset( $params['sortby'] ) ) {
        $this->SetPreference('article_sortby', str_replace("'",'_',$params['sortby']));
    }
    if( isset( $params['pagelimit'] ) ) {
        $this->SetPreference('article_pagelimit',(int)$params['pagelimit']);
    }
    $allcategories = (isset($params['allcategories'])?$params['allcategories']:'no');
    $this->SetPreference('allcategories',$allcategories);
    unset($_SESSION['news_pagenumber']);
    $pagenumber = 1;
}

....

$query1 = "SELECT SQL_CALC_FOUND_ROWS n.*, nc.long_name FROM ".CMS_DB_PREFIX."module_news n LEFT OUTER JOIN ".CMS_DB_PREFIX."module_news_categories nc ON n.news_category_id = nc.news_category_id ";
$parms = array();
if ($curcategory != '') {
    $query1 .= " WHERE nc.long_name LIKE ?";
    if( $allcategories == 'yes' ) {
        $parms[] = $curcategory.'%';
    }
    else {
        $parms[] = $curcategory;
    }
}
$query1 .= ' ORDER by '.$sortby;

Come è possibile osservare, l’applicazione prende un parametro controllabile da un utente di nome sortby, ed esegue dapprima una sanificazione mediante sostituzione del carattere apice ' (str_replace("'",'_',$params['sortby']));). Solo poche righe di codice dopo, tale parametro viene concatenato in una query SQL ($query1 .= ' ORDER by '.$sortby;). Il problema in questo caso è che, nonostante la sanificazione, è possibile iniettare codice senza far uso dell’apice (che verrebbe comunque rimosso). A questo punto non rimaneva che testare l’applicazione e verificare che la vulnerabilità fosse sfruttabile. Nella parte di codice iniziale potete notare che è stata inserita l’istruzione commentata //die("i'm here!"); questo è uno dei metodi veloci che utilizziamo per fare test dinamici e verificare di raggiungere una certa porzione di codice; infatti la funzione die termina lo script stampando la stringa passata come parametro. Navigando l’applicazione utilizzando il menù è possibile raggiungere la sezione News ed eventalmente generare la seguente richiesta POST.

img post-req

img post-req

img post-req

Come è ossibile osservare, ci sono due news presenti. Inoltre, nella richiesta POST c’è un parametro m1_sortby che risulta essere proprio quello valutato nella porzione di codice riportata precedentemente, ora l’obiettivo è arrivare quantomeno ad un PoC di SQL-injection. A tale proposito è utile tornare al codice sorgente e analizzare la query che viene eseguita. Infatti, è possibile iniettare codice SQL immediatemente dopo la keyword ORDER BY. Sfruttare una SQL-injection in una clausola ORDER BY è particolare; infatti, non è più possibile usare UNION, WHERE, OR e altre keyword in questo punto della query. Per l’exploit utilizzeremo una query annidata al posto del parametro della ORDER BY. Una query annidata è sostanzialmente una SELECT all’interno di un’altra istruzione. Un’altra cosa importante da notare attraverso l’analisi dinamica è che, anche nel caso in cui si inserisca un payload che generi una query sintatticamente errata, il CMS non è verboso nella risposta.

img post-req

img post-req

Come è possibile osservare nelle immagini soprastanti, se la query presenta un errore di sintassi, il CMS risponde con nessun articolo creato. Inoltre, non potendo usare lo UNION statement, non è possibile estrarre dati dal database aggiungendoli con quelli restituiti in pagina. In questo caso una valida alternativa per ottenere un PoC è quella di usare un payload time-based, ossia un payload di SQL che obbliga il database ad attendere per un periodo di tempo specificato prima di rispondere. Un payload time-based funzionante è il seguente:

(SELECT (CASE WHEN 1=1 THEN sleep(2) ELSE news_id END)) -- -

Questo payload contiene un costrutto CASE WHEN con una clausola vera 1=1. Nel caso in cui la condizione sia vera verrà eseguita la funzione sleep(); altrimenti se la condizione fosse falsa, verrebbe restituito il campo news_id. Nelle immagini sottostanti è possibile osservare che la vulnerabilità di SQL-injection esiste ed è sfruttabile quantomeno con una tecnica basata sul tempo.

img SQLi-poc

Infatti, la rispsota arriva al client sempre dopo 4 secondi, questo perchè il payload iniettato ha forzato il database a eseguire una sleep di 2 secondi per ogni risultato restituito (2 articoli). La tecnica time based ha però le sue limitazioni, prima di tutto l’estrazione dei dati dal DB risulta lenta proprio a casusa delle varie sleep che si aggiungono ai normali delay dovuti ai tempi di conessione ed elaborazione delle richieste HTTP tra client e server. Inoltre, potrebbe anche dare falsi positivi soprattutto se il tempo di sleep risulta essere troppo vicino a quello di una normale richiesta, ed è necessario quindi adattarlo bene alla situazione, altrimenti il rischio di avere degli errori nei dati estratti è alto. Preferibilmente sarebbe meglio poter contare su una tecnica del tipo blind-boolean, ossia una tecnica che fa uso dell’inferenza logica e si basa sull’invio di una query SQL al database che costringe l’applicazione a restituire un risultato diverso a seconda che la query restituisca un risultato VERO o FALSO. A tale proposito vecchie esperienze maturate grazie ai CTF sono tornate utili; infatti esiste un meteodo per forzare il DB ad andare in errore sfruttando l’overflow dei numerici. Potete trovare il writeup del CTF a questo indirizzo. In sostanza questa tecnica prevede il controllo di un errore del DBMS, ad esempio se la condizione è VERA allora viene volutamente mandato in errore il database, in caso contrario nessun errore viene forzato. La distinzione tra le diverse risposte offre la possibilità di eseguire l’estrazione dei dati dal db. Questa tecnica, è sicuramente più affidabile e veloce rispetto alla time-based.

img SQLi-poc

img SQLi-poc

img SQLi-poc

img SQLi-poc

Nelle immagini sopra è possibile osservare come la risposta del server cambia nel momento in cui si forza il DB a eseguire la funzione pow(99999,99999). Questo errore di overflow numerico è particolare; infatti, a differenza di errori di sintassi nella query SQL, fa restituire un 500 Internal Server Error come risposta. A questo punto non rimane che da costruire un payload che consenta di estrarre dati dal DB come il seguente.

(SELECT (CASE WHEN (SELECT ascii(substring(username,{},{})) FROM cms_users where user_id=1)={} THEN pow(9999,9999) ELSE news_id END)) -- -

Con questo payload, è possibile enumerare un carattere alla volta un dato contenuto in una tabella del DB. In caso il match della seguente condizione ascii(substring(username,{},{})) FROM cms_users where user_id=1)={} sia corretto, ossia abbiamo testato il carattere giusto, allora il database andrà ad eseguire la funzione pow(9999,9999), forzando così il DBMS ad un errore di overflow numerico. In caso contrario nessun errore verrà forzato e la risposta sarà una normale 200 OK. A questo punto abbiamo un metodo per fare inferenza ed estrarre dati arbitrari dal DB. Di seguito alcune immagini che dimostrano il funzionamento di questa tecnica.

img SQLi-poc

img SQLi-poc

Potete trovare documentazione utile a questo indirzzo per quanto riguarda questo tipo di errore su MySQL, mentre a questo indirizzo informazioni utili sui tipi di dato in MySQL.

Ricerca di vecchie vulnerabilità

A questo punto uno dei nostri interessi era quello di unire questa vulnerabilità con altre vulnerabilità che dessero un impatto ancora più alto. Dopo un pò di ricerche, abbiamo scoperto che l’exploit di Remote Code Execution relativo al CVE-2018-10086 è ancora funzionante sulla versione 2.2.15 del CMS. L’unico problema da risolvere ora era trovare un modo per fare privilege escalation, in quanto per sfruttare quest’ultimo CVE-2018-10086 sono necessari privilegi ammistrativi sul CMS, mentre per la SQL-injection scoperta no.

Chaining all together

Una delle cose più semplici da immaginare nel momento in cui si scopre una SQL-injection è quella di estrarre dati sensibili dal database come ad esmpio utenti e password. Una volta ottenuti gli hash di utenze amministrative è possibile provare a craccare gli stessi per ottenere la password di accesso. Purtroppo però questo tipo di attacco è fortemente dipendente dalla robustezza della password scelta; infatti se la password risultasse molto robusta, allora questo attacco potrebbe fallire (nel senso che di solito richiederebbe un tempo troppo lungo e risorse computazionali non accessibili all’attaccante). Dopo un pò di ricerche è stato possibile osservare che la funzionalità di reset della password potrebbe essere sfruttata per eseuguire un privilege escalation. La procedura di reset infatti prevede che si conosca solo l’username dell’utenza di cui si vuole eseguire il ripristino della password e questa informazione è tranquillamente estraibile dal database mediante la SQL-injection scoperta. Dopodichè, viene inviato un token di reset per e-mail all’indirizzo email associato all’utenza. Lo stesso token viene salvato anche nel database, quindi possiamo di nuovo sfruttare la SQL-injection per estrarlo dal DB ed eseguire il reset della password. Per riassumere gli step sono:

  • Da utente non privilegiato sfruttare il CVE-2021-40961 di SQL-injection per estrarre un nome utente relativo ad utenza con alti privilegi;
  • Eseguire la richiesta di reset della password con il nome utente estratto nel passo precedente;
  • Da utente non privilegiato sfruttare nuovamente il CVE-2021-40961 per estrarre il token di reset della password;
  • Resettare la password per l’utenza amministrativa utilizzando il token ottenuto;
  • Una volta loggati come amministratore, sfruttare il CVE-2018-10086 per ottenere Remote Code Execution sul server remoto.

Per automatizzare il tutto, abbiamo scritto un python script che è in grado di automatizzare la procedura descritta fino a creare una webshell sul server remoto mediante la quale è possibile eseguire comandi di sistema. Lo script è reperibile al seguente indirizzo. Abbiamo anche creato un video che usa il python script per ottenere RCE su un nostro endpoit sul quale abbiamo installato il CMS.

alt video

Reference