KittenGate XSS challenge solution

hacker cat from ./hidden/

Hey-hey! Time to reveal cards and explain solution of my first challenge. My point was to make a not easy challenge with tricks I haven’t seen in challenges before.

I like cats and I like XSS. So that’s why the challenge contains cats and Cross Site Scripting :) But, let’s begin to solution review, the thing which you came here for!

Code explanation (you can skip it if you have seen and understood this code before)

First thing we see in the page — access error. A bit confusing, but let’s check the source code: There’s rules&solvers <div> block, <iframe> and <script>. Let’s start from the last point — the <script>.

script on the ./index.html page

First line was added as quick-fix for cross-origin solves, but it was bypassed too, so I’ve added new rule about solution type. Next two lines of variables definition, URL params by their names. After that, the init function it’s being set as onload event for iframe (which will be triggered once the page inside iframe loaded). The next line loads the ./hidden/cat.html file in the iframe, which takes its role later in the challenge. Let’s look at the init function deeper:

  • Define sanitizer variable which will be used to clean note from malicious characters
  • Remove all malicious characters
  • Create script element with some code, which is contains inline comment with note variable
  • Append the script to iframe’s contentWindow

“Some code”. Seems like I should explain it too, it makes sense. It has comment for hackers to better understand that this webpage is secured and they had no chances to gain access :)

As I said, this script is being injected to the iframe’s contentWindow right once the page loaded (as the init is called by the iframe page loading). So the contentWindow object must contain _debug to let the page load properly instead of getting body code overwritten. At this point you may think — but I cant inject this thing there, I can clobber it only if the page loaded already, but there’s setTimeout() on the main function. You know JavaScript bad, if you’d thought like that :)

So, what will be if we magically pass this _debug check? init_kitty() from iframe contents, which is loaded from ./hidden/cat.html, is being called. It has several scripts too. (At this point I understood that there’s too many scripts). I promise, it’s last teleportation, we have come to the lowest level now.

./hidden/cat.html scripts
  • LIBS section: Some filtering stuff, here we go again. I hope I don’t have to explain what do encodeHtml function does, so lets check the filter function. It has some regex, which removes all events and other malicious stuff by adding space into them.
  • MAIN section: isValid added to check that the parent is not malformed, skipping that. Next thing is init_kitty function. I will explain it lower not to break this markup :)
  • HACKER PROTECTION section: just to harden your life — removing alert function from the window :P

init_kitty() function. Saving img param without any encoding for now, but text is being html-encoded already to prevent escaping from HTML tag in the future. Then creating scheme variable, where’s HTML code is being stored. Now using filter and then encodeURI to the img value to make sure that we are safe. The text variable is already filtered so we just include it there. Now we have scheme variable, which is contains HTML code of a new block. Then check the parent and, if it’s right, write scheme to host block’s innerHTML with 500ms delay. Delay is used to make sure there’s no clobbering issues needed. (And for me to be sure that you will not bypass this by clobbering xd)

Now, when the code is explained, let’s see how to hack this.

As you can guess, this challenge has two parts you need to pass:

  • somehow add that _debug variable to iframe’s contentWindow
  • break the filter inside the iframe

Let’s start from _debug, which seem to be impossible in those conditions. We have only one point of injection — inline comment inside the script injected from the parent frame. But.. Wait.. It’s already inside the if operator, which is false, what are you talking about? It’s JS, here’s another logic. As we can break out of the inline comment using linebreak, we can use ‘hoisting’, which evaluates variable from the unknown if’s body. All we need is to define _debug using var. Space is banned? No problem, we can use same linebreak as space. So now our payload looks like: /kittengate/?note=%0avar%0a_debug; and we can see a cat, yay! This is against logic, but as you see now, it works :) So we have passed the _debug check, whats next? init_kitty() is being called, now we have a chance to make some injection in HTML contents. (no CSP here, just need to pass filters)

(example of this behavior: https://jsfiddle.net/y8kj3rz7/)

The second part — iframe contents. As we understood from code — there’s no way to inject events to img tag aswell as urlencoded value doesn’t allow us to close the tag. So here the apostrophe trick comes to help us. This char is not being urlencoded, but “ does ( “ becomes %22, ‘ becomes ‘ ). The trick is that that guy will not stop until he finds a friend. Means you can open tag that equals to ‘ and it will ate all content afterwards until next ‘ is reached. Let’s remember then, how textarea contents is being encoded.. Only urlencode used here, wow, then we can find a friend for our ‘. So all that remains to be done — to add our payload with inline comment to cut off all dead tags:

?img=’ &note=%0avar%0a_debug; ‘onerror=alert()//

So now the scheme should look like this:

<img src=‘ alt=”No valid image given!”>
<textarea>var _debug;’
onerror=alert()//</textarea>

But.. There’s no alert appears, panic, that should have worked... Call the parent and tell him :) Seems like all we need is to call alert from parent frame, since alert=null in ./hidden/cat.html:

?img=’ &note=%0avar%0a_debug; ‘onerror=parent.alert()//

Don’t forget that slash is being eaten inside init() function from top frame, which is providing script for iframe. This filter causes script to be broken, so no init_kitty() is executed:

var _debug; ‘onerrorparentalert

this code will break the script execution. So we can add other apos to the end of the payload:

?img=’ &note=%0avar%0a_debug; ‘onerror=parent.alert()//

in injected script this line will look like this:

And that’s valid JS code, so _debug is defined, init_kitty() is called, HTML is injected.

Opening the page one more time.. That’s it! You’re awesome!

Final payload: https://rooteval.github.io/challenges/kittengate/?img=%27&note=%0avar%0a_debug;%27onerror=parent.alert()//%27

P.S. last steps correcting js code is can be done by different ways, so this is the one of them.

E.g. img=%27&note=%27onerror=parent.alert()//%0avar%0a_debug;

in this case unwanted string will stay inside comment.

Unintended solutions

There was a several unintended solutions like in most challenges about XSS. So it’s interesting tricks too, let review them:

  • First unintended solution was from @insertScript. Here it is: https://jsfiddle.net/atnq1bsz/ (now its fixed). It was before parent validation and other little code review, so he just had skipped the top frame and went to the ./hidden/cat.html part. Then bypassed filter using iframe name. Then he took the alert function from newly created iframe, which is very smart way to be honest! The second one (https://jsfiddle.net/xshqyfcr) /was like some kind of race condition, so I just said that no iframes required to make intended solution, stop that xd
  • @fransrosen’s unintended solution was interesting, lets look at it too. My first parent check was not function, it was just isValid variable. So he just created page with the second iframe named isValid. And it does work, lol.
  • The second solution was similar to @insertScript’s ones, so I added a new rule about iframes in the challenge to end this. So now I understood that X-FRAME-OPTIONS can’t be replaced to JS code, anyway, it could be bypassed somewhere.

Thanks for reading, I hope you have enjoyed the challenge. Don’t worry if you haven’t managed to solve this. Thanks for trying! It’s not an easy one :)

// A big Thanks to @insertScript for this write-up suggestions ❤

Pentester, XSS-lover.