Files
ALPHA-TRAIN2/services/rusgram/proxy/proxy.py
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

42 lines
2.0 KiB
Python
Executable File

#!/usr/bin/python3
from flask import Flask, request, redirect
import requests as r
app = Flask(__name__)
proxy_node_port = 5000
static_node_port = 5001
logic_node_port = 5003
content_node_port = 5004
static_node = "static:" + str(static_node_port)
logic_node = "logic:" + str(logic_node_port)
content_node = "content:" + str(content_node_port)
@app.route('/', defaults={'path': 'index.html'})
@app.route('/<path:path>', methods=["GET", "POST"])
def catch_all(path: str):
print(path)
ext = path.split('.')[-1]
if ext == "html":
resp = r.get(f"http://{content_node}/{path}?{request.full_path.split('?')[1]}", headers=request.headers)
return resp.content, resp.status_code, resp.headers.items()
if ext in ["css", "png", "jpg", "ico", "js", "otf", "eot", "svg", "ttf", "woff", "woff2"]:
resp = r.get(f"http://{static_node}/{path}", stream=True)
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())
# 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)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=proxy_node_port)