feature image

2021年4月27日 | ブログ記事

CPCTF2021 作問者writeup by sappi_red

はじめに

こんにちは、19の翠(sappi_red)です。
いつもはSysAd班で部内サービスの開発・保守をしています。

CPCTFへのご参加ありがとうございました。
ぼくはWeb問の4つ(最大獲得可能点数合計1200点分)の作問と、スコアサーバーのフロントエンドの開発をしていました。
この記事では、CPCTF2021の作問者writeupを書いていきます。スコアサーバーの記事は後日出ます!

作問者writeup

Web/Are you still using IE 8? (200点, 57AC)

are_you_still_using_ie8
タイトルから何となくわかるようにInternet Explorer 8が鍵になる問題です。
古い感じを出すために、XHTMLでかかれていたり<frame>タグが使われていたり、文字コードがShiftJISになってたりします。

User-AgentでInternet Explorer 8を詐称すると、元の状態だと見れないページが現れてそこにフラグが載っています。(Google ChromeでUserAgentを変更する - Qiita)
User-Agentで振り分けられていることはエスパーっぽいですが、レスポンスのヘッダーにVary: User-Agentがついていることで一応気づくことができます。

ところでInternet Explorer 8で問題のサイトを見るには設定からTLS 1.2を有効にする必要があったりします[1]

ちなみに問題の発想元はICTSCに出てたtraPのチームの「Windows XP のサポートは終了しました」のチーム名からです。

Web/Line to line (200点, 25AC)

line2line
サイトにアクセスすると、「パスワードはクライアントのソースコードのコメントを参照すること。」と書いてあります。

ソースコードは問題で配れているわけではないので何らかの方法でソースコードを入手する必要があります。

下の「Bundled with Webpack.」がほんの少しのヒントになっていたりします。
Webpackの仕組みや行っていることを調べていくと、JavaScriptのファイルをまとめてminifyしたりしていることや、minifyされた状態でもデバッグができるようにソースマップというものがあるということが書いてあります。

なので、ソースマップというものを見つければよいのですが、どこにあるんでしょうか。
そこでWebpackの設定のドキュメント(Devtool | Webpack, Output | webpack)を見ると、デフォルトだと元のファイル名に.mapを付与したところにソースマップがあることがわかります。

main.js.mapにアクセスすると、元のソースコードが含まれたソースマップが存在していて、そこにフラグがコメントで書いてあります。

ってことで、ソースコードにはコメントであってもパスワードとかは書くべきではないということと、場合によってはソースマップをは公開しないほうがよいかもしれないという問題でした。
問題名はソースマップが行の対応関係を保持していることからつけた名前なので、もしかしたらそれで分かった人もいたかもしれません。
ACあまり多くなかったので300点問題にしたほうがよかったかもですね…。

Web/Offline compatible (300点, 20AC)

offline_compatible
一番最初は、登録したりログインしたりしても同じ画面が表示されてバグってる?ってなる問題です。

F12などでDevToolsを開いてみてみると、レスポンスがServiceWorkerから返ってきていることがわかります。
最初のアクセスのときにログインしていない状態でServiceWorkerがレスポンスのキャッシュを保持して、ログインしていてもしていない状態でも常にログインしていない状態のレスポンスを返していたというのが原因です。
ServiceWorkerが怪しいということでそのソースであるsw.jsを見ると条件分岐なしでレスポンスを返しているコードが見つかります。

なので、ServiceWorkerを無効にした状態(例: ハードリロード(Ctrl + Shift + R)やServiceWorkerの削除)でアクセスをしてみると、フラグが表示されます。

問題名がオフライン対応なんですが、完全に嘘って感じですね。対応したつもりになっているだけのサイトでした。

知識がない状態でServiceWorkerに辿り着くには「オフライン サイト 対応」→「PWA オフライン」の順でググるとよかったりします。

Web/Pollen common ancestor (500点, 6AC)

pollen-collection
CPCTFで3問しかない500点問題の一つです。

サイトを適当にさわる[2]と、登録とログインと自分のプロフィールの変更のページだけがあることがわかります。
珍しくソースコードが配布されているのでそれを読みます。

コードを読むと/api/adminでフラグを返していそうなことがわかります。アドミンになって/adminにアクセスするとこのエンドポイントが叩かれてフラグが表示されます。

