Init. import
This commit is contained in:
91
GAME/narod/writeup.md
Normal file
91
GAME/narod/writeup.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# NarodUslugi - полный разбор (пошагово)
|
||||
|
||||
Этот разбор объясняет, как устроен сервис и как решатель получает флаг.
|
||||
Ссылки идут на локальные файлы задачи в этой директории.
|
||||
|
||||
## 1) Входные данные и их источники
|
||||
|
||||
- URL цели, partA и user можно читать из `target.txt` или передать через CLI.
|
||||
- Поведение сервера описано в `files/server.js`.
|
||||
- Логика решателя находится в `solver.py`.
|
||||
- Значения окружения указаны в `web_naroduslugi_6.yml`.
|
||||
|
||||
## 2) Формула пароля (на сервере)
|
||||
|
||||
В `files/server.js` сервер считает пароль так:
|
||||
|
||||
- `PASS = sha1(f"{PWD_PART_A}:{PWD_PART_B}").hexdigest()[:12]`
|
||||
|
||||
`partA` у нас есть, но `partB` скрыт.
|
||||
|
||||
## 3) Где спрятан partB
|
||||
|
||||
Эндпоинт `/assets/app.js.map` отдает sourcemap со строкой:
|
||||
|
||||
- `pepper_xor_hex` (hex-строка)
|
||||
|
||||
Она вычисляется так:
|
||||
|
||||
- `pepper_xor_hex = xorHex(PWD_PART_B, sha1(user)[:6])`
|
||||
|
||||
Значит `partB` можно восстановить, если известен `user`.
|
||||
|
||||
## 4) Как восстановить partB
|
||||
|
||||
Шаги, которые делает `solver.py`:
|
||||
|
||||
1. `mask = sha1(user).digest()[:6]`
|
||||
2. Преобразовать `pepper_xor_hex` в байты.
|
||||
3. XOR-нуть каждый байт с `mask` (по кругу).
|
||||
4. Декодировать результат как UTF-8 и получить `partB`.
|
||||
|
||||
После этого вычисляется пароль:
|
||||
|
||||
- `password = sha1(f"{partA}:{partB}").hexdigest()[:12]`
|
||||
|
||||
## 5) Trusted-device логин для доступа к экспорту профиля
|
||||
|
||||
Есть альтернативная авторизация, описанная в `/.well-known/`:
|
||||
|
||||
- Заголовки: `X-TS` и `X-Trusted-Device`
|
||||
- `X-Trusted-Device = hex(hmac_sha1(pass, ts))`
|
||||
- `ts` должен быть в окне 120 секунд
|
||||
|
||||
Если заголовки валидны, сессия помечается как `mfa=true`, но
|
||||
`mfaOtp=false`. Это дает доступ к `/profile` и `/profile/export`.
|
||||
|
||||
## 6) Экспорт OTP-секрета отдается с маской
|
||||
|
||||
`/profile/export?fmt=ini` возвращает:
|
||||
|
||||
- `otp_secret` с заменой последних 2 base32-символов на `??`
|
||||
|
||||
Есть CSRF-проверка: нужен `Referer: /profile`.
|
||||
Решатель выставляет этот заголовок.
|
||||
|
||||
## 7) Брут последних 2 base32-символов
|
||||
|
||||
Алфавит base32 длиной 32, значит всего 32 * 32 = 1024 вариантов.
|
||||
Решатель перебирает все кандидаты:
|
||||
|
||||
1. Логинится обычным способом (без trusted заголовков).
|
||||
2. Считает TOTP для кандидата.
|
||||
3. POST на `/mfa` с `otp`.
|
||||
4. При успехе открывает `/wallet` и `/wallet/drain`.
|
||||
|
||||
## 8) Синхронизация времени
|
||||
|
||||
Решатель читает `/__status__`, чтобы получить смещение времени сервера
|
||||
и учитывать его при генерации TOTP (защита от дрейфа часов).
|
||||
|
||||
## 9) Получение флага
|
||||
|
||||
Если OTP верный, сессия получает `mfaOtp=true`, и `/wallet/drain`
|
||||
возвращает флаг.
|
||||
|
||||
## 10) Важные ловушки
|
||||
|
||||
- Повторный trusted-логин во время активного окна доверия вызывает бан.
|
||||
- Если trusted-сессия протухла без OTP, при следующем запросе будет бан.
|
||||
- Решатель использует отдельные cookie-jar и лимит попыток на сессию.
|
||||
|
||||
Reference in New Issue
Block a user