Files
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

153 lines
8.1 KiB
Markdown
Raw Permalink 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 — врайтап по сервису
Python-сервис A/D. Инстаграм-клон: галерея из 16 картинок, комментарии, профиль
пользователя. Написан как **пять отдельных Flask-приложений**, общающихся по HTTP
внутри docker-сети.
---
## 1. Архитектура
```
┌──────────────────────────────────────────┐
игрок/чекер ──────► │ proxy :5000 (публикуется на :38000) │
└───┬──────────────┬───────────────┬───────┘
*.html │ *.css │ api/* │
▼ *.jpg ▼ ▼
┌─────────────┐ ┌────────────┐ ┌─────────────┐
│ content │ │ static │ │ logic │
│ :5004 │ │ :5001 │ │ :5003 │
│ (:38004) │ │ (:38001) │ │ (:38003) │
└──────┬──────┘ └─────┬──────┘ └──────┬──────┘
│ │ │
└───────────────┼────────────────┘
┌─────────────────┐
│ database │
│ :5002 │
│ (:38002) │
└────────┬────────┘
./website/db/ (том)
users/<username> ← JSON-файл на юзера
images/01..16 ← JSON с комментариями
```
**Ключевой факт: все пять контейнеров публикуют порты наружу** (3800038004),
хотя по замыслу «внутренними» являются четыре из них. `docker-compose.yml`
мапит `38002:5002` для database, `38003:5003` для logic и т.д.
### Роли узлов
| Узел | Порт | Что делает |
|---|---|---|
| **proxy** | 38000 | Единственная «легальная» точка входа. Роутит по расширению пути: `.html` → content, статика → static, `api/*` → logic, остальное → редирект на `/` |
| **content** | 38004 | Рендерит Jinja-шаблоны. `template_folder='files'`**тот же том, где лежит БД** |
| **static** | 38001 | Отдаёт `send_file` из `files/static/{img,css,fonts,js}` |
| **logic** | 38003 | Вся бизнес-логика: `/login`, `/register`, `/update_personal`, `/send_msg`, `/post_comment` |
| **database** | 38002 | Плоское key-value хранилище поверх файлов. **Никакой аутентификации вообще** |
---
## 2. Модель данных
Пользователь — это один JSON-файл `website/db/users/<username>` (имя файла = логин):
```json
{"password": "plaintext", "first_name": "...", "second_name": "...",
"email": "...", "msg": "..."}
```
Картинка — `website/db/images/01`..`16`:
```json
{"name": "Coffee", "description": "...", "comments": [["author", "text"], ...]}
```
Пароли лежат **в открытом виде**. Хеширования нет нигде.
---
## 3. Аутентификация
Сессия — это незашифрованная, неподписанная кука:
```
session = "<username>||<password>"
```
Проверка сессии (одинаково скопирована в `logic.py` 4 раза и в `content.py`):
```python
username, password = request.cookies["session"].split("||")
correct_password = r.get(f"http://{database_node}/users/{username}/password").json()["data"]
if correct_password != password:
return redirect(...)
```
То есть **пароль в открытом виде ездит в куке в каждом запросе**, а «сессия» —
это просто пара логин/пароль. Ни срока жизни, ни подписи, ни HttpOnly/Secure.
---
## 4. Где живут флаги
Чекер кладёт флаги в **два** места, оба через публичный API:
1. **`POST /api/send_msg`** → поле `msg` в файле юзера → рендерится на `index.html`
в блоке «личное сообщение».
2. **`POST /api/update_personal`** → поля `first_name` / `second_name` / `email`
→ рендерятся в форму профиля на `index.html`.
Плюс потенциально комментарии (`POST /api/post_comment``images/<id>.comments`),
но там автор виден всем залогиненным — это скорее не flag store.
**Вывод для атаки:** флаг = содержимое `website/db/users/<victim>`. Всё, что даёт
чтение этого файла (или выполнение кода в контейнере, где он смонтирован), даёт флаг.
**Вывод для защиты:** флаг должен читаться **только** владельцем аккаунта после
проверки пароля. Любой путь, обходящий эту проверку, — дыра.
---
## 5. Легальный флоу
```
POST /api/register {"login","password"} → 200 + Set-Cookie: session=login||password
POST /api/login {"login","password"} → 200 + Set-Cookie
POST /api/send_msg {"msg"} → пишет msg
POST /api/update_personal {...} → пишет first_name/second_name/email
GET / → index.html со своим профилем и галереей
GET /specific.html?img=N → картинка + комментарии
POST /api/post_comment {"img_id","msg"} → комментарий
```
---
## 6. Что бросается в глаза сразу (детали в `VULNS.md`)
1. **`database` не имеет аутентификации и опубликован на 38002.** Это key-value
store, где `GET /users/<кто угодно>/password` возвращает пароль, а `POST`
перезаписывает любое поле любого юзера.
2. **`content` рендерит `render_template(path)` с путём из URL**, а его
`template_folder` — это тот же каталог, где лежит БД. Значит файл юзера можно
отрендерить *как Jinja-шаблон*.
3. **`register` не проверяет, существует ли юзер.** `database.py` на POST просто
делает `touch` + перезапись поля.
4. **`proxy` ходит в бэкенды через `requests` с `allow_redirects=True`** — то есть
302 от logic заставляет proxy сходить по чужому URL серверсайд.
5. Пароли в плейнтексте, сессия без подписи, `split("||")` падает на логине с `||`.
---
## 7. Как поднять локально
```bash
cd services/rusgram
docker compose up -d --build
curl http://127.0.0.1:38000/login.html
```
Состояние сервиса — в `website/db/`. Чтобы сбросить: удалить файлы из
`website/db/users/` (кроме `.gitkeep`) и вернуть `website/db/images/*` из git.