writeup以外

ASIS CTF Finals 2023の感想と反省、Shadow DOMと久々のSQLiについて

感想

昨年末に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-writewindow.finddocument.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 をバイパス
  • detailssummary の組み合わせで innerText のチェックをバイパス
  • document.execCommand('insertHTML', …) でcustom elementsを仕込む。connectedCallback でShadow DOMに潜入

という流れらしかった。

catfinder

ペイロード/^[a-z()`']+$/ にマッチしないとダメ、flagsleep といった文字列は含ませてはいけないし、かつ ( は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:

(https://dev.mysql.com/doc/refman/8.0/en/select.html)

とそのカラムのポジションでも指定できるので、これで 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)