Files
ALPHA-TRAIN2/services/rusgram/PATCH.md
T
dmeetrogonandClaude Opus 5 14b5855b86 rusgram: патч RCE, захвата аккаунта, SSRF и падений в 500
Разбор сервиса и все PoC — в services/rusgram/WRITEUP.md и VULNS.md.
Каждая находка воспроизведена на живом стенде до патча и перепроверена после.

content.py — V1 (RCE, крит) и V5:
  template_folder='files' совпадает с каталогом БД, поэтому render_template(path)
  с путём из URL рендерил файл пользователя как исходник Jinja-шаблона. Имя юзера
  выбирает атакующий: регистрируем `pwn.html`, кладём payload себе в first_name,
  дёргаем /db/users/pwn.html через публичный :38000 — RCE от root в контейнере,
  где смонтирован весь website/. Теперь рендерим только 4 реальные страницы.

logic.py — V2 (захват аккаунта) и V3/V6 (SSRF/открытый редирект):
  register не проверял существование юзера, а database делал read-modify-write,
  так что повторная регистрация ПЕРЕЗАПИСЫВАЛА пароль, сохраняя msg и профиль:
  чужой аккаунт вместе с флагом и сломанный логин у чекера. Теперь 409.
  Location строился из заголовка Origin, а proxy ходил по нему серверсайд —
  чтение внутренней сети. Редирект стал фиксированным /login.html.

database.py — снижает ущерб от V4:
  валидация имени юзера как имени файла, белый список полей, пароль можно только
  создать, но не перезаписать (смены пароля в сервисе нет), 400/404 вместо 500.

proxy.py — defense in depth к V3: allow_redirects=False на ветке api/.

V8: единая безопасная проверка сессии вместо 4 копий — кривая кука, отсутствие
куки, несуществующий юзер и нечисловой img больше не роняют воркер в 500.

Формат куки login||password и плейнтекстовые пароли осознанно НЕ трогали:
чекер почти наверняка на них завязан.

Тесты: services/rusgram/test_rusgram.py — SLA 16/16, SEC 21/21 на патче;
на services_vulnerable та же секция SEC падает 12 раз. Сплойт собирает 0 флагов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NN8sHQuGbTBLGGkJyfxzXr
2026-08-26 11:31:28 +03:00

