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
Выводы
- Дорогостоящие WAF системы от топовых компаний далеко не гарантия безопасности ваших веб-приложений.
- Если вы охотник за багами, то старайтесь проникнуть как можно глубже. Чем меньше пользователей имеют доступ к интерфейсу, тем больше вероятности найти в этом интерфейсе уязвимость.
- Разработчики и владельцы бизнеса, абсолютно безопасных приложений нет! Но вы держитесь. Хорошего вам настроения!
Комментарии (47)
Renaissance
01.10.2018 18:08+1Круто, что Valve начали платить за найденные уязвимости. Всего с лет пять назад они тикеты с сообщениями об уязвимостях просто молча закрывали, даже без спасибо.
Igor_Sib
01.10.2018 19:13Не знаете, а у Сбербанка есть такая программа — платить за найденные уязвимости? Я нашел в Сбербанк Онлайн потенциальную уязвимость.
pyrk2142
01.10.2018 19:57Нет, более того, у них очень своеобразный подход к безопасности: возможность получения доступа к информации о балансе карты и списку операций при некоторых условиях (крайне слабая авторизация) они считают нормальной.
Murimonai
01.10.2018 22:19+2Классная статья. Короткая и захватывающая.
Мы так-то на работе тоже Akamai WAF используем, и до сих пор я особо не задавался вопросами «можно ли его обойти?» и «насколько он эффективен вообще?».
Было познавательно.
altman
02.10.2018 07:52Мы отправили заспрос с DB_NAME/всёчтоугодно/() — WAF ничего не понял — можно репортить еще и в WAF?
nerlihmax
02.10.2018 08:42-4когда-либо сгенерированные разработчиками игор Steam.
Неигор
, а игрaleaksah
02.10.2018 16:15когда-либо сгенерированные разработчиками игор Steam.
когда-либо сгенерированные разработчиками Игор, Steam
McDermott
02.10.2018 09:22WAF блокирует запрос, когда встречает в нём функцию. Вы знали, что DB_NAME/**/() — вполне валидный вызов функции? Файрвол тоже знает и блокирует. Но, благодаря этой фиче, мы можем разделить вызов функции на два параметра!
Не совсем понял. Если WAF блокирует запрос, в котором встречается DB_NAME/**/(), то почему он не заблокировал countryFilter[]=UA’,DB_NAME/*&countryFilter[]=*/(),’RU?TimsTims
02.10.2018 10:12потому-что этот запрос был раздлен по разным никак не связанным параметрам как мозайка. Логика её обработки лежит все-же на бэкенде.
Seryojik
02.10.2018 10:32если бы это был один параметр countryFilter=UA’,DB_NAME/*все что угодно*/(),’RU, то он бы заблокировал. Но в данном случае он видит два отдельных параметра: countryFilter[]=UA’,DB_NAME/* И countryFilter[]=*/(),’RU. И ни в одном из параметров он не видит вызова функции
scruff
02.10.2018 11:26Фокус с кавычкой так и не понял. Как количество кавычек может влиять на исход дела? MySQL вообще капризная к синтаксису штука, кавычку/скобку не там поставил и запрос тупо вылетает по синтаксической ошибке.
Anshi85
02.10.2018 14:04Спасибо автору, только что сгенерировал себе 100500 тысяч ключей
ad1Dima
02.10.2018 14:24через закрытую уязвимость?
8street
02.10.2018 14:43А вот интересно, гипотетически, valve заметит генерацию такого количества ключей или будет все валидно? Так что не ясно является ли эта уязвимость настолько критической (но то что она желанная — это до), если аккаунт потом всё равно забанят.
moskowsky Автор
02.10.2018 15:03Уязвимость была в функциональности скачивания ключей, а не генерации. Какое-то количество ключей уже продано конечным покупателям, какое-то количество продаётся прямо сейчас на легальных торговых площадках. Так что заблокировать все ключи или забанить аккаунты — это большой ущерб как для Valve, так и для партнеров.
bro-dev0
02.10.2018 20:54Я не совсем понимаю как такие уязвимости вообще появляются, за всё время своей коммерческой практики я не разу не писал sql запросы вручную, всегда использую драйвера к базе в которых уже есть защита от подобного, последний раз писал сам только в школе на продвинутых курсах информатики.
slavait
У такой крупной корпорации с мешком денег нет специалистов способных оформить работу с базой без таких дыр?
С такой проектировкой вполне себе возможно ожидать подобные проблемы в других местах их сервиса, вопрос в том когда методом тыка ткнем куда надо. А если не методом тыка, а подумать… То сколько дыр найдем?
Popadanec
Дыры есть у всех. Просто чтобы их найти все, нужен нетрадиционный подход. Кто то выплачивает вознаграждение, кто то прячет голову в песок, а кто то грозит судом.
KoToSveen
Popadanec
Была подобная некрасивая история с каким то нашим банком. Косяк долго не закрывали и чел сказал СБ банка что расскажет обществу. Ну а те не долго думали. Деталей к сожалению память не сохранила. В поиске уже не ищется(видимо выпилили).
scruff
Очень хочется верить, что выпилили не чела.
Popadanec
Как ниже подсказали, это был все же сбербанк(в чем я не был до конца уверен). А чел тот писал по ситуации на форумах, по моему даже на банки.ру. Но что то я его сообщений больше не вижу.
click0
ПриватБанк. Этого чела довольно долго чморили, вплоть до навешивания уголовки.
Kwisatz
Конкатенация вместо параметризованных запросов? В 2018? Это не дыры…
aPiks
Код писали еще в 2012, когда такого не было.
Вы ни разу не сталкивались в работе с древним кодом что-ли?
Melkij
Это где это аж в 2012 не было prepared stmt?
kalininmr
он и сейчас не во всех либах используется. между прочим
Kwisatz
А если еще с persistent connection то в малом их числе.
kalininmr
я парочку видал где жестко всегда с параметрами(ну никто конечно не мешает обойти)
aPiks
В каком-то древнем фреймворке — вполне может быть.
ad1Dima
Если этого не было в 2012, то как я это мог изучать в середине 00х…
Kwisatz
Сталкивался, году эдак в 99ом… А если серьезно то в любой книжке еще лет 20 назад было написано, что данные должны быть отделены от запросов.
pronvit
Справедливости ради, даже джуниоры знать, что делать, чтобы не допускать таких тупых инъекций. Так что да, это позор, а не то, для нахождения чего нужен «нетрадиционный подход».
stul5tul
Был в одной знакомой фирме проект — не было никакой программы вознаграждения.
Написал некто, попросил денег за уязвимости. Ему вообще не ответили.
Затем система сломалась (наверное он сломал).
Просто заплатили мне (не ему), чтобы я поставил новую обновленную (без уязвимостей) версию системы.
Искать уязвимости и на этом зарабатывать — не так уж и романтично.
Suvitruf
У Valve очень много старого легаси кода, который написан много лет назад, когда компания ещё не была такой большой. В то время они тесты вообще не писали.
kalininmr
а тестами такие штуки не больно таки и найдешь.
только ревью, пожалуй, поможет