Ovo je treći blog post u seriji blog postova o prototype pollution-u. U ovom postu objasnićemo kako se testira prototype pollution u backend aplikacijama. I osvrnućemo se na to šta su problemi koje nosi testiranje server side prototype pollution-a.
Zašto server side prototype pollution zaslužuje poseban post? Šta je to specifično za ovu ranjivosti kada se ona nađe u okviru node.js aplikacije?
Najznačajnija od stvari jeste testiranje na samu ranjivost. Kada testiramo za odgovarajući gadget ili source u client side Prototype Pollution aplikaciji, na raspolaganju imamo:
browser konzolu, gde možemo proveriti da li smo uspešno upisali vrednost u Object.prototype
source kod od aplikacije, gde možemo da postavimo break point, da analiziramo kod
svi testovi u okviru aplikacije menjaju kontekst aplikacije samo u okviru sesije u kojoj se testira. Testovi ne utiču na druge korisnike kao ni istog korisnika koji ima pokrenut drugu konekciju u drugom browseru ili tab-u.
Sve su ovo stvari koje nam nisu dostupne kada radimo testiranje backend aplikacije na server side prototype pollution. Još jedna bitna stvar, kada testiramo server side prototype pollution ranjivost, ako uspešno upišemo atribut u Object.prototype koj se koristi na neki način za u kontekstu aplikacije, lako možemo izazvati DoS (Denial of Service) napad. U tom slučaju za ponovno korišćenje servera, potrebno je resetovani dati servis. Ovo naravno nije bas uvek moguće i nije praktičan način da se testiraju produkcioni serveri.
Prava stvar je šta možemo učiniti da testiramo da li imamo uticaj na Object.prototype a da pri tome ne proizvedemo DoS (Denial of Service) napad.
Za to postoji par načina i većina je obrađena u fantastičnoj prezentaciji Gareth Heyes-a
Za demonstraciju primera detektovanja server side prototype pollution-a predlažemo da lokalno pokrenete servis ranjiv na date ranjivosti.
Servis ranjiv na prototype pollution nalazi se na URL-u:
https://github.com/pulsarpoint-rs/server_side_pp
U okviru README.md fajla postoji uputstvo kako lokalno pokrenuti dati servis.
Sada ćemo prikazati nekoliko načina kako možemo detektovati prototype pollution. Sledeće metode će verovatno izazvati DoS (Denial Of Service) na testnoj aplikaciji. Nije preporučeno koristi date metode u slučaju testiranje ranjivosti na produkcionim node.js aplikacijama.
Primer 1 – Encoding (DoS)
prvi način je dodavanje proizvoljne vrednosti u “encoding” atribut u Object.prototype
{"__proto__": {"encoding": "x"}}
API endpoint koji možemo koristi za testiranje prototype pollution ranjivosti.
/json_reflection
Ako datu vrednost pošaljemo kao zahtev na API dobićemo response koji je prazan objekat. Ovaj zahtev je procesiran i Object.prototype na serveru je updejtovan sa vrednošću
encoding: "x"
Bilo koji sledeći zahtev na serveru usloviće exception u aplikaciji.
Zahtev
{
"test":"test"
}
Server exception
Primer 2 – Object constructor (DoS)
Drugi primer je promena vrednosti u samom Object konstruktoru. U ranijim blog postovima smo pomenuli da svaki objekat u JS–u ima konstrutor. Konstruktor je atribut koji pokazuje na funkciju koja je kreirala objekat. Funkcija koja kreira object je Object()
let obj1.= Object()
obj1
> {}
Pogledajmo i primer kako izgleda kreiranje objekta metodom object literal i na šta ukazuje object constructor
constructor je pointer na funkciju koja kreira objeckat, ta funkcija se zove Object(). Funkcija ima statičke metode koji mogu da se koriste sa datim objektom, primer takve metode je keys koji vraća listu ključeva u objektu.
Pogledajmo sada sledeći primer
{”constructor”: {”keys”: “x”}}
Server će vratiti error sa status kodom 500 “Object.keys is not a function”. Greška nastaje zato što modul content-type koristi Object.keys funkciju. Pošto nakon našeg modifikovanja Object.keys više nije statička funkcija već običan string, dogodiće se greška.
82: var params = Object.keys(parameters).sort()
ova linija koda u
node_modules/content-type/index.js
uzrokuje grešku. Ovo nije u pravom smislu te reči prototype pollution. Object.prototype nije modifikovan ali je prepisan poziv na staticku metodu Object funkcije koja se koristi u kodu. Na taj način smo proverili da imamo kontrolu nad modifikovanjem ključeva u okviru Object.constructora-a. Ovo znači da možemo i kontrolisati vrednosti u constructor.prototype i time imamo prototype pollution gadget.
Primer 3 (DoS)
Sledeći primer je takođ DoS. Zahtev je
{"__proto__":{"expect": 0 }}
nakon ovog zahteva svaki sledeći odgovor od servera biće prazan objekat i HTTP status kod 417 (expect). Deo koda zadužen za datu grešku
Nakon aktiviranja ove funkcije u našoj node.js aplikaciji vidimo trace koji ukazuje na mesto u kodu gde se proverava request.header.expect. Pošto smo mi injectovali expect vrednost u Object.prototype data provera će učitati našu vrednost i pokrenuti granu gde request.header.exepect nije undefined
Primer 4 -parameterLimit (Safe)
U ovom primeru gadget se nalazi u express biblioteci. Ako imamo mogućnost prototype pollution-a možemo promeniti vrednost “parameterLimit”.
{"__proto__":{"parameterLimit": 1}}
Ovim smo ograničili broj parametara na 1. Da bi smo testirali dati slučaj potrebno je da apikacija ima api koji procesira query parametre tako da mi možemo da vidimo koji parametri su procesirani.
Za to smo kreirali poseban API (/query_params). Prvo pošaljemo zahtev koji će modifikovati parameterLimit u Object.prototypeu:
Time smo definisali Object.prototype.parameterLimit na vrednost = 1. Parsiranje query string parametara nam služi da bi smo bili sigurni da smo uspešno upisali vrednost u Object.prototype
Samo prvi paramatar je vraćen nazad. Time smo sigurni da smo uspešno modifikovali vrednost u okviru Object.prototypa. Deo koda koji je zalužan za to, nalazi se u qs biblioteci
Sledeći primer takođe koristi qs biblioteku za testiranje. Pogledajmo kako se ponaša query_param endpoint ako mu pošaljemo sledeći zahtev
URL: http://localhost:3000/query_params??z=test
U okviru QS biblioteke postoji opcija ignoreQueryPrefix, ako je taj parametar true drugi upitnik se ignoriše ako postoji. Ovo je verovatno korisno ako nismo sigurni kako da formiramo url sa query parametrima i onda ne znamo da li će ? biti već u stringu pa dodamo ? bez obzira da li već postoji.
Pogledajmo kako QS biblioteka odgovara na parsiranje query parametara koji u sebi sadrže tačku
Query parametar je:
?z.c=test
query parametri su parsirani kako smo i očekivali, z.c predstavlja string i . nema specijalno značenje. Međutim qs biblioteka ima opciju da tačka dobije posebno značenje, ako se setuje parametar
allowDots = true
Za demonstriranje date funkcionalnosti prvo ćemo uraditi prototype pollution varijable allowDots na vrednost true.
Za to ćemo koristiti json_reflection endpoint. Nakon uspešnog modifikovanja Object.prototype i dodavanja allowDots atributa sa vrednošću true. P Poslaćemo ponovo zahtev
ovog puta z.c query parametar se parsira kao ugnežđeni objekat. Praktično z postaje naziv query parametra a c=test postaje objekat koji je sadžan u parametru z.
Primer 7 – ContentType (*safe)
U ovom primeru ćemo videti kako menjanje Content-Type vrednosti utiče na parsiranje JSON objekta.
Pogledajmo prvo ovaj primer
Na endpoint json_reflection pošaljemo zahtev koji u hederima ima setovan Content-Type: application/json; charset=utf-7. A sadržaj našeg zahteva ima karaktere prezentovan utf-8 enkoding šemom.
Na prvoj slici je prikazan sadržaj val1 ključa koji se šalje sa vrednošću koja ima karaktere: šđž. Dati karakteri mogu biti prikazani u tom formatu samo u slučaju da se dekodovani kao utf-8. Ako pošaljem zahtev sa Content-Type poljem: application/json; charset=utf-7. Ovim kažemo serveru da su karakteri encodovani kao utf-7. Server date karaktere dekoduje kao utf-7 karaktere i ponovo encoduje u utf-8 kao output. Zato karaketeri u outputu izgledaju neprepoznatljivo. Ako bi smo naše utf-8 karaktere enkodovali u utf-7 i takve poslali na server. Zatim podesili Content-Type vrednost na application/json; charse=utf–7 server bi dekodovao date karaktere kao utf–7 i encodovao ih ponovo kao utf–8 i mi bi smo videli karaktere u utf–8 formatu. Pogledajmo dati primer:
karaktere: šćž ćemo pretovoriti u utf-7 encodovani string. Za to ćemo koristi python biblioteku codecs
nakon enkodovanja probajmo isti zahtev, samo ovog puta umesto šđč unesemo string: +AWEBEQEN-
šta smo naučili iz ovoga?
Naučili smo da ako pošaljemo vrednost koja je encodovana nekim encoding mehanizmom koji može biti prepoznat na strani servera, server će datu vrednost decodovati i ponovo encodovati u utf-8 charset kada šalje output. To je način kako node.js server funkcioniše.
Kako ovo iskoristi za detektovanje prototype pollution-a?
Ako uspemo da promenimo kako node.js parsira zahteve, možemo definisati da je podrazumevani charset utf-7 a zatim poslati određene karaktere enkodovan kao utf-8 ili utf-7 i videti ponašanje servera.
Pogledajmo primer gde promenimo default “Content-Type” da bude utf–7 i pošaljemo string koji sadrži utf-8 karaktere koji su enkodovani kao utf-7.
Prvo ćemo uraditi importovanje Content-Type vrednosti u okviru Object.prototype
pogledajmo kako aplikacije reaguje ako sada pošaljemo string koji ima karaktere koji mogu prikazani samo u utf-8 enkodingu
Kao što vidimo deo string-a koji bi trebalo da bude reflectovan nazad kao utf-8 nije dobro prezentovan. Server je raspakovao utf-8 karaktere kao utf-7 i zatim dati svaki od datih karaktera pretvorio u utf-8 (ovog puta nevalidan utf-8).
Ova metoda menja na serveru encoding mehanizam i nije kompletno bezbedna. Ako servis prihvata karaktere koji nisu u ascii opsegu tj. rest endpointi prihvataju utf-8 karaktere i to je bitno za aplikaciju, onda se može dogoditi da poruke budu pogresno dekodovane i da to izazove greske u aplikaciji.
Dobra stvar kod ovog tipa testa što je moguće promeniti podrazumevani enkoding, uraditi test i odmah vratiti encoding na utf-8. Time bi se smanjila šansa da se dogodi problem sa legitimnim pozivom aplikaciji.
Sada ćemo proći kroz par testova koji su deo BurpSuite Server side prototype pollution plugin-a i koji se mogu koristi pri automatskom detektovanju ranjivosti na serveru.
Primer 8 –JSON spaces (Safe)
Standardni način formatiranja JSON output-a od strane servisa je takav da ne postoji dodatni prazan prostor.
U Raw output-u se vidi da je JSON koncizno prezentovan da zauzima što manje prostora. Express framework koristi atribut json spaces da doda space karaktere na mestima koje bi učinile json output lepšim za prikaz.
Nakon ovog zahteva pogledajmo ponovo prethodni zahtev
ovog puta output sadrži JSON sadrži space ispred val1 i val2 atributa. Ovo ne menja značenje JSON-a na način da izazove bilo kakav problem na klijentu. A nama daje mogućnost da bezbedno utvrdimo da li je došlo do prototype pollution-a
Ova je moguće samo u Express frameworku do verzije 4.17.3. Sve verzije nakon toga ignorišu dati parameter.
Exposed headers (CORS)
Sledeći trik koji je moguće koristi jeste dodavanje CORS Access-Control-Expose-Headers u okviru responsa. Ovo je moguće uraditi ako se u Object.prototype-u nalazi
exposedHeaders: ["header name"]
Ako pošaljemo sledeći zahtev na /json_reflection endpoint
svaki sledeći odgovor od strane bilo kog API poziva imaće setovan Access-Control-Exposed-Headers sa vrednošću “foo”
Status
Sledeći način ja menjanjem error coda koji se šalje od servera u slučaju da JSON input nije validan. Pogledajmo primer
Na json_reflection poslali smo request koji nije validan JSON (json sadrži dodatni , na kraju). Standardno ponašanje node.js servera je odgovor sa opisom greške i 400 HTTP status code. Ovo ponašanje može biti promenjeno na sledeći način
{"__proto__":{"status":510}}
ako smo uspešano promenili status atribut na vrednost 510 sledeći zahtev sa pogrešno formatiranim JSON-om vratiće nam
Isti error ali status kod je cifra koja je definisana u status atributu. U našem slučaju 510.
Da bi smo utvrdili zašto je ovo tako, probajmo sledeće:
Za property status u okviru Object.prototype-a i definišimo getter koji će nam vratiti console.trace poruku da je dati atribut pozvan
Prvi zahtev gde dodamo status u Object.prototype ne trigueruje dati hook koji smo napravili.
{"__proto__":{"status":510}}
Sledeći zahtev uzrokuje da se dogodi exception u node.js kodu i da se servis ugasi
Na ovaj zahtev na serveru će se dogoditi exception in nećemo dobiti odgovor. Pogledajmo kako izgleda konzola od node.js servera
error je nastao usled naše dodate defineProperty funkcije, koja je definisala status atribut u Object.prototype-u samo sa raspoloživim getterom a ne i setterom, kada ja aplikacija pokušala da modifikuje dato polje čija je definicija u tom trenutku u Object.prototype-u, node.js server je izbacio exception.
Pogledajmo u kod u kom se dogodio exception
Options
Ako pošaljemo options zahtev serveru i dobijemo odgovor gde je OPTIONS HEAD zahtev dozovoljen.
Da bi smo uspešno testirali ovaj primer cors plugin mora biti ugašen
Ako pre našeg prototype pollutiona pošaljemo OPTIONS zahtev na API Server, dobićemo odgovor
Pošto dati API prihvata samo GET zahtev. Node.js u standardnoj konfiguraciji daje opciju da pošaljemo i HEAD zahtev da dobijemo sve hedere koji su podržani na datom API-u kao i veličinu sadržaja koji bi bio pročitan sa GET zahtevom.
Kako ovo možemo iskoristiti da dobijemo informaciju o uspešnos prototype pollution napada?
Ako setujemo atribut head u Object.prototype-u na vrednost true node.js neće u responsu vraćati HEAD.
Ako probamo sada kako izgleda OPTIONS zahtev na / endpointu
ovog puta u odgovoru od servera ne postoji HEAD
JSON reflection
Pogledajmo ponašanje našeg api endpoint-a “/json_reflection”
Ako prosledimo json zahtev koji u sebi ima ključ i vrednost, response od servera je taj isti JSON. Ako prosledimo nakon toga prazan json zahtev, respons od servera je prazan json.
Pogledajmo sada kako izgleda ako je vrednost uspešno upisana u Object.prototype
u prvom zahtevu pošaljemo json koji modifikuje Object.prototype
{"__proto__":{"a":"b"}}
Output tog zahteva je prazan json. Pogledajmo kod json_reflection funkcije
app.post('/json_reflection', jsonParser, function(req, res) {
let out = {};
out = mergeObjects(out, req.body);
res.send(out);
});
Prvo je kreiran objekat out = {} koji nema nijedan key value, zatim je poslat zahtev koji je popunio Object.prototype sa vrednošću {”a” :”b”}, kada je sledećem zahtevu poslat prazan objekta, mergeObject je koristeći for in enumerisala i atribute koji su dodati u Object.prototype i ti atributi su ubačeni u novi out objekat.
for (key in req.body) {
}
Exploitovanje ranjivost
Kako exploitovati ranjivost u slučaju da je server side prototype pollution moguć?
Ovde ćemo pokazati kako iskoristiti prototype pollution za izvšavanje koda na ranjivom serveru. Node.js dolazi za modulom child_process. U okviru ovog modula postoji više funkcija koje se mogu koristi za izvršavanje eksterne aplikacije.
Sad je pitanje kako su date funkcije povezane sa prototype pollution–om i kako se prototype pollution ranjivost može koristi da se utiče na to šta date funkcije izvršavaju.
fork funkcija kao jedan od argumenta ima execArgv. Dati argument sadrži listu argumenata koji će biti prosleđeni funkciji koja se izvršava.
Zadatak fork funkcije je da kreiran novi node.js process. Ako negde u kodu postoji izvršavanje fork funkcije i pri tome data fork funkcija nema explicino podešen execArgv argument, napadač može koristeći prototype pollution ranjivost da promeni datu vrednost.
među ponuđenim opcijama je i —eval. Ovo daje mogućnost napadaču da izvrši proizvoljni Javascript kod u kontektu node.js aplikacije. Pogledajmo kako to izgleda
ako postoji fork poziv u okviru aplikacije moguće je iskoristiti prototype pollution i dodati argument execArgv
Pogledajmo execArgv primer
execArgv (fork)
U ranjivom serveru postoji api poziv /fork
poziv ovoj funkciji kreira novi node.js proces i startuje child.js fajl. Sadržaj tog fajla je funkcija koja štampa na konzoli child procesa.
Kreirajmo prvo u folderu gde se nalazi index.js, neki dummy fajl koji ćemo kasnije obrisati.
sada imamo dummy fajl u folderu gde se nalaze index.js i child.js fajl. Koristeći prototype pollution u /json_reflection dodajmo execArgv
Nakon uspešnog dodavanja execArgv vrednosti u Object.prototype koristeći api pozovimo /fork endpoint
Nakon toga proverimo da li dummy.txt fajl postoji još uvek na serveru
uverićemo se da je fajl obrisan.
Kako sprečiti prototype pollution ranjivosti
Koristi Map Set
Koristiti Map i Set umesto objekta za čuvanje podataka u formi ključ – vrednost.
Map()
Set()
Null prototype
Ako je potrebno generisati objekat i koristi ga za čuvanje parametara onda takav objekat može biti generisa korišćenjem Object funkcije, gde se specificira parent object, ako se navede da je null, taj objekat neće nasleđivati parametre iz Object.prototype