Files
Serdtse-Sysoly/finals/caplagcombat-web-hard/WRITEUP.md
T
2026-09-17 00:50:07 +03:00

100 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<h1 align="center">CaplagCombat</h1>
<p align="center">
<img src="https://img.shields.io/badge/category-Web-blueviolet" alt="Web"/>
<img src="https://img.shields.io/badge/difficulty-hard-critical" alt="hard"/>
</p>
Caplag Combat — Telegram Mini App с тапами, карточками и рейтингом. В игровом клиенте и API находятся пять флагов. Получаем сессию через обычный вход Mini App и начинаем смотреть запросы в DevTools.
При запуске frontend сам отправляет `Telegram.WebApp.initData` в `/api/auth/telegram`, а сервер проверяет подпись и устанавливает HttpOnly-cookie сессии. Интерфейс запускается только на мобильных клиентах (`android`, `android_x`, `ios`), поэтому в обычном браузере и Telegram Desktop показывается экран неподдерживаемого устройства. Для перехвата запросов используем remote debugging мобильного WebView или прокси. HttpOnly-cookie из JavaScript не читается, но same-origin `fetch` в консоли WebView отправляет её автоматически.
## Решение
Нужные операции находятся в `/api/ctf/*`. Без сессии они отвечают `401`, а с сессией неизвестный путь даёт `404`, существующая POST-ручка при GET возвращает `405`, GET-ручки отвечают `200`. `OPTIONS` для разведки бесполезен, поскольку общий CORS-обработчик отвечает `204` до маршрутизации. Так находятся пять независимых задач, это `tap/claim`, `cards/upgrade`, `contracts/search`, `telegram-proof/template` и `telegram-proof`.
Для тапов и гонки у них отдельное состояние, поэтому ориентируемся на ответы этих эндпоинтов. Для запросов нужны cookie авторизованной сессии. В гонке используем отдельных новых игроков, а в остальных ветках сохраняем первоначальную сессию.
> Солвер создаёт игроков через `POST /api/auth/telegram` с `devName`, поэтому ему нужен стенд с `ALLOW_DEV_AUTH=1`. При обычном Telegram-входе запросы выполняем из авторизованной сессии мобильного WebView.
### Отладочный пакет
Клиент загружает `/api/ctf/debug-bundle.js`. Запрашиваем его напрямую из авторизованной сессии:
```http
GET /api/ctf/debug-bundle.js
```
Скрипт создаёт `window.__CAPLAG_DEBUG__`, внутри которого лежит первый флаг. Он читается прямо из тела ответа.
### Проверка параметров тапа
`POST /api/ctf/tap/claim` принимает `taps`, `tapPower` и `comboMultiplier`. Начисление считается как произведение этих трёх чисел, а силу тапа сервер берёт у клиента без сверки с прогрессом, ограничивая лишь диапазоны, включая `tapPower` до `10000000` и `comboMultiplier` до `100`.
Отправляем JSON:
```json
{"taps":10,"tapPower":100000,"comboMultiplier":1}
```
Получается `10 × 100000 × 1 = 1000000`. Это порог выдачи второго флага, который приходит в поле `flag`. У обычного игрока такого tap power нет, но запрос проходит все проверки диапазонов.
### Гонка при улучшении карточки
Карточка `Parallel Miner` улучшается через `POST /api/ctf/cards/upgrade` со стартовым балансом `1000`, ценой апгрейда `700` и целевым уровнем `5`. При последовательных покупках баланса хватает лишь на один уровень. Но между проверкой денег и обновлением состояния есть задержка около 125 мс, а повторной проверки баланса в `UPDATE` нет.
Создаём нового игрока и отправляем восемь запросов одновременно, с одной cookie и JSON-телом `{}`. Несколько запросов проходят проверку баланса до первого списания, поэтому каждый обработчик увеличивает `ctf_card_level`. Когда уровень достигает `5`, в ответе появляется флаг.
Если запросы не успели пройти проверку одновременно, повторяем попытку с новым игроком, поскольку у предыдущего баланс уже мог измениться. Солвер делает до четырёх таких попыток, создавая отдельного игрока для каждой.
### SQL-инъекция в поиске контрактов
Поиск `GET /api/ctf/contracts/search?q=...` вставляет `q` в SQL-условие `ILIKE`. Пробуем boolean-инъекцию, открывающую скрытые строки:
```text
' OR is_hidden = true --
```
Значение передаём в `q` с URL-кодированием. Условие позволяет вернуть контракт с `is_hidden = true` и четвёртым ответом в поле `flag`. Фильтр вырезает слова вроде `union` и `select` и блокирует составные запросы, но пропускает одинарную кавычку и `OR`, поэтому для этой ветки достаточно изменить логическое условие.
Вместо комментария можно закрыть строку поиска и открыть её снова, чтобы сохранить хвост исходного запроса:
```text
' OR is_hidden = true OR title ILIKE '
```
В обоих вариантах в ответе появляется скрытый контракт `ghost-contract-42`, чьё поле `flag` не очищается.
### Дублирование ключей
Берём подписанный шаблон из `GET /api/ctf/telegram-proof/template`, поле `initData`. Это отдельная CTF-строка, свежеподписанная сервером и содержащая `ctf_role=observer` и `ctf_claim=viewer`. Шаблон действителен в течение десяти минут после получения. При проверке подписи обработчик использует **первое** значение каждого ключа, а для авторизации — **последнее**.
Дописываем в конец шаблона дубли:
```text
&ctf_role=auditor&ctf_claim=claim_hard_flag
```
Затем отправляем изменённую строку в `POST /api/ctf/telegram-proof`:
```python
forged = template + "&ctf_role=auditor&ctf_claim=claim_hard_flag"
payload = {"initData": forged}
```
Подписанные первые значения остались прежними, поэтому подпись сходится. Проверка прав видит последние `auditor` и `claim_hard_flag` и возвращает пятый флаг.
[Солвер](solve/solve.py).
## Все этапы
| Этап | Флаг |
|---|---|
| 1 | `caplag{cc_easy_1_05873347cbcbfa04}` |
| 2 | `caplag{cc_easy_2_a9570608a3553c4a}` |
| 3 | `caplag{cc_medium_1_00d451298c753b3d}` |
| 4 | `caplag{cc_medium_2_bdaef85af11b3c2b}` |
| 5 | `caplag{cc_hard_1_ffaa2e690177555d}` |
Значения флагов генерируются при развёртывании стенда, поэтому на другом инстансе они будут другими (на проде, например, с префиксом `prod_` вместо `cc_`).