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

7.3 KiB
Raw Blame History

Rusgram — что запатчено

Оригинал (для диффа): services_vulnerable/rusgram Пропатчено: services/rusgram

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, если пусто.
  • Внутренние узлы больше не публикуются наружу: 3800138004 привязаны к 127.0.0.1. Наружу торчит только proxy на 38000.
  • restart: unless-stopped.

⚠️ Единственное изменение с риском для SLA — порты

Привязка 3800138004 к localhost сделана отдельным шагом и легко откатывается. Предположение: чекер ходит только в proxy на 38000. Из исходников это не доказать — проверить по Pacmate/Firegex на vulnbox.

Контейнеры между собой общаются по docker-сети (database:5002 и т.д.), публикация портов на это не влияет — функциональность сохраняется полностью (проверено с внешнего IP: весь флоу чекера проходит через один 38000).

Откат, если чекер начнёт падать:

ports:
  - "38002:5002"     # вернуть 0.0.0.0 для нужного узла

Локальный доступ для отладки на самом vulnbox при этом сохраняется (curl 127.0.0.1:38002/...).


Что осознанно НЕ менялось

  • Пароли в плейнтексте и формат куки login||password (V7). Чекер почти наверняка завязан на этот формат сессии; переделка схемы хранения на хеши в середине A/D — это гарантированный SLA-фейл ради очков, которые не начисляют. Добавлены только httponly и samesite=Lax на куку — это не меняет её значение.
  • Логика галереи, шаблоны, вёрстка — не трогались.

Как проверить после деплоя

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_этой_машины>              # пусто