The full writeup is a little bit long because I want to write what I have tried and how I found the solution in details. If you are not interested in reading, you can just check the tl;dr section below and the source code at the end of the article.
TL;DR
Open target in a new window
Set current identifier we found to location.hash
Compare location.hash with identifier to leak it one byte after another
Leak via img.src and received result on the server
Return to step 2 until we have full identifier
postMessage to perform XSS
There is no working POC online because it requires a server to run and I have no plan to host it. Below is the video recording, you can check it first:
If you really want to see and reproduce it, I provide the source code at the end, you can run a server and check it yourself. But the server is a little buggy, you may need to manually restart the server if it's broken.
Writeup in details
There are two pages for this XSS challenge, index and waf.html, below is the JS part of index page:
When the page received the message, it checks if there are any forbidden or dangerous words. The rules are quite strict and just leave a tiny room for XSS.
I found that we can use <img src=x onerror=xxx> to bypass the tag check to run JavaScript.
But this is just the beginning, we can't use string in JS because ', ", and ` are all blocked.
Moreover, []{}() and = are also blocked, so we can't call any function and do the assignment.
The restriction here is crazy, I don't know how to do for a couple of hours. In the end I can only come up with: <img src=x onerror=throw/0/+identifier> to throw identifier as error message, but there is no way to read this message cross origin.
The hint
At first, I tried to perform XSS directly via error query string, but after a while I started to think if it's really possible.
I googled xss without parentheses and found this: XSS Without parentheses, but it's not so useful because of the WAF restriction.
One day, I saw the hint:
"Behind a Greater Oracle there stands one great Identity" (leak it)
Oh, maybe I was in the wrong way. I knew that there are actually two possible ways to perform XSS, the one is via error directly and another one is via postMessage.
We can't embed index page in iframe because of the x-frame-options header, but we can use window.open to open it and have the access on it.
Then, we can postMessage to the window and pass anything we want with safe: true:
It's like flatten the for loop and replace the for loop with ternary.
It looks nice and we know what is the first character of identifier. But what about the second character?identifier[1] is not allow. What can I do to get the second character of identifier?
I was stuck here for a very, very long time, and I came up with an idea: what if I can make it a number?
If identifier were a number, it's much easier to leak it byte by byte:
To transform a string to number, besides function call like Number() or parseInt(), we can use +str. There are alphabets in identifier, so we can make it a hex number by prepending '0x': '0x'+identifier
But the problem is, it's not guarantee that identifier can be cast to number.
var count = 0
for(let i=0; i<100000;i++) {
var id = getIdentifier()
if (!Number.isNaN(Number('0x' + id))) {
count++
}
}
// 7, 0.007
console.log(count, (count * 100) / 100000)
It's about 0.01% chance, which we can transform the identifier from string to number.
At least I have 0.01% chance, better than 0.
After keep fighting and thinking how to get substring without [] and (), I thought that's all I can do and it's too hard for me to solve it.
But, even I can't solve this harder version, I still want to have a solution on this simpler one. So let's assume identifier can be cast to number and keep moving forward.
How to send data out?
We can't use variable, we can't call any function, even we know what is the identifier, how to pass this information to my own website?
I tried to see if there is anything writable on window.opener properties, but unfortunately none.
I can't even do the assignment, how to send data at the correct moment?
Again, thanks for the hint, at first I don't know what is it, but now I know:
Here's an extra tip: ++ is also an assignment
I have an idea about how to leak the data by leveraging lazy loading feature. We can let image lazy loaded and make it invisible first:
For example, x00 means the last bit of identifier is 0, x11 means the length-1 bit is 1. When certain condition matched, we do x00.loading++, so x00.loading become NaN. After the loading attribute became NaN, it's not lazy anymore and the browser will try to load the image.
By knowing which request has been sent to server, we know what is the identifier and we can send it via websocket or long polling from server to poc.html to perform XSS.
If identifier only contains 0-9a-f, I could solve it.
Think another way
I feel I was closer to the real answer after I solved the simpler one. Maybe just one step or two steps, but I can't find what is it even I spent hours on it.
There are one final question need to be resolved:
How to access identifier[x]?
After a while, I gave up this way. I believe that It's impossible to access identifier[x].
But I haven't gave up on this challenge yet, can't access identifier[x] doesn't mean I can't conquer this challenge.
I want to access it because I want to know what is it. What if there is another way to know without accessing it?
If there is a place for us to store the identifier we found so far, we can do something like this:
<body>
<script>
var identifier = 'z123'
var found = 'z'
</script>
<div id=a>a</div>
<div id=b>b</div>
<img src=x onerror=identifier<found+1?found+=1&&leak:identifier<found+a.innerText?found+=a.innerText&&leak:identifier<found+b.innerText?found+=b.innerText&&leak:keep_trying>
</body>
By comparing the concatenated string, we know what is the nth element in identifier. In order to know all the elements, we need to have something like for loop.
We can achieve this by setting img src again and gain:
count++&&src++ can be reaplced with count++ + src++.
So now we have two more problems:
Lazy load trick above can only send data one time, it's okay for number but not okay for string
Where to store the information we found?
For the first question, every request we want to send need to have one img element because once we do image.loading++, request will be sent and that's all, there are no second chance.
I checked the documentations about <img> to see if there is anything I can use. After trying for a while, to my surprise, src and srcset work!
When you have srcset and src both set, the browser ignore src and load srcset only. But when you do img.src++ to update the src attribute(not real "update" because change from NaN to NaN also works), the browser will load the image url in srcset again.
For the second question, I found that I can utilize location.hash! Because I can update the hash part without refreshing and it's allow to update location from opener. I can combine # and identifier then compare with location.hash + character.
There is one important thing to note, location.hash can't be #. If url is url#, hash is an empty string instead of #. To solve this problem, I start with #1 and assume the first character of identifier is 1. If it's not, I will close the window and try again.
Final exploit
Piece all the things I mentioned above, the flow will be something like this:
I have a server to decide when to response because I need to control the flow
There is a poc.html file
Open poc.html, it opens the target in a new window
Leak identifier[n] via image, run "img-based for loop"
Before another loop start, we need to update location.hash to make sure it reflects the info we just found. We can hold the response on server side until we updated location.hash.
After location.hash gets updated, let loop continue by responding the request on server. img got response and trigger onerror event again with new location.hash, leak next character.
Keep leaking until token.length > 10 (the length should be 10~14, we don't know what is the correct length so just try it)
Try to postMessag and perform XSS
It's a little bit complicated because we need to run a server to control the image based loop by holding the response and release it at the right time.
Here is the poc.html, I updated the format for better readability:
Open POC.html, reset the server and open a new window (name it as xssWindow)
Load initial images and /loop
Hold the response for /loop until we received all initial images request
Release the response for /loop on server side to trigger onerror event on client side, leak one character by loading corresponding image. Start another loop.
Server side received leaked character, send it back to POC.html via /polling. Hold the loop again and release it after 500ms
POC.html received new identifier we found, update xssWindow.location.hash
if identifer.length > 10, try to post message to xssWindow
back to step 4 until XSS success
When poc.html loaded, it loads images immediately so we need to ignore the first request and start img loop after images loaded. I can't use lazy loading here because payload will become too big and cause server side error.
The /polling part can be replaced with websocket, we I am lazy to do it so I use long polling to send data back from server to poc.html.
Both poc.html and server side code can be improved for sure, but I didn't because I am lazy and I don't have time to do it so I use workaround instead, like assuming identifier start with 1 and trying to post message after we have part of identifier when length > 10.
Footnote
This XSS challenge is quite hard but also extremely interesting! I spent almost a week to try to solve it, every day I think I am closer, and finally I made it.
Thanks @terjanq for this fun XSS challenge. I learned a lot from it.
challenge link: Intigriti's 0421 XSS challenge - by @terjanq
The full writeup is a little bit long because I want to write what I have tried and how I found the solution in details. If you are not interested in reading, you can just check the tl;dr section below and the source code at the end of the article.
TL;DR
location.hash
location.hash
with identifier to leak it one byte after anotherThere is no working POC online because it requires a server to run and I have no plan to host it. Below is the video recording, you can check it first:
If you really want to see and reproduce it, I provide the source code at the end, you can run a server and check it yourself. But the server is a little buggy, you may need to manually restart the server if it's broken.
Writeup in details
There are two pages for this XSS challenge, index and waf.html, below is the JS part of index page:
This file is simple, it sends
error
query string towaf.html
viawindow.postMessage
, and append the returned value into DOM.The key here is, we need to let
e.data.safe
be true, otherwise we can only insert pure string.Let's take a look at
waf.html
:When the page received the message, it checks if there are any forbidden or dangerous words. The rules are quite strict and just leave a tiny room for XSS.
I found that we can use
<img src=x onerror=xxx>
to bypass the tag check to run JavaScript.But this is just the beginning, we can't use string in JS because
'
,"
, and ` are all blocked.Moreover,
[]{}()
and=
are also blocked, so we can't call any function and do the assignment.The restriction here is crazy, I don't know how to do for a couple of hours. In the end I can only come up with:
<img src=x onerror=throw/0/+identifier>
to throw identifier as error message, but there is no way to read this message cross origin.The hint
At first, I tried to perform XSS directly via
error
query string, but after a while I started to think if it's really possible.I googled
xss without parentheses
and found this: XSS Without parentheses, but it's not so useful because of the WAF restriction.One day, I saw the hint:
Oh, maybe I was in the wrong way. I knew that there are actually two possible ways to perform XSS, the one is via
error
directly and another one is viapostMessage
.We can't embed index page in iframe because of the
x-frame-options
header, but we can usewindow.open
to open it and have the access on it.Then, we can postMessage to the window and pass anything we want with
safe: true
:By doing so, we can also perform XSS. I thought this way is impossible because we don't know what is the identifier.
After I saw the hint, I knew it should be possible, and it's the right way to solve this challenge.
I think the attack flow will be something like this:
But there are two problems we need to solve
How to leak a string?
I came up with something like this:
<img src=x onerror=identifier<'a'?leak_it:try_another_char>
We can't use
if else
but we can useternary
, we can't use===
to check but we can use<
instead.But how about string? We can't use string, so
'0'
is illegal. It's easy, we can leverage DOM id access and innerText or other properties:for
try_another_char
, we can use another ternary:It's like flatten the for loop and replace the for loop with ternary.
It looks nice and we know what is the first character of identifier. But what about the second character?
identifier[1]
is not allow. What can I do to get the second character of identifier?I was stuck here for a very, very long time, and I came up with an idea:
what if I can make it a number?
If identifier were a number, it's much easier to leak it byte by byte:
To transform a string to number, besides function call like
Number()
orparseInt()
, we can use+str
. There are alphabets in identifier, so we can make it a hex number by prepending '0x':'0x'+identifier
But the problem is, it's not guarantee that identifier can be cast to number.
It's about 0.01% chance, which we can transform the identifier from string to number.
At least I have 0.01% chance, better than 0.
After keep fighting and thinking how to get substring without
[]
and()
, I thought that's all I can do and it's too hard for me to solve it.But, even I can't solve this harder version, I still want to have a solution on this simpler one. So let's assume identifier can be cast to number and keep moving forward.
How to send data out?
We can't use variable, we can't call any function, even we know what is the identifier, how to pass this information to my own website?
I tried to see if there is anything writable on
window.opener
properties, but unfortunately none.I can't even do the assignment, how to send data at the correct moment?
Again, thanks for the hint, at first I don't know what is it, but now I know:
I have an idea about how to leak the data by leveraging lazy loading feature. We can let image lazy loaded and make it invisible first:
For example,
x00
means the last bit of identifier is 0,x11
means the length-1 bit is 1. When certain condition matched, we dox00.loading++
, sox00.loading
becomeNaN
. After the loading attribute becameNaN
, it's not lazy anymore and the browser will try to load the image.By knowing which request has been sent to server, we know what is the identifier and we can send it via websocket or long polling from server to poc.html to perform XSS.
If identifier only contains 0-9a-f, I could solve it.
Think another way
I feel I was closer to the real answer after I solved the simpler one. Maybe just one step or two steps, but I can't find what is it even I spent hours on it.
There are one final question need to be resolved:
After a while, I gave up this way. I believe that It's impossible to access identifier[x].
But I haven't gave up on this challenge yet, can't access identifier[x] doesn't mean I can't conquer this challenge.
I want to access it because I want to know what is it. What if there is another way to know without accessing it?
If there is a place for us to store the identifier we found so far, we can do something like this:
By comparing the concatenated string, we know what is the nth element in identifier. In order to know all the elements, we need to have something like for loop.
We can achieve this by setting img src again and gain:
count++&&src++
can be reaplced withcount++ + src++
.So now we have two more problems:
For the first question, every request we want to send need to have one img element because once we do
image.loading++
, request will be sent and that's all, there are no second chance.I checked the documentations about
<img>
to see if there is anything I can use. After trying for a while, to my surprise,src
andsrcset
work!When you have
srcset
andsrc
both set, the browser ignoresrc
and loadsrcset
only. But when you doimg.src++
to update the src attribute(not real "update" because change fromNaN
toNaN
also works), the browser will load the image url insrcset
again.This will load
x2
for 10 times.For the second question, I found that I can utilize
location.hash
! Because I can update the hash part without refreshing and it's allow to update location from opener. I can combine#
and identifier then compare withlocation.hash
+ character.There is one important thing to note,
location.hash
can't be#
. If url isurl#
, hash is an empty string instead of#
. To solve this problem, I start with#1
and assume the first character of identifier is1
. If it's not, I will close the window and try again.Final exploit
Piece all the things I mentioned above, the flow will be something like this:
location.hash
to make sure it reflects the info we just found. We can hold the response on server side until we updatedlocation.hash
.location.hash
gets updated, let loop continue by responding the request on server. img got response and trigger onerror event again with newlocation.hash
, leak next character.It's a little bit complicated because we need to run a server to control the image based loop by holding the response and release it at the right time.
Here is the poc.html, I updated the format for better readability:
And it's the code for server:
Flow in details:
xssWindow
)xssWindow.location.hash
When poc.html loaded, it loads images immediately so we need to ignore the first request and start img loop after images loaded. I can't use lazy loading here because payload will become too big and cause server side error.
The /polling part can be replaced with websocket, we I am lazy to do it so I use long polling to send data back from server to poc.html.
Both poc.html and server side code can be improved for sure, but I didn't because I am lazy and I don't have time to do it so I use workaround instead, like assuming identifier start with
1
and trying to post message after we have part of identifier when length > 10.Footnote
This XSS challenge is quite hard but also extremely interesting! I spent almost a week to try to solve it, every day I think I am closer, and finally I made it.
Thanks @terjanq for this fun XSS challenge. I learned a lot from it.