Not in the mood for reading? Fair enough, here’s the solution:
PoC URL: https://bugpoc.com/poc#bp-DAPAxYtZ
Password: huMANEemu69
Well would you look at that. A *wild* XSS challenge has appeared and it looks like my weekend plans has to be scrapped.
Let’s play a game… or maybe just pop an XSS?
Let’s start by checking out the challenge page:
The page appears to be a game where you have to pick three cards, one from each pile, and get a sum of 18. While the game looks fun, that’s not why we’re here is it? Let’s get to work and see if we can pop that beautiful little white box we all love!
When we click a card, a new window opens up:
And it looks like the card we picked via the new window somehow get’s transferred back to the parent window:
Hmm, is this magic? No it’s actually just a message received by listening for the message event:
Let’s check out the page that the cards are shown in! After clicking a random card we see a request is made to: https://cards.buggywebsite.com/popup.html#indexes=2|0|0&name=Player%201&autoClose=1
. Let’s navigate to that URL:
We see a card, but it’s not turned around with the cool animation we saw before… Let’s check for errors in the console:
Look’s like the page is trying to send a message to the opener window (the page that opened the window) by using postMessage
function. But that’s not possible since no page opened the window as we just manually navigated to it. But we now understand how the two windows can communicate with each other. Our trained hacker eyes notice one thing which is worth noting down: the second parameter given to the postMessage
function, the targetOrigin
, is set to ‘*’ (think wildcard). This means that the function doesn’t care who receives the message. This is (often) bad practice as it can also be read in the MDN Web Docs:
Always provide a specific
targetOrigin
, not*
, if you know where the other window's document should be located. Failing to provide a specific target discloses the data you send to any interested malicious site.
But who cares if we could potentially read the cards picked by a victim? Cards might not be that interesting, but maybe it’ll come in handy later. Let’s note it down and move on.
Checking the source code
The ‘Pop up’ page uses the script script.js
to take care of all the functionality. In the bottom of the script we cans see the following:
It looks like loadPage
is the ‘main’ function of this script, and we can see that it’ll be called every time the page loads and every time the URL fragment is changed. The loadPage
function starts by retrieving the parameters specified in the URL fragment and stores them in an object called params
The parseFragment
function looks as follows:
The approach seems to be to construct a JSON object and parse it with JSON.parse
to create a JavaScript object. The challenge creator has been kind enough to tell us where they got this function from, Stack Overflow, and provide us with a link, so let’s check that out. One of the comments to the accepted answer is a bit interesting:
Seems like we can rather easy get this approach to fail, eg. by using multiple ‘=’ chars without any ‘&’. But that’s probably also why the approach has been wrapped in a try...catch
statement. But hey, the code in the catch
statement looks vulnerable! It sets the innerHTML
property of a div to include whatever we specified in the URL fragment!
HTML injection
Let’s try to create a URL fragment which should hopefully make JSON.parse
fail so we can reach the catch
statement. Let’s also make sure it contains some HTML payload. The following fragment should do the job: ==<h1>hihi
making the URL: `https://cards.buggywebsite.com/popup.html#==<h1>hihi`
Cool! It worked! We successfully injected a HTML payload into the page. Let’s just pop an XSS now using a payload like: <img src=x onerror=alert(1)>
Ehh, no alert box? Let’s check the console:
XSS? No, CSP
Oh. CSP strikes again. Let’s use Google’s CSP evaluator to test this website’s CSP policy:
Hmm. It looks annoyingly strong. We can’t execute inline JavaScript and we can observe that script tags needs to provide a nonce to be executed.
Briefly explained, a nonce is a cryptographic unique value generated by the server to ensure that the client will only execute the intended scripts. In other words, if a script doesn’t provide this value, it won’t be executed. All this makes it practically impossible for use to just directly inject a XSS payload.
More source code
Let’s move along and proceed to check the loadPage
function.
After the params
object is created, three parameters are extracted. The indexes
parameter, which is split in to an array using ‘|’ as a separator, name
which is assigned to the playerName
variable, and autoClose
.
After this, the functiongenerateCards
is called which, generate a bunch of cards and stores them in a 3-dimensional array.
Then the function indexIntoMultidimentionalArray
is then called with the 3-dimensional array as one argument and the indexes
array as another.
The function looks very interesting. It starts by setting the variable item
to the 3-dimensional card array. It then iterates over the user defined indexes
array and sets the variable item
to elementi
of the variable item
. Let’s look at a example. If we say that that the indexes
array consists of three elements: ‘a’, ‘b’ and, ‘c’, the following would be the result:
indexes = ["a","b","c"];result = indexIntoMultidimentionalArray(array, indexes);
//result will be:
//result = array["a"]["b"]["c"];
This seems like very interesting behavior since we control the indexes
array.
Afterwards, the returned value is properly escaped by the function stringify
before being packed into an object and send with postMessage
via the custom sendMsg
function.
Time to exploit
Now that we got an understanding of what’s going on, it’s time to exploit! We already know that we should be able to receive the message sent with postMessage
on our own attacker-controlled site, since the targetOrigin
is set to ‘*’, but let’s test it in pratice:
Yup, it worked just as excepted (if you allow the pop-up). But this standard info is really boring… What would be the best info for us to get? We’ve already found a way to inject HTML and the only thing stopping us from an XSS is that annoying nonce value. But would it maybe be possible to fetch that and set it back!? That would be great! Let’s try and explore which info we can access via the indexIntoMultidimentionalArray
function. To do so we can set a breakpoint in the function and reload the page:
We know from our investigation earlier that we can access anything inside the item
variable, so let’s check what it contains by using the console:
The 3-dimensional array is made up of text nodes, each representing a different card. These nodes have a bunch of properties. One property stands out though, the ownerDocument
. This property returns the top-level document object. This means we get access to read from document node! This could be very interesting, let’s check ownerDocument
:
Well what do we have here? The scripts from the page! Let’s see which properties these contain:
Bingo! The nonce value! Just what we needed. Let’s verify we can access the nonce value via the item
array:
Yup, indeed that’s possible. Now let’s try to extract this value from the page and send it back to our attacker-controlled website! To do so, the URL fragment should be: indexes=0|0|0|ownerDocument|scripts|0|nonce&name=bla
. Let’s test:
Yes! We got the nonce value! Now we can create an XSS payload using the nonce value to avoid being blocked by CSP! “But what can we use the nonce value for when we’ve already sent our payload and the page has loaded?” you may ask. Well, remeber this:
The loadPage
function will be called when the URL fragment get’s modified. And you know what? Modifying the URL fragment won’t cause the page to reload! Let’s create an exploit which does the following:
- Send a payload to extract the nonce value
- Dynamically create an XSS payload with extracted nonce
- Send a XSS payload in an URL fragment which will cause
JSON.parse
to fail and pop the sacred alert box
Our exploit code will be:
Ehh, so that was a bit weird. Our exploit looked like it was successful, but it doesn’t look like our injected script was executed. That’s pretty sad. Oh but wait. Remeber that our payload get’s inserted into the DOM with the use of innerHTML
?
Well it turns out that HTML5 won’t execute scripts inserted into the DOM via innerHTML
:
script
elements inserted usinginnerHTML
do not execute when they are inserted.
(https://www.w3.org/TR/2008/WD-html5-20080610/dom.html#innerhtml0)
Hmm, so what do we do about that?
Iframes to the rescue
To overcome our struggle we can simply use the iframe tag and it’s beautiful attribute: srcdoc. This attribute allows us to specify HTML we want to have embedded into our very own browsing context created by the iframe tag. A payload as the following will do the job: <iframe srcdoc="<script nonce=ETRACTED_VAL>alert(origin);</script>">
Let’s try!
How cool is that!? We successfully solved the challenge! Thanks for reading along! I hope you maybe learned something new or at least just had fun reading :)
Thanks to The Xss Rat and Bug PoC for the fun challenge!