Разбор сервиса и все 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
123 lines
7.3 KiB
Markdown
123 lines
7.3 KiB
Markdown
# 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_этой_машины> # пусто
|
||
```
|