こんにちは、Nemoolaです!
前回の記事に引き続き、AlpacaHackのデイリーチャレンジに挑戦しようと思います!
One More Login Challenge
問題の概要
ログインフォームがあるWebアプリで、バックエンドにMongoDBが使われている問題。
アプローチ
MongoDBを使ったことがなかったので、まず公式ドキュメントを読むところからスタート。
ドキュメントを読み進めていくと クエリ演算子 というものを見つけた。
その中でも$exists演算子が気になった。「フィールドが存在するかどうか」を条件にクエリを書ける演算子で、これを使えば何かできそうな予感がした。
攻撃方針を組み立てる
次に、この演算子をどうやってリクエストに乗せるかを考えた。ソースコードに以下の記述があった。
// Allow both application/x-www-form-urlencoded and application/json app.use(urlencoded({ extended: false })); app.use(express.json());
ここで2つのミドルウェアが登録されている点が重要だ。
urlencoded({ extended: false })の制約
Express公式ドキュメントによると、extended: falseの場合、パース結果の値は 文字列か配列のみ になる。つまり通常のHTMLフォーム送信(application/x-www-form-urlencoded)ではpasswordに渡せるのは文字列だけで、{"$exists": true}のようなオブジェクトは注入できない。
express.json()が突破口になる
一方でexpress.json()はContent-Type: application/jsonのリクエストをJSON.parse()でパースし、任意のオブジェクト構造をそのままreq.bodyに展開する。つまりpasswordの値を文字列ではなく{"$exists": true}というオブジェクトとして渡せる。
この2つのミドルウェアが共存していることで、JSONで送るだけで型の制約を回避できる状態になっていた。
実行
curl -X POST http://34.170.146.252:19790/ \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":{"$exists": true}}'
passwordフィールドが「存在する」というクエリになるため、実際のパスワードと照合せずに条件を満たしてしまう。NoSQL Injection成立!
FLAG: Alpaca{M0ng0_is_a_w3ird_D4taba5e...}
HTML2PNG
問題の概要
HTMLを送るとPNGに変換して返してくれるWebアプリ。Puppeteerを使ってサーバーサイドでレンダリングしている。
ソースコードを読む
ソースコードに以下の記述があった。
await page.goto(`file://${htmlPath}`, { waitUntil: "networkidle0" });
file://スキームでローカルファイルを開いている。ここで「ブラウザでfile://のディレクトリを開くとファイル一覧が見れる仕様があったな」と思い出した。
Step 1: ファイル名を特定する
PuppeteerはJavaScriptを実行できるので、window.location.hrefでルートディレクトリに飛ばせばファイル一覧が取得できるはず。
<html> <body> <script> window.location.href = "/"; </script> </body> </html>
返ってきたPNGを確認すると、ルートディレクトリのファイル一覧が写っていた。その中にflag-3f1816a5.txtを発見。
Step 2: フラグを読む
ファイル名がわかったので、あとは直接開くだけ。
<html> <body> <script> window.location.href = "/flag-3f1816a5.txt"; </script> </body> </html>
FLAG: Alpaca{Puppet3er_m4g1C!}
まとめ
| 問題 | 分類 | ポイント |
|---|---|---|
| One More Login Challenge | Web / NoSQL Injection | JSONボディでMongoDBクエリ演算子を注入 |
| HTML2PNG | Web / LFI | file://スキームとJavaScriptリダイレクトでサーバー内ファイルを読み取り |
どちらもソースコードの「ちょっとした仕様」を突いた問題だった。ドキュメントを丁寧に読む習慣が活きた回だったと思う。