ModSecurity Bypass e Denial of Service CVE-2019-19886

ModSecurity Bypass e Denial of Service CVE-2019-19886

A fine ottobre del 2019 ho scoperto una vulnerabilità critica sul Web Application Firewall open source libModSecurity (CVE-2019-19886) dalla versione 3.0.0-3.0.3 che permette di generare un DoS sul webserver in cui è configurato. Questa vulnerabilità impatta principalmente i webserver compatibili con il nuovo branch (v3) di ModSecurity e quindi, di fatto, Nginx e OpenLiteSpeed.

Grazie all'impagabile lavoro di Ervin Hegedus che, come me, è parte del team di sviluppo del progetto OWASP Core Rule Set, abbiamo identificato alcune problematiche sul parser della cookie string della nuova versione di ModSecurity. Per agevolare i test, Ervin ha creato un piccolo programma in C++ che riproduce esattamente il cookie parser originale di ModSecurity e la versione fixata di Ervin.

Ma facciamo un passo indietro: Per il protocollo HTTP la cookie string di una request è composta da coppie di chiave=valore divise dal carattere ;. Per esempio:

Anche se può sembrare banale, esistono molti casi in cui il parser può essere indotto a non rispettare ciò che è stato previsto nello standard del protocollo. In particolare:

La prima cosa che ha attirato la mia attenzione è che all'interno del RFC 6265 si prenda in considerazione il fatto che la cookie string può essere composta da un valore che non contiene alcun carattere =. In questo caso il parser dovrebbe ignorare completamente il cookie, nonostante sia compito del WAF entrare nel merito del valore dell'header Cookie a prescindere dalla sua corretta formattazione.

Un altro aspetto interessante è questo:

è possibile che il nome o il valore di un cookie sia vuoto. Ammetto che avevo dato per scontato che il valore di un cookie potesse essere vuoto, ma non avevo mai riflettuto sulla possibiltà che anche il nome del cookie potesse non essere valorizzato. Quindi qualcosa tipo:

Cookie: =bar; foo=;

Il che ci porta alla scoperta della vulnerabilità sul vecchio cookie parser di ModSecurity. L'annuncio fatto da Trustwave parla di DoS (e ci arriveremo) ma voglio partire con un Bypass che il vecchio parser permetteva e che non è stato riportato nell'annuncio originale (e credo neanche nella descrizione della CVE). Nello screenshot qui sotto vedete l'interpretazione della cookie string fatta dal vecchio parser e dal nuovo, notate nulla di strano?

il vecchio parser faceva uno split della coppia nome/valore usando il carattere = come separatore. Il risultato è che il valore del cookie "foo" diventa "bar" e non "bar=ciao" come dovrebbe essere e come è attualmente. Questo può rappresentare un possibile bypass di una regola del WAF, immaginatevi uno scenario in cui il cookie "foo" è vulnerabile a stored XSS. Se avessi inviato una cookie string come la seguente, il payload XSS sarebbe stato completamente ignorato:

Come vedete il vecchio parser avrebbe ignorato <script>alert(1)</script> e la mia request HTTP non sarebbe stata intercettata.

DoS Vulnerability

La vulnerabilità più critica che ho trovato subito dopo è sicuramente quella più interessante e ciò che ha convinto Trustwave ha rilasciare una security release (non senza poche pressioni da parte nostra). Si tratta di ciò che abbiamo detto prima: stando a ciò che viene riportato sulla RFC 6265, il nome di un cookie può essere vuoto se il suo valore non lo è. Usando il programmino di Ervin mi sono accorto che inviando il cookie =bar causava un out_of_range exception e crashava:

confrontandomi con Ervin su quanto scoperto ci siamo chiesti che impatto avrebbe avuto se avessimo inviato lo stesso cookie su di un webserver reale. Quindi ho avviato un container docker che eseguiva Nginx + libModSecurity versione 3.0.3 e il risultato è stato drammatico:

Come si può vedere dallo screenshot, il worker di Nginx muore scrivendo sui log lo stesso errore e il master ne avvia subito uno nuovo. La mia request inviata con curl non ha ricevuto nessuna risposta. Da qui è stato semplice causare un DoS, semplicemente inviando in sequenza la stessa request e facendo morire i worker di nginx in continuazione.

curl -s -H 'Cookie: =foo' 'http://localhost/?a=[1-10000]'

References

Video