Разбор сервиса и все 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
7.3 KiB
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, если пусто.- Внутренние узлы больше не публикуются наружу:
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).
Откат, если чекер начнёт падать:
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_этой_машины> # пусто