Server-Side Request Forgery (SSRF)
Make servers send requests on your behalf to internal services with URL parsing tricks
Server-Side Request Forgery is triggering a server to make arbitrary requests for you. Often through functionality that looks up some resource via a URL or a proxy-like functionality that intentionally lets you craft any request. If the vulnerable server is inside some private network, requests to internal services can be made that an attacker normally isn't able to reach.
Before attacking every URL field you see, you should check whether it is really the server making the request or just your own browser via fetch(). You won't gain anything from your own browser as you're already in that network. But a server may be in other networks.
Debug by making it request your own server first, then change the target to what you actually want to reach. requestrepo is a simple tool that gives you a unique subdomain which logs DNS and HTTP/HTTPS requests in full.
Tip: you might see only DNS logs coming in, but this does not mean an HTTP request is not made. Network policies of the application often block outbound networking, and internal targets may still work. Debugging is just a little harder.
Filters
The tricky part about SSRF defenses is that you often want to allow some subset of URLs to still be requested, while internal hosts should be disallowed. This implementation is known as the filter, and is often a lot more complicated than it looks on the surface. For this you can thank IP/URL formats, DNS, and unexpected HTTP features enabled by default.
With source code access, carefully read how the filter is implemented exactly to see if any of the following techniques apply. Otherwise, probe the filter with varying inputs to rule out certain checks and find bypasses that way.
IP parsing
A naive way of blocking, say localhost access, is to block keywords like "localhost" or "127.0.0.1", because surely those are the only two ways to reference localhost. Unfortunately for those developers, IP addresses are standardized to support many other formats, all explained in man inet_pton. Any IP address can be encoded like this.
The ipobf tool below implements a bunch of these encoding formats to generate a fuzzing list:
If you thought this wasn't complex enough, you can encode IPv4 addresses with IPv6 as "IPv4-mapped" addresses for backwards compatibility. These can be decimal or hex like:
Another fact about specifically loopback addresses (pointing to your locally hosted services) is that the entire 127.0.0.0/8 range is actually considered as loopback. So 127.1.2.3 works just as well, and through all the above formats.
Lastly, on Linux, 0.0.0.0 also points to your loopback. This can break some filters that categorize "private IPs" because this special IP is not always considered private, but rather "multicast".
For reference about these special types of IPs, you can check Go's implementation of IsGlobalUnicast(): https://go.dev/src/net/ip.go
URL Injection
When you have control over any part of a URL that is fetched by the server, you should ask yourself, can I control the host that this request goes to? When an injection is after the path, you're often out of luck:
But where it gets more interesting is if you can do anything before the path:
There are multiple ways to exploit this now:
Using
@attacker.tldas theINJECTION, the URL becomeshttps://example.com@attacker.tld. Anything before the@is seen as the "credential" part of the URL, whileattacker.tldnow becomes the host.Using
.attacker.tld, the URL becomeshttps://example.com.attacker.tldwhich is a registerable subdomain under the attacker.
Even when you're stuck in the path, you can still influence the URL greatly. For example:
Injecting ../whatever# here would turn the URL into https://example.com/subdir/../whatever#?safe=true, which causes https://example.com/whatever to be requested.
Parameter pollution is another realistic attack vector, where in the following injection:
We can write x&safe=false in hopes that the receiving server sees only the last value of safe. For more tricks like this, see Path Traversal.
Parser differentials
When preparing to request a URL it is common to parse it and check if it's safe, before initiating the request. It is crucial that the parsers used for the check and the request are the same though, because if they differ in any way, there is a chance that one URL is parsed in two different ways. While it may look safe to the parser during the check, it can be interpreted another way when it's actually requested.
One specific example is highlighted by SonarSource's article below:
Many libraries disagree on how to parse the following URL:
Even inside Python, two different libraries urllib and urllib3 parse the hostname as b.tld and a.tld respectively:
Python requests uses urllib3. So if it were parsed by urllib, one could hide the real URL and fake a hostname like this:
Research by Orange Tsai shows more examples of URL parser quirks potentially exploitable for SSRF: "A New Era of SSRF - Exploiting URL Parser in Trending Programming Languages!"
Another differential that's worth highlighting is in curl. When your input is parsed by practically any parser, and then the cURL command line tool requests it without disabling globbing, you can bypass any filter. Look for a missing -g flag (libcurl doesn't support globbing, only the CLI is affected). Using {} characters you can make cURL request 2 URLs instead of one, where the 2nd is bypassed.
We can do this practically by starting { in the credential part of the URL, and then ending } somewhere in the hash fragment. A normal URL parser will allow and ignore this. Then cURL recognizes the globbing syntax and splits on the , into 2 URLs:
PHP parses its host as safe.example:
But curl expands the {a,b} syntax into these 2 requests:
If you can't find a working payload online, you can look for differentials yourself through fuzzing. All you have to do is programmatically set up the parsers you want to compare, generate random inputs, and compare their outputs. We could rediscover the urllib vs. urllib3 differential this way:
Redirects
A simple but effective way of bypassing a URL filter is sending it to an attacker-controlled allowed host first, but then redirecting it to another URL in the Location: response header. If the requester follows redirects and does not re-check the new URL, the 2nd time may bypass any checks.
Many libraries implicitly follow redirects unless you opt out. And even then it's awkward to have to read the Location: header yourself and check the new URL again.
Below is a simple PHP server that returns a 302 redirect (shouldn't be cached as opposed to 301):
Tip: You can also use requestrepo for this by editing the Response. Update the status code to 302 and add a Location header set to http://localhost, then press Save.
Through a server-side redirect, paths and URL parameters aren't preserved so you'll have to add these like http://localhost/path?key=value in the Location: header, but that also means you fully control them!
The method is forced to be GET, only if the original request was a POST request, you can set the status code to 307 in order to preserve the method and body (POST). This means you can even send POST requests anywhere, you just don't have control over the body as it will be kept from the original request.
DNS Rebinding
As you know, we don't just use IP addresses on the web. Domain names are much more common. But if you're implementing a filter that differentiates internal from external IP addresses, how would you do that for domain names?
One approach often taken is to first resolve the domain to an IP address, then check that IP address just like in IP parsing (but now normalized by DNS). The problem here is that DNS entries can change. In this approach, we can change the IP address our domain resolves to right after it was checked to be safe. The request will resolve the domain once more and now get back the edited value (eg. 127.0.0.1). This is known as "DNS Rebinding" or more generally a "TOCTOU" vulnerability.
To set this up, you need a few things:
A VPS that can listen on UDP port 53 with a public IP (eg.
1.3.3.7). This is the "DNS Server"An
Arecord pointing to the IP of your DNS server (eg.ns1.hacker.tld -> 1.3.3.7)An
NSrecord pointing to theArecord (eg.rebind.hacker.tld -> ns1.hacker.tld)
When this is set up, you can run a DNS server on the VPS like this:
Requesting any domain name under the NS record (eg. test.rebind.hacker.tld) now gets resolved by your Python server.
Using online tools
Luckily existing tools such as "Singularity of Origin" have implemented this already with a smart subdomain-based configuration that anyone can use. The format is explained here:
Basically, if you want to use the "First then always second" method you should choose the fs strategy and use the following Python script to format your IPs. Here I set the first to 1.1.1.1, and the second to 127.0.0.1.
Resolving this domain name now indeed gives 1.1.1.1 first (check), then always 127.0.0.1 (use):
Tip: It may sometimes be smarter to use the rd (random) strategy and spam it until it works. Some systems have multiple checks where the first, second and third must be 1.1.1.1, for example. With random you can just get lucky that the resolutions happen to line up how you need them.
DNS can have multiple entries for one name. Each one must be checked, otherwise a library sending the request might fall back to another entry if the first one fails. You can try this in Python dnslib by just adding more reply.add_answer() calls before returning.
Some routers deny DNS replies containing loopback addresses like 127.0.0.1 to block these exact attacks. In the "Protection Bypasses" section a few more variations are explained, like using 0.0.0.0 on a Linux-based target and returning CNAME records instead which are essentially aliases to other names that the requester will have to request again. Setting the value to localhost may make the resolver internally return 127.0.0.1. If you know any records exist in the internal network of the target (eg. server.target.tld) you can set the CNAME to this, which will get its A record.
Note: All these DNS attacks assume the Host: header isn't validated by the receiving server. Because it will be on the attacker's domain still, we only change the IP. HTTPS will also break with this, so you have to hope this is not verified either. Though it is common for internal services to be run over HTTP.
Impact
Possibly the hardest part of an SSRF vulnerability is figuring out what you can do with it. In a blackbox scenario, this is a bunch of guesswork and having knowledge of your target's internal infrastructure. Though this is not a prerequisite, as you can also learn a lot about the target by playing with the SSRF primitive.
By requesting random internal services you may come across well-known applications that weren't made to be publicly accessible. Such apps can intentionally disclose sensitive information or provide dangerous actions, or even be outdated and contain n-day vulnerabilities you may be able to exploit through an SSRF.
Exactly what you can do with your SSRF depends on how it works technically.
Does it expect a certain format in the response?
-> Decides if it is Full-Read SSRF or Blind SSRF
Does it give detailed errors on failed connections?
-> Decides if you can find IP addresses before ports
How fast is a single attempt?
-> Decides how large of a range you can fuzz
Targets
With SSRFs you want to generally cross network boundaries. By requesting localhost or other internal IPs in 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16.
prips is a useful small tool that takes a subnet/range and prints out all IP addresses within. It makes creating fuzzing lists easy:
You can also access firewalled hosts that have rules to only allow connections from internal IPs. With SSRF, you've become such an internal host and can potentially request subdomains that don't seem to respond from the outside.
Cloud environments often have special metadata IPs that return information about the current machine. The most well-known is AWS at http://169.254.169.254. Other platforms have more security measures where some extra request headers are required, assuming that you cannot control these request headers. Check out all the details in the page below:
Lastly, if your SSRF is working as effectively a proxy, you can intentionally send a request to your own server and control the response headers. Some reverse proxies handle certain response headers in a special way, see Special Response Headers for details.
Docker
When your application is running inside Docker, there may be more containers running on the same machine. By default containers get an incremental IP in the 172.17.0.0/16 subnet but if a docker network is created, that subnet number (17) can increase. Containers start at .2 and increment from there.
In some desktop environments you can reach the host via the special host.docker.internal domain name. However, in most instances you need to manually find the default gateway. This will always be the .1 host on the subnet the container has (eg. 172.17.0.0/16 -> 172.17.0.1).
Ports
After finding an IP, you can try to find HTTP/HTTPS services on it by scanning ports. Depending on how fast you can fuzz, you should decide if you want to test only a few ports or many ports. In common-http-ports.txt there are the top 36 most widely used ports where you can find HTTP, with of course 80 being by far the most common.
For internal services, however, you'll often also find 8080, 8000, 5000, 3000 etc. to avoid clashing with other in-use ports. So especially on localhost, scanning a broader range is important.
Method, Path, Query & Headers
The more fields of the request you control in an SSRF, the more possibilities open up. In URL Injection there are some ideas for when you only control the Path or URL. But when you have an input where you also decide the method or a header, for example, it can become more interesting.
Surprisingly often request libraries don't properly sanitize their inputs. This means you may be able to inject special characters like \r\n (%0d%0a) into the request Method, Path or Header value to add extra headers or a body to a request. This may be required for modern Cloud metadata endpoints as mentioned in Targets. Read CRLF / Header Injection for more details about this attack.
Protocols
When inputting a URL to fetch, you're not always forced to use http: or https:. With different protocols you can trigger different behavior. Fetching a file:// URL for example, you may be able to read local files (Local File Disclosure).
SMB
On Windows, SMB also provides some interesting functionality. Firstly, on 127.0.0.1 you can always access your current machine's filesystem. Drives are mapped as shares with a $ suffix, so the following path will read C:\Windows\win.ini when requested (// starting syntax signifies SMB, you can also try smb://):
When the network policies allow it, making an SMB connection to an external server can leak the password hash of the account initiating it because Windows automatically sends it. Tools like Responder can capture these hashes. Then either relay it if you are already inside the internal network or try to crack it (Forcing Authentication to Relay).
Gopher
An old protocol called Gopher is still supported by most notably curl through gopher://. What it allows attackers to do today is send raw TCP packets and get back raw TCP responses. You specify a host like normal, then prefix the path with _ and the rest of the path becomes a URL-encoded packet you wish to send. This CyberChef recipe can be used to encode a packet.
Any service accepting TCP can be interacted with. The only limitation is that you cannot keep a TCP conversation going. Only your one packet is sent, a response is received, then the connection immediately closes. Below is a collection of known gadgets that can be exploited with a single packet when found in the internal network:
Protocol confusion (mixing)
Another idea is sending HTTP, but to a different port that doesn't necessarily speak HTTP. Protocols that are similarly newline-delimited (such as SMTP) can have commands be injected through header names or the request body. It is worth assessing what protocols are running and whether or not you can craft a valid packet for them with your SSRF request format.
One famous example is Redis which is a fast key-value store with a simple newline-delimited command protocol. Read SSRF for a detailed explanation on how it can be exploited.
This has now mostly been fixed by adding a protection looking for POST or Host: commands as heuristics of this attack, and closing the connection when either is encountered.
Full-Read SSRF
The most powerful form of SSRF is when you can read the response. Most often this will be the body, maybe in an error or just as a proxy functionality. Anyhow, through your fuzzing you should find servers and try to recognize what software they are running.
You can then browse the website by manually requesting URLs, reading href='s and requesting more. Or, write a simple proxy that you can connect to your browser to fully browse an internal website. You can take the following two mitmproxy scripts as reference, implement your own SSRF here:
Run either of these scripts with mitmproxy and a port for the proxy to listen on:
Then configure your tools or browser to use http://127.0.0.1:8081 as the proxy. In Burp Suite you do this via the Network -> Connections -> Upstream proxy servers configuration.

Blind SSRF
If your request only gets sent and the user never sees a response, it is considered "blind". This may also be the case if it requires such an esoteric response format that you're effectively never able to read any response from an unintended host.
Tip: If your response needs to be an image, you can try requesting /favicon.ico or /favicon.png for various hosts to find not only if they work, but also what software it is by their favicon. You can then reverse image search or even look for the hash on shodan.
You can still find which IPs or ports work via error messages or timing in most cases. Try working versus non-working hosts, and know that when you see "No route to host" it means the IP could not be reached, so you don't need to waste time port scanning such a host.
Automated Browsers
One last variant of SSRF is when it involves an automated headless browser. Because these make requests by design, SSRF is a natural idea. If a browser renders your HTML, many features can cause requests to be triggered, though all of these must be Simple Requests. The following repository collects all ways HTML can make blind requests:
Warning: These techniques use a lot of HTML, and some renderers require strictly correct HTML, no formatting errors such as missing " or missing </ closing tags. Ensure it follows the HTML spec without triggering any "Parse errors" the browser usually fixes for you.
PDF/Screenshot
A very powerful primitive is when you get back a PDF or screenshot of the page that the browser was on. This gives you a way to exfiltrate data. CORS blocks things like fetch() but visually a browser can often still display cross-origin data, it may just not be programmatically accessible.
Most of the techniques above are blind, but one stands out: <iframe>. This can render an external website inside another website. If the application takes a screenshot of some HTML-injected page, adding the following would render http://localhost:8000.
Temporary bits of HTML are often rendered through the file:// protocol. What's special about this is that from a file protocol, you can iframe other files without restrictions (just can't read them by default):
By pointing it to a directory, you even get a nicely rendered list of files and directories in there, so you don't have to fuzz file paths:

If iframes are denied for any reason, you can also try navigating the browser. Even with just HTML you can achieve this through the <meta> tag:
If a screenshot is made too quick (eg. before loading some resource/navigation), you can try slowing down the load by a slow image. This will hold waitUntil: "networkidle2" calls too:
For PDFs specifically, there are features that intentionally attach files via HTML to the resulting file as attachments. Try including the following three HTML tags, which work for mPDF < 7.0, WeasyPrint, and PD4ML, respectively:
After getting the PDF, look for attached files with pdfdetach:
For later versions of mPDF, one technique exists to SSRF with Gopher and another using PHP phar deserialization:
You may find n-days or 0-days in other libraries by looking at the Creator & Producer EXIF data. Libraries often leave their mark here with version numbers to search online:
Server-Side XSS
In browsers we're often not limited to just HTML, but also JavaScript. In Cross-Site Scripting (XSS) you can learn all sorts of ways to potentially execute arbitrary JavaScript with which you can access anything you would be able to in a normal XSS attack.
It is smart to enumerate some information about where your JavaScript is being executed. By reading location.href and navigator.userAgent you can quickly learn how the HTML is rendered and what software the browser is based on. Try fetch()'ing around with relative URLs to see if you can access anything interesting.
Some configuration is often different from regular browsers and may allow for more exploitation vectors. CLI flags become very important. Developers sometimes set these to fix bugs without realizing the implications.
Check if CORS is enforced. Because the --disable-web-security flag would disable such check and allow any website to request any other website's data:
If you find yourself on a file:// location, also try fetch other relative files. Some browsers will see them all as same-origin, while regular browsers should see each file as a separate origin.
Tip: some headless browser libraries don't use the standard JavaScript engine, and instead expose only a subset of functions. document.write() is a relatively reliable way to put text inside the document:
Instead of the modern fetch(), you may have to fall back to XMLHttpRequest as well:
You can attack the underlying system with techniques described in Headless Browsers. The most common problem is using outdated versions that have known memory corruption/sandbox escape vulnerabilities. The best part is that commonly, you can skip the sandbox stage because --no-sandbox is set. Some containers that have no CAP_SYS_ADMIN capability or run as root don't support the sandbox, so require this flag to be set.
Note that sometimes CLI flags can disable certain features exploits rely on, like WebAssembly or JIT.
Last updated