router.ts(118行目~134行目)router.get('/api/admin', async context => {
  const username = await context.state.session.get('username')
  const user = users.get(username)
  if (!user) {
    setUnauthorized(context)
    return
  }

  if (!config.adminsSet[username]?.includes('active')) {
    context.response.status = Status.Forbidden
    return
  }

  setJsonResponse(context, {
    flag: Deno.env.get('FLAG_POLLEN')
  })
})

config.adminsSet[username]?.includes('active')trueにすれば表示できそうです。
ここでconfig.adminSetの定義を見てみると、下のようにオブジェクトで定義されています。

config.tsexport const config = {
  adminsSet: {
    'admin': ['active']
  } as Record<string, Array<'active'> | undefined>
}

ここでは、config.adminsSet.自分のユーザー名undefinedになることでfalseになってForbiddenが返っています。

まずは、このundefinedになっている部分がundefinedにならないようにします。
プロトタイプ汚染攻撃をすることで、存在しないプロパティが存在するようにできそうです。

utils.tsconst mergeObject = (obj1: any, obj2: any, depth = 0) => {
  if (depth > 50) return

  for (const key in obj2) {
    const val = obj2[key]
    const valType = typeof val
    if (valType === 'function') {
      throw new Error('cannot merge function')
    }
    if (valType !== 'object') {
      obj1[key] = val
      continue
    }
    if (val === null) {
      obj1[key] = null
      continue
    }

    if (
      !(key in obj1) ||
      (typeof obj1[key] !== 'object' && typeof obj1[key] !== 'function')
    ) {
      obj1[key] = Object.create(null)
    }
    mergeObject(obj1[key], val, depth + 1)
  }
}

utils.tsを見るとmergeをしている怪しいコードが見つかります。
このコードを見ると__proto__プロパティが除外されていないのでそれによってプロトタイプ汚染攻撃ができそうだと思いつきます。
しかし、サイトの最下部のところに書いてある実行環境を見ると、これはDenoで動いているようで、__proto__は存在しません
そこでobject.constructor.prototypeを利用します。

以下のようなJSONをユーザープロフィールの更新(PATCH /api/me)で送り付けることで、config.adminsSet.myUsernametrueにすることができます。

{
  "bio": "po",
  "constructor": {
    "prototype": {
      "myUsername": true
    }
  }
}

ただ、これだけでは不十分です。これだとconfig.adminsSet[username]?.includes('active')true.includes('active')となってしまいます。

では以下のようなリクエストを送るとどうでしょうか。

{
  "bio": "po",
  "constructor": {
    "prototype": {
      "myUsername": ["active"]
    }
  }
}

utils.tsmergeObject関数をよく読むと配列はオブジェクトになってしまうことがわかります。includesという関数は調べてみると配列以外にも文字列型にも存在します

{
  "bio": "po",
  "constructor": {
    "prototype": {
      "myUsername": "active"
    }
  }
}

なので最終的にはこのように送り付けることで、config.adminsSet[username]?.includes('active')'active'.includes('active')になり、フラグを表示できるようになります。

プロトタイプ汚染攻撃は結構なことができるはずなので非想定解がないか割とヒヤヒヤしてました。
ちなみにプロトタイプ汚染されてると/api/meなどでそれらのプロパティが追加されるので意図せずそれがヒントになってたかもしれません…。

問題名の「pollen」は「prototype pollution」に似てる単語を探してきて、「common ancestor」はオブジェクトのプロトタイプをすべての値が基本的に継承していることが由来です。

コード

実はそれぞれの問題で、言語とフレームワークが違ったりします。

触ったことのない言語やフレームワークを触れたのは楽しかったです。

最後に

CPCTFへの参加、作問、スコアサーバーのサーバーの開発やビジュアライザの開発やそれらのデプロイ、ありがとうございました!


  1. 動作確認はしてないです ↩︎

  2. 花粉症の人にキレられそう ↩︎

sappi_red icon
この記事を書いた人
sappi_red

19B/22M。SysAd班。 JavaScript書いたりTypeScript書いたりGo書いたりRust書いたり…

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】 feature image
2018年11月3日
ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】
Azon icon Azon
2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2023年4月27日
Vulkanのデバイスドライバを自作してみた
kegra icon kegra
2021年12月8日
C++ with JUCEでステレオパンを作ってみた【AdC2021 26日目】
liquid1224 icon liquid1224
2021年5月16日
CPCTFを支えたインフラ
mazrean icon mazrean
記事一覧 タグ一覧 Google アナリティクスについて