はじめに
こんにちは、19の翠(sappi_red)です。
いつもはSysAd班で部内サービスの開発・保守をしています。
CPCTFへのご参加ありがとうございました。
ぼくはWeb問の4つ(最大獲得可能点数合計1200点分)の作問と、スコアサーバーのフロントエンドの開発をしていました。
この記事では、CPCTF2021の作問者writeupを書いていきます。スコアサーバーの記事は後日出ます!
作問者writeup
- Web/Are you still using IE 8? (200点)
- Web/Line to line (200点)
- Web/Offline compatible (300点)
- Web/Pollen common ancestor (500点)
Web/Are you still using IE 8? (200点, 57AC)
タイトルから何となくわかるように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)
サイトにアクセスすると、「パスワードはクライアントのソースコードのコメントを参照すること。」と書いてあります。
ソースコードは問題で配れているわけではないので何らかの方法でソースコードを入手する必要があります。
下の「Bundled with Webpack.」がほんの少しのヒントになっていたりします。
Webpackの仕組みや行っていることを調べていくと、JavaScriptのファイルをまとめてminifyしたりしていることや、minifyされた状態でもデバッグができるようにソースマップというものがあるということが書いてあります。
なので、ソースマップというものを見つければよいのですが、どこにあるんでしょうか。
そこでWebpackの設定のドキュメント(Devtool | Webpack, Output | webpack)を見ると、デフォルトだと元のファイル名に.map
を付与したところにソースマップがあることがわかります。
main.js.map
にアクセスすると、元のソースコードが含まれたソースマップが存在していて、そこにフラグがコメントで書いてあります。
ってことで、ソースコードにはコメントであってもパスワードとかは書くべきではないということと、場合によってはソースマップをは公開しないほうがよいかもしれないという問題でした。
問題名はソースマップが行の対応関係を保持していることからつけた名前なので、もしかしたらそれで分かった人もいたかもしれません。
ACあまり多くなかったので300点問題にしたほうがよかったかもですね…。
Web/Offline compatible (300点, 20AC)
一番最初は、登録したりログインしたりしても同じ画面が表示されてバグってる?ってなる問題です。
F12などでDevToolsを開いてみてみると、レスポンスがServiceWorkerから返ってきていることがわかります。
最初のアクセスのときにログインしていない状態でServiceWorkerがレスポンスのキャッシュを保持して、ログインしていてもしていない状態でも常にログインしていない状態のレスポンスを返していたというのが原因です。
ServiceWorkerが怪しいということでそのソースであるsw.js
を見ると条件分岐なしでレスポンスを返しているコードが見つかります。
なので、ServiceWorkerを無効にした状態(例: ハードリロード(Ctrl + Shift + R
)やServiceWorkerの削除)でアクセスをしてみると、フラグが表示されます。
問題名がオフライン対応なんですが、完全に嘘って感じですね。対応したつもりになっているだけのサイトでした。
知識がない状態でServiceWorkerに辿り着くには「オフライン サイト 対応」→「PWA オフライン」の順でググるとよかったりします。
Web/Pollen common ancestor (500点, 6AC)
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.myUsername
をtrue
にすることができます。
{
"bio": "po",
"constructor": {
"prototype": {
"myUsername": true
}
}
}
ただ、これだけでは不十分です。これだとconfig.adminsSet[username]?.includes('active')
はtrue.includes('active')
となってしまいます。
では以下のようなリクエストを送るとどうでしょうか。
{
"bio": "po",
"constructor": {
"prototype": {
"myUsername": ["active"]
}
}
}
utils.ts
のmergeObject
関数をよく読むと配列はオブジェクトになってしまうことがわかります。includes
という関数は調べてみると配列以外にも文字列型にも存在します。
{
"bio": "po",
"constructor": {
"prototype": {
"myUsername": "active"
}
}
}
なので最終的にはこのように送り付けることで、config.adminsSet[username]?.includes('active')
が'active'.includes('active')
になり、フラグを表示できるようになります。
プロトタイプ汚染攻撃は結構なことができるはずなので非想定解がないか割とヒヤヒヤしてました。
ちなみにプロトタイプ汚染されてると/api/me
などでそれらのプロパティが追加されるので意図せずそれがヒントになってたかもしれません…。
問題名の「pollen」は「prototype pollution」に似てる単語を探してきて、「common ancestor」はオブジェクトのプロトタイプをすべての値が基本的に継承していることが由来です。
コード
実はそれぞれの問題で、言語とフレームワークが違ったりします。
- Line to Line
- client: CoffeeScript + Solid & Webpack
- server: dart + Jaguar
- Offline compatible
- Pollen common ancestor
- client: alpine.js + SkyBlue CSS Framework
- server: Deno
触ったことのない言語やフレームワークを触れたのは楽しかったです。
最後に
CPCTFへの参加、作問、スコアサーバーのサーバーの開発やビジュアライザの開発やそれらのデプロイ、ありがとうございました!