# 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/ ← JSON-файл на юзера images/01..16 ← JSON с комментариями ``` **Ключевой факт: все пять контейнеров публикуют порты наружу** (38000–38004), хотя по замыслу «внутренними» являются четыре из них. `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/` (имя файла = логин): ```json {"password": "plaintext", "first_name": "...", "second_name": "...", "email": "...", "msg": "..."} ``` Картинка — `website/db/images/01`..`16`: ```json {"name": "Coffee", "description": "...", "comments": [["author", "text"], ...]} ``` Пароли лежат **в открытом виде**. Хеширования нет нигде. --- ## 3. Аутентификация Сессия — это незашифрованная, неподписанная кука: ``` session = "||" ``` Проверка сессии (одинаково скопирована в `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/.comments`), но там автор виден всем залогиненным — это скорее не flag store. **Вывод для атаки:** флаг = содержимое `website/db/users/`. Всё, что даёт чтение этого файла (или выполнение кода в контейнере, где он смонтирован), даёт флаг. **Вывод для защиты:** флаг должен читаться **только** владельцем аккаунта после проверки пароля. Любой путь, обходящий эту проверку, — дыра. --- ## 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.