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

в 14:42, , рубрики: Steam, Valve, взлом, информационная безопасность, Тестирование веб-сервисов

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

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

1. SQL Injection

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

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

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

Как я взломал Steam. Дважды - 3
где «UA» — это код страны.

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

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

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

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

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

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

Почему?

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

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'

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

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

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

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

Автор: moskowsky

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js