How I hacked Steam. Twice

    Hi, Habr! Today, I’ll tell you what Valve paid the most bounties in the history of their reward program for vulnerabilities. Welcome under the cut!



    1. SQL Injection


    Service partner.steampowered.com is designed to obtain financial information from Steam partners. On the sales report 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 quotes!
    Let's try “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 ', then the instruction to the database 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```;

    Note that we are dealing with an array of countryFilter [] . I assumed that if in the request to duplicate the countryFilter [] parameter several times, then all the values ​​that we send will be combined in the SQL query in the following way:

    'value1', 'value2', 'value3'

    We 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 :)

    Bypassing the Web Application Firewall

    Steam servers hide 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, so there are comments in our assortment.
    We had several variants of syntax, local databases for testing payloads, comment symbols and an infinite number of quotes for all encodings, as well as self-written scripts on Python, documentation on all databases, instructions for circumventing firewalls, Wikipedia and anti-mail. Not that it was the necessary reserve for the promotion of the injection, but since it began to break the database, it is difficult to stop ...
    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.

    Getting values ​​from the database

    So, an example of getting the length of the 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:

    SELECTCASEWHEN (len(DB_NAME())= 1) THEN'UA'ELSE'qwerty'END

    Well, humanly:

    Если длина DB_NAME() равна "1", то результат  “UA”, иначе результат “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 the values ​​from 1 to infinity, we will find the right one sooner or later.

    In the same way, you can iterate through text values:

    Если первый символ  DB_NAME() равен “a”, то "UA", иначе "qwerty". 

    Usually, the “substring” function is used to get the Nth character, but WAF stubbornly blocked it. Here the combination came to the rescue:

    right(left(system_user,N),1)

    How it works? 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 received 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’ve hired that my turn to report was not yet reached. But the bug was fixed, which means it could have been reported before me.

    2. Getting all the keys from any game


    In the Steam partner interface there is the functionality of generating keys to the games.
    Download the generated set of keys by using 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 keyids , but in response, I received an error: “ Couldn`t generate CD keys: No assignment for 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.

    findings


    • 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!

    But seriously
    Make pentests, pay for vulnerabilities, think strategically.

    Also popular now: