Как я взломал Steam. Дважды

    Привет, Хабр! Сегодня я расcкажу за что же Valve заплатила наибольшие баунти за историю их программы по вознаграждению за уязвимости. Добро пожаловать под кат!



    1. SQL Injection


    Сервис partner.steampowered.com предназначен для получения финансовой информации партнеров Steam. На странице отчётов о продажах рисуется график с кнопками, которые меняют период отображения статистики. Вот они в зелёненьком прямоугольнике:



    Запрос загрузки статистики выглядит вот так:


    где «UA» — это код страны.

    Ну что ж, пришло время кавычек!
    Давайте пробуем «UA'»:



    Статистика НЕ вернулась, чего и следовало ожидать.

    Теперь «UA''»:



    Статистика снова вернулась и это похоже на инъекцию!

    Почему?
    Допустим что инструкция к базе данных выглядит таким образом:

    SELECT * FROM countries WHERE country_code = `UA`;

    Если отправить UA’, то инструкция к базе данных будет:

    SELECT * FROM countries WHERE country_code = `UA``; 

    Заметили лишнюю кавычку? А это значит, что инструкция невалидна.
    Соответсвенно синтаксису SQL — запрос ниже вполне валиден (лишних кавычек нет):

    SELECT * FROM countries WHERE country_code = `UA```;

    Обратите внимание, мы имеем дело с массивом countryFilter[]. Я предположил, что если в запросе продублировать параметр countryFilter[] несколько раз, то все значения, которые мы отправим, будут объединены в SQL запросе таким образом:

    'value1', 'value2', 'value3'

    Проверяем и убеждаемся:



    Фактически, мы запросили у БД статистику трёх стран:

    `UA`, `,` ,`RU`

    Синтаксис верный — статистика вернулась :)

    Обход Web Application Firewall

    Сервера Steam прячутся за Akamai WAF. Данное безобразие вставляет палки в колёса хорошим (и не очень) хакерам. Однако, мне удалось одолеть его благодаря объединению значений массива в один запрос (то что я объяснил выше) и комментированию. Для начала убедимся в наличии последнего:

    ?countryFilter[]=UA`/*&countryFilter[]=*/,`RU

    Запрос валиден, значит в нашем ассортименте есть комментарии.
    У нас было несколько вариантов синтаксиса, локальные базы для тестирования пэйлоадов, символы комментариев и бесконечное множество кавычек всех кодировок, а также самописные скрипты на пайтоне, документация по всем базам данных, инструкции по обходу файрволов, википедия и античат. Не то чтобы это был необходимый запас для раскрутки инъекции, но раз уж начал ломать базу данных, то сложно остановиться...
    WAF блокирует запрос, когда встречает в нём функцию. Вы знали, что DB_NAME/**/() — вполне валидный вызов функции? Файрвол тоже знает и блокирует. Но, благодаря этой фиче, мы можем разделить вызов функции на два параметра!

    ?countryFilter[]=UA’,DB_NAME/*&countryFilter[]=*/(),’RU

    Мы отправили заспрос с DB_NAME/*всёчтоугодно*/() — WAF ничего не понял, а вот база данных успешно обработала такую инструкцию.

    Получение значений из базы данных

    Итак, пример получения длины значения DB_NAME():

    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),'
    

    По-SQLному:

    SELECT CASE WHEN (len(DB_NAME())= 1) THEN 'UA' ELSE 'qwerty' END

    Ну и по-человечески:

    Если длина DB_NAME() равна "1", то результат  “UA”, иначе результат “qwerty”.

    Это значит, что если сравнение истинно, то в ответ получим статистику для страны «UA». Не сложно догадаться, что перебирая значения от 1 до бесконечности, мы рано или поздно найдём верное.

    Таким же способом можно перебирать текстовые значения:

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

    Обычно для получения N-ого символа используют функцию «substring», но WAF упорно её блокировал. Тут на помощь пришла комбинация:

    right(left(system_user,N),1)

    Как это работает? Получаем N символов значения system_user из которых забираем последний.
    Представим, что system_user = “steam”. Вот так будет выглядеть получение третьего символа:

    left(system_user,3) = ste
    right(“ste”,1) = e

    С помощью простого скрипта этот процесс был автоматизирован и я получил hostname, system_user, version и названия всех БД. Этой информации более чем достаточно (последнее даже лишнее, но было интересно) для демонстрации критичности.

    Через 5 часов уязвимость была исправленна, однако статус triaged (принята) ей выставили через 8 часов и, чёрт возьми, для меня это были очень сложные 3 часа за которые мой мозг успел пережить стадии от отрицания до принятия.

    Пояснение паранойи
    Так как уязвимость не обозначили принятой, я полгал что очередь до моего репорта ещё не дошла. Но баг то исправили, а значит его могли зарепортить раньше меня.

    2. Получение всех ключей от любой игры


    В интерфейсе партнера Steam существует функциональность генерации ключей к играм.
    Скачать сгенерированный набор ключей можно с помощью запроса:

    https://partner.steamgames.com/partnercdkeys/assignkeys/
    
    &sessionid=xxxxxxxxxxxxx&keyid=123456&sourceAccount=xxxxxxxxx&appid=xxxxxx&keycount=1&generateButton=Download

    В этом запросе параметр keyid – id набора ключей, а keycount – количество ключей, которое необходимо получить из данного набора.

    Конечно же, руки мгновенно потянулись вбивать разные keyid, но в ответ меня ждала ошибка: «Couldn`t generate CD keys: No assignment for user.». Оказалось, не всё так просто, и Steam проверял принадлежит ли мне запрошенный набор ключей. Как же я обошёл данную проверку? Внимание…

    keycount=0

    Сгенерировался файл с 36,000 ключей от игры Portal 2. Вау.
    Только в одном наборе оказалось такое количество ключей. А всего наборов на данный момент более 430,000. Таким образом, перебирая значения keyid я потенциальный злоумышленник мог скачать все ключи, когда-либо сгенерированные разработчиками игр Steam.

    Выводы


    • Дорогостоящие WAF системы от топовых компаний далеко не гарантия безопасности ваших веб-приложений.
    • Если вы охотник за багами, то старайтесь проникнуть как можно глубже. Чем меньше пользователей имеют доступ к интерфейсу, тем больше вероятности найти в этом интерфейсе уязвимость.
    • Разработчики и владельцы бизнеса, абсолютно безопасных приложений нет! Но вы держитесь. Хорошего вам настроения!

    А если серьезно
    Делайте пентесты, платите за уязвимости, думайте стратегически.
    Поделиться публикацией
    Комментарии 50
      –8
      У такой крупной корпорации с мешком денег нет специалистов способных оформить работу с базой без таких дыр?
      С такой проектировкой вполне себе возможно ожидать подобные проблемы в других местах их сервиса, вопрос в том когда методом тыка ткнем куда надо. А если не методом тыка, а подумать… То сколько дыр найдем?
        +28
        Дыры есть у всех. Просто чтобы их найти все, нужен нетрадиционный подход. Кто то выплачивает вознаграждение, кто то прячет голову в песок, а кто то грозит судом.
          0
          кто то прячет голову в песок, а кто то грозит судом.
          и правда, некоторые используют
          нетрадиционный подход
            0
            Была подобная некрасивая история с каким то нашим банком. Косяк долго не закрывали и чел сказал СБ банка что расскажет обществу. Ну а те не долго думали. Деталей к сожалению память не сохранила. В поиске уже не ищется(видимо выпилили).
              0
              Очень хочется верить, что выпилили не чела.
                0
                Как ниже подсказали, это был все же сбербанк(в чем я не был до конца уверен). А чел тот писал по ситуации на форумах, по моему даже на банки.ру. Но что то я его сообщений больше не вижу.
                0
                ПриватБанк. Этого чела довольно долго чморили, вплоть до навешивания уголовки.
              +5
              Конкатенация вместо параметризованных запросов? В 2018? Это не дыры…
                –2
                Код писали еще в 2012, когда такого не было.
                Вы ни разу не сталкивались в работе с древним кодом что-ли?
                  +2
                  Это где это аж в 2012 не было prepared stmt?
                    0
                    он и сейчас не во всех либах используется. между прочим
                      0
                      А если еще с persistent connection то в малом их числе.
                        0
                        я парочку видал где жестко всегда с параметрами(ну никто конечно не мешает обойти)
                      0
                      В каком-то древнем фреймворке — вполне может быть.
                      +1
                      Код писали еще в 2012, когда такого не было.

                      Если этого не было в 2012, то как я это мог изучать в середине 00х…
                        +1
                        Сталкивался, году эдак в 99ом… А если серьезно то в любой книжке еще лет 20 назад было написано, что данные должны быть отделены от запросов.
                      +2

                      Справедливости ради, даже джуниоры знать, что делать, чтобы не допускать таких тупых инъекций. Так что да, это позор, а не то, для нахождения чего нужен «нетрадиционный подход».

                        0
                        Кто то выплачивает вознаграждение, кто то прячет голову в песок, а кто то грозит судом.

                        Был в одной знакомой фирме проект — не было никакой программы вознаграждения.
                        Написал некто, попросил денег за уязвимости. Ему вообще не ответили.
                        Затем система сломалась (наверное он сломал).
                        Просто заплатили мне (не ему), чтобы я поставил новую обновленную (без уязвимостей) версию системы.

                        Искать уязвимости и на этом зарабатывать — не так уж и романтично.
                          0
                          Недавно слышал интересную историю от исследователя с немного другим результатом: он нашел достаточно специфичный баг, написал о нем в компанию. Ему ответили нечто неопределенное, потом стали игнорировать. Через некоторое время баг закрыли заплаткой. Но оказалось, что заплатка не закрывала баг полностью, а только очень сильно усложняла нахождение бага и немного — его эксплуатацию.

                          Итоги: есть компания с серьезным багом, требующим еще более специфичных знаний для обнаружения и понимания, что он здесь вообще есть, и внешний исследователь, который о нем знает, но при этом очень неоднозначно относится к компании.
                        +2
                        У Valve очень много старого легаси кода, который написан много лет назад, когда компания ещё не была такой большой. В то время они тесты вообще не писали.
                          +1
                          а тестами такие штуки не больно таки и найдешь.
                          только ревью, пожалуй, поможет
                        +6
                        Круто, что Valve начали платить за найденные уязвимости. Всего с лет пять назад они тикеты с сообщениями об уязвимостях просто молча закрывали, даже без спасибо.
                          +1
                          Видимо дошло чем это грозит. Особенно на фоне последних крупных сливов.
                          0
                          Не знаете, а у Сбербанка есть такая программа — платить за найденные уязвимости? Я нашел в Сбербанк Онлайн потенциальную уязвимость.
                            +4
                            Нет, более того, у них очень своеобразный подход к безопасности: возможность получения доступа к информации о балансе карты и списку операций при некоторых условиях (крайне слабая авторизация) они считают нормальной.
                              +19
                              Хорошо, если не обвинят тебя во взломе и не попытаются посадить.
                                +2
                                Причем что то такое мелькало в новостях. Не помню конкретно ли со сбером или с другим российским(представительством) банком.
                                  +5
                                  Именно со Сбером и было, самый невменяемый банк.
                                    +1
                                    Ясно, спасибо за предупреждение.
                              0
                              Продайте ее в даркнете, напишите им через тор, а после напишите сюда — все будут довольны
                              +4
                              Классная статья. Короткая и захватывающая.
                              Мы так-то на работе тоже Akamai WAF используем, и до сих пор я особо не задавался вопросами «можно ли его обойти?» и «насколько он эффективен вообще?».
                              Было познавательно.
                                +3

                                Мы отправили заспрос с DB_NAME/всёчтоугодно/() — WAF ничего не понял — можно репортить еще и в WAF?

                                  –18
                                  когда-либо сгенерированные разработчиками игор Steam.
                                  Не
                                  игор
                                  , а игр
                                    +2
                                    «Игор» — это мем откуда-то. Ну и опечатки лучше в личку :)
                                      +4
                                      lurkmore.to/PS3_has_no_games — вот отсюда это мем :)
                                      • НЛО прилетело и опубликовало эту надпись здесь
                                          0
                                          Есть ещё мем «Игорь тонет» на эту же тему :)
                                          0
                                          когда-либо сгенерированные разработчиками игор Steam.

                                          когда-либо сгенерированные разработчиками Игор, Steam
                                          +1
                                          WAF блокирует запрос, когда встречает в нём функцию. Вы знали, что DB_NAME/**/() — вполне валидный вызов функции? Файрвол тоже знает и блокирует. Но, благодаря этой фиче, мы можем разделить вызов функции на два параметра!

                                          Не совсем понял. Если WAF блокирует запрос, в котором встречается DB_NAME/**/(), то почему он не заблокировал countryFilter[]=UA’,DB_NAME/*&countryFilter[]=*/(),’RU?
                                            +1
                                            потому-что этот запрос был раздлен по разным никак не связанным параметрам как мозайка. Логика её обработки лежит все-же на бэкенде.
                                              +3
                                              если бы это был один параметр countryFilter=UA’,DB_NAME/*все что угодно*/(),’RU, то он бы заблокировал. Но в данном случае он видит два отдельных параметра: countryFilter[]=UA’,DB_NAME/* И countryFilter[]=*/(),’RU. И ни в одном из параметров он не видит вызова функции
                                              –1
                                              Фокус с кавычкой так и не понял. Как количество кавычек может влиять на исход дела? MySQL вообще капризная к синтаксису штука, кавычку/скобку не там поставил и запрос тупо вылетает по синтаксической ошибке.
                                                –2
                                                try {
                                                    return $db->getResults();
                                                } catch(...) {
                                                    return [];
                                                }

                                                ?
                                                0
                                                Спасибо автору, только что сгенерировал себе 100500 тысяч ключей
                                                  +4
                                                  через закрытую уязвимость?
                                                    0

                                                    А вот интересно, гипотетически, valve заметит генерацию такого количества ключей или будет все валидно? Так что не ясно является ли эта уязвимость настолько критической (но то что она желанная — это до), если аккаунт потом всё равно забанят.

                                                      +2
                                                      Как я понял, там таки идет не генерация, а запрос уже выданных разработчику.
                                                        +2
                                                        Уязвимость была в функциональности скачивания ключей, а не генерации. Какое-то количество ключей уже продано конечным покупателям, какое-то количество продаётся прямо сейчас на легальных торговых площадках. Так что заблокировать все ключи или забанить аккаунты — это большой ущерб как для Valve, так и для партнеров.
                                                    +3
                                                    Я не совсем понимаю как такие уязвимости вообще появляются, за всё время своей коммерческой практики я не разу не писал sql запросы вручную, всегда использую драйвера к базе в которых уже есть защита от подобного, последний раз писал сам только в школе на продвинутых курсах информатики.
                                                      0
                                                      Если в шутку то это знаменитые php fullstack

                                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                    Самое читаемое