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
This commit is contained in:
@@ -24,8 +24,15 @@ def catch_all(path: str):
|
||||
headers = [("Cache-Control", "max-age=36000;"), ("Content-Type", resp.headers["Content-Type"])]
|
||||
return resp.content, resp.status_code, headers
|
||||
if path.startswith("api/"):
|
||||
if request.method.lower() not in ("get", "post"):
|
||||
return redirect("/", code=302)
|
||||
passcall = getattr(r, request.method.lower())
|
||||
resp = passcall(f"http://{logic_node}/{path[4:]}", json=request.get_json(), headers=request.headers)
|
||||
# PATCH V3: allow_redirects=False. requests по умолчанию ХОДИТ по Location,
|
||||
# который отдаёт logic, а Location раньше строился из заголовка Origin =>
|
||||
# proxy делал запрос во внутреннюю сеть за атакующего (SSRF).
|
||||
# Редирект теперь отдаём клиенту, а не отрабатываем сами.
|
||||
resp = passcall(f"http://{logic_node}/{path[4:]}", json=request.get_json(silent=True),
|
||||
headers=request.headers, allow_redirects=False)
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
return redirect("/", code=302)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user