感想
昨年末にASIS CTF Finals 2023へチーム std::weak_ptr<moon>
で出ていた。Webのgimme cspとPuppetearを解いたけれども、それぞれmeta refresh(SECCON CTF 2023 Finals - cgi-2023で考えてダメだったやつ)と Error.prepareStackTrace
からの Error.prototype.stack
の参照という解法だった。特筆すべきこともなく、writeupを書ける問題を解いていないなと思ったので書いていない。Arkさんの別解祭りは素晴らしかった。
PuppetearはXSS botのパスにフラグが含まれているので、それをWebページ側からリークさせよという変わった問題でよかった。Object.keys(globalThis)
をやってみると __ariaQuerySelector
, __ariaQuerySelectorAll
というPuppeteerが生やした謎の関数があり、これが気になった。結局のところ、さっき書いたように Error
のスタックトレースから漏れていたのだけれども。
この謎の関数でPrototype Pollutionなりなんなり(実際 JSON.stringify
の置き換え等はできたが、特に有用な攻撃へつなげることはできなかった)で偽のCDPの命令を送って、変なところにJSコードをダウンロードさせて…みたいなことを考えた。けれども、そういうバイパスは少なくとも自分にはできなかったし、それが想定されているなら /getflag
という実行ファイルを実行せよという形式になっているだろうと真面目には探していなかった。要は作問者のparrot409さんを信頼していた。
DiceCTF 2022 - shadowを改変したSayeha, Sayeha Revengeはまた面白かった。解けなかったんだけど。catfinderも、こういうSQLi問は久しぶりに見た気がする。解けなかったんだけど。ほかの人の解法を見てこのブログで反省する。
反省
Sayeha, Sayeha Revenge
こういう問題。attachShadow({mode: 'closed'})
ですけど、どうやって外からアクセスしますか? というやつ。DiceCTF 2022 - shadowも似たようなシチュエーションで、contenteditable
相当の -webkit-user-modify: read-write
→ window.find
→ document.execCommand('insertHTML', …)
というコンボで解けたらしい。今回は default-src 'none'; script-src 'unsafe-eval'
と余計なCSPがくっついてきていたり、ほか色々変わっておりつらい。
<html> <head> <title>Sayeha</title> </head> <body> <div id="ctx"></div> <script> function containsText(){ for(let i=0;i<0x10000;i++){ if(window.find(String.fromCharCode(i))){ return true } } return false } let params = new URLSearchParams(document.location.search) let html = params.get('html') ?? '<!-- hi -->' let p = params.get('p') ?? 'console.log(1337)' let shadow = ctx.attachShadow({mode: 'closed'}); let mtag = document.createElement('meta') mtag.httpEquiv = 'Content-Security-Policy' mtag.content = `default-src 'none'; script-src 'unsafe-eval';` document.head.appendChild(mtag) shadow.appendChild(document.createElement('div')) shadow.children[0].innerHTML = `<!-- ${localStorage.getItem('secret') ?? 'ASIS{test-flag}'} -->` shadow.children[0].innerHTML += html.slice(0,0x2000) localStorage.removeItem('secret') if( shadow.children.length != 1 || shadow.children[0].innerText != '' || containsText() ){ throw 'no' } shadow = null mtag = null setTimeout(p,500) </script> </body> </html>
containsText
が絵文字等は(範囲外なので)見つけられないことには気づいていた。また、たとえば <object><h1 contenteditable>abc</h1></object>
のように object
要素を使うとなぜか shadow.children[0].innerText
が空文字列になってチェックをバイパスできることにも気づいていた。それで終わり。object
以外にも、dialog
みたいな表示を切り替えられそうなやつならば同じく innerText
のチェックをバイパスできるのではと思ったけど、色々試してダメだった。正解は
- 絵文字で
containsText
をバイパス details
とsummary
の組み合わせでinnerText
のチェックをバイパスdocument.execCommand('insertHTML', …)
でcustom elementsを仕込む。connectedCallback
でShadow DOMに潜入
という流れらしかった。
catfinder
ペイロードが /^[a-z()`']+$/
にマッチしないとダメ、flag
や sleep
といった文字列は含ませてはいけないし、かつ (
は2回以上出現してはいけないという制約のもとで、MySQLでSQLiをせよという問題。
抜き出したい情報のある secrets
テーブルはカラムがひとつ、レコードもひとつしかないので SELECT 'hoge' > (SELECT * FROM secrets)
がいけるじゃんと思ったけど、SELECT *
相当のことをどうやるかという問題がある。order by
は
Columns selected for output can be referred to in ORDER BY and GROUP BY clauses using column names, column aliases, or column positions. Column positions are integers and begin with 1:
とそのカラムのポジションでも指定できるので、これで 1
を指定すると flag
が指定できるじゃん、あとは SELECT 'a' UNION SELECT …
みたいに適当な文字列とUNIONしてソートさせて、どちらが先に来たかというのをオラクルにできそうと思った。けれども、やっぱり SELECT *
相当のことをどうやんねん、あとそのオラクルとやらは、レコードが1行以上あるかどうかしかわからない状態で、どうやって外から観測すんねんという問題があった。
答えは table
らしかった。table
ステートメントで、"returns rows and columns of the named table" とのこと。SELECT * FROM secrets
相当だ。
mysql> select (table `secrets`); +-------------------+ | (table `secrets`) | +-------------------+ | ASIS{testflag} | +-------------------+ 1 row in set (0.00 sec)