123 lines
7.3 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.
# Rusgram — что запатчено
Оригинал (для диффа): `services_vulnerable/rusgram`
Пропатчено: `services/rusgram`
```bash
diff -ru services_vulnerable/rusgram services/rusgram
```
Проверено на живом стенде: легальный флоу чекера проходит полностью,
`sploits/rusgram_sploit.py` против пропатченного сервиса собирает **0 флагов**
и не может даже перечислить пользователей.
---
## Изменения по файлам
### `content/content.py` — закрывает V1 (RCE) и V5
* **Белый список шаблонов.** `render_template(path)` теперь вызывается только для
`index.html`, `login.html`, `register.html`, `specific.html`. Всё остальное — 404.
Это корень SSTI: `template_folder='files'` совпадает с каталогом БД, поэтому
файл пользователя рендерился как исходник Jinja-шаблона.
* `get_login()` вынесен из тела вьюхи и больше не падает: нет куки, кука кривого
формата, юзера не существует, database недоступен — всё возвращает `None`.
* `specific.html`: `img` приводится к `int` и проверяется на диапазон 1–16.
Побочно чинит незакавыченную подстановку `{{img['id']}}` внутрь `<script>`.
### `logic/logic.py` — закрывает V2 (ATO), V3 (SSRF), V6
* **Регистрация проверяет, что юзер не существует** → `409 user_exists`.
Раньше повторный `register` перезаписывал пароль, сохраняя остальные поля,
то есть отдавал чужой аккаунт вместе с флагом.
* **`Origin` больше не попадает в `Location`.** Редирект на неавторизованный
запрос теперь фиксированный: `redirect("/login.html")`. Это убивает и открытый
редирект, и SSRF (proxy ходил по этому Location серверсайд).
* `valid_username()` — отклоняет только опасное: `||` (ломает формат куки),
`/`, `\`, `.`/`..`, имена на `.html`/`.htm` (defense in depth к V1).
Юникод и точки внутри имени **разрешены** — чтобы не поймать SLA-фейл.
* Единый `current_user()` вместо четырёх копий проверки; валидация тел запросов;
таймауты на все внутренние вызовы.
### `database/database.py` — снижает ущерб от V4
* `safe_username()` — имя юзера становится именем файла, режем разделители пути.
* Белый список полей профиля — нельзя насоздавать произвольных ключей.
* **Пароль можно только создать, но не перезаписать** (`409`). Функции смены
пароля в сервисе нет, легальный флоу не ломается, а прямой захват аккаунта
через `:38002` закрывается даже без фаервола.
* Валидация тел, ограничение длины значений (4096), корректные 400/404 вместо 500.
### `proxy/proxy.py` — defense in depth к V3
* `allow_redirects=False` на ветке `api/`. `requests` по умолчанию **ходит** по
`Location`; редирект теперь отдаётся клиенту, а не отрабатывается прокси.
* Ветка `.html` намеренно **оставлена как была** — там `Location` захардкожен
в content (`/login.html`), пользовательского ввода нет, и менять поведение
без нужды рискованно для SLA.
### `docker-compose.yml` — закрывает V4 и V5 по сети
* `image: git.itqdev.xyz/4x10m/rusgram-<node>:${IMAGE_TAG:-latest}` на **все пять**
сервисов. Без этого CI-шаг падает: он делает
`docker compose config --images | grep "^git.itqdev.xyz/4x10m/"` и `exit 1`, если пусто.
* **Внутренние узлы больше не публикуются наружу:** `38001``38004` привязаны к
`127.0.0.1`. Наружу торчит только proxy на `38000`.
* `restart: unless-stopped`.
---
## ⚠️ Единственное изменение с риском для SLA — порты
Привязка `38001``38004` к localhost сделана **отдельным шагом и легко
откатывается**. Предположение: чекер ходит только в proxy на `38000`.
Из исходников это не доказать — проверить по Pacmate/Firegex на vulnbox.
Контейнеры между собой общаются по docker-сети (`database:5002` и т.д.),
публикация портов на это не влияет — функциональность сохраняется полностью
(проверено с внешнего IP: весь флоу чекера проходит через один `38000`).
**Откат, если чекер начнёт падать:**
```yaml
ports:
- "38002:5002" # вернуть 0.0.0.0 для нужного узла
```
Локальный доступ для отладки на самом vulnbox при этом сохраняется
(`curl 127.0.0.1:38002/...`).
---
## Что осознанно НЕ менялось
* **Пароли в плейнтексте и формат куки `login||password` (V7).** Чекер почти
наверняка завязан на этот формат сессии; переделка схемы хранения на хеши
в середине A/D — это гарантированный SLA-фейл ради очков, которые не начисляют.
Добавлены только `httponly` и `samesite=Lax` на куку — это не меняет её значение.
* Логика галереи, шаблоны, вёрстка — не трогались.
---
## Как проверить после деплоя
```bash
cd services/rusgram
docker compose up -d --build
# 1) сервис жив и легальный флоу работает
curl -s -X POST http://127.0.0.1:38000/api/register \
-H 'Content-Type: application/json' -d '{"login":"t1","password":"p"}' # 200
# 2) ключевые дыры закрыты
curl -s -X POST http://127.0.0.1:38000/api/register \
-H 'Content-Type: application/json' -d '{"login":"t1","password":"x"}' # 409
curl -s -X POST http://127.0.0.1:38000/api/register \
-H 'Content-Type: application/json' -d '{"login":"a.html","password":"p"}' # 400
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:38000/db/users/t1 # 404
# 3) сплойт не собирает флаги
python3 ../../sploits/rusgram_sploit.py <внешний_ip_этой_машины> # пусто
```