When the response shows nothing: how out-of-band testing finds blind SSRF and injection

DedSec Application Security ·

A dynamic scan sends a payload, reads the response, and decides. For a large class of vulnerabilities that is enough: the injected value is reflected, an error message changes, a row appears that should not. For another class it is useless. The server-side request to an internal address is made and its result is thrown away. The injected shell command runs and its output goes nowhere the client can see. The XML parser resolves an external entity and returns the same document either way. The response is byte-for-byte what it would be on a safe server.

These are the blind vulnerabilities, and the only way to confirm most of them is to stop reading the response and start watching a second channel. That is out-of-band application security testing: you hand the target a unique address that points at infrastructure you control, and you wait to see whether the target contacts it. This post is about how that channel actually works, what a callback does and does not prove, and how its false positives differ from everything else a scanner reports.

Why in-band detection misses these

Two techniques cover most non-blind findings, and both fail here for structural reasons.

Response diffing compares the application's output across payloads and looks for a difference that implies the payload was interpreted. It needs the vulnerability to change something the client can observe: reflected content, an error, a different row count, a redirect. A blind server-side request forgery issues a back-end request whose response is never returned to the front end, so there is nothing to diff. PortSwigger's definition is exactly this: the application can be induced to issue a back-end request to a supplied URL, but the response from that request is not returned in the application's response. Diffing sees two identical pages and reports nothing.

Timing infers a vulnerability from how long the response takes. For blind SQL injection you inject a conditional sleep; for blind command injection you inject a command that pings a loopback address for a fixed number of seconds to stretch the response. It works when it works, and it is noisy by nature. A single slow response proves little: shared database load, garbage collection, a cold cache, network jitter and a busy CI runner all produce the same delay, so a timing check has to repeat and threshold, and a scanner that sends a conditional delay against tens of thousands of parameters is deliberately injecting latency into a system it is also trying to measure. Worse, timing cannot distinguish a vulnerability that executed from one that executed asynchronously, on another thread, after the response was already sent. An asynchronous sink is invisible to both diffing and timing, because by the time the dangerous work happens the request is closed.

The out-of-band channel sidesteps all of it. It does not care whether the response changed or how long it took. It asks one question: did the target, or something acting for it, reach out to an address only this payload contained?

How an OAST callback works

The mechanism has four parts: a unique address per payload, a server that is authoritative for that address across several protocols, a client that polls for interactions, and a correlation step that maps an interaction back to the request that caused it. Interactsh, the open-source OAST server and client library from ProjectDiscovery, is a concrete and auditable example, and the pieces below are drawn from its public documentation and source.

A unique address per payload. The client holds a correlation identifier, a random preamble of a fixed length by default. Each time it is asked for a new interaction address, it appends a fresh random nonce and the server's hostname, producing a subdomain like preamble-plus-nonce dot the-server-domain. The preamble is constant for a session and ties every address back to this client; the nonce makes each individual address unique, so two payloads sent to two parameters carry two different hostnames that still share one correlation prefix.

A server that is authoritative and listening. The OAST server is the authoritative DNS server for its domain: any resolver that chases the name ends up asking it, and it answers with its own address. It then runs listeners on the protocols a blind payload might use: DNS itself, HTTP and HTTPS, SMTP and LDAP, and, on a self-hosted instance with the relevant options enabled, SMB and FTP. When anything resolves or connects to one of the unique hostnames, the server records the protocol, the full hostname that was used, the query type for DNS, the raw request, the source address, and a timestamp.

A client that polls. The scanner does not receive a push. The client periodically asks the server for new interactions tied to its correlation identifier, by default every few seconds. On Interactsh, interactions returned to the client are encrypted to a key the client registered at startup. The server still receives each interaction in plaintext as it arrives, so using a shared public server means trusting its operator with whatever the target sends. Self-hosting removes that intermediary.

Correlation back to the exact request. This is the part that turns a callback into a finding. Because the scanner chose the hostname before it sent the payload, it already knows which request and which parameter that hostname went into. When an interaction arrives carrying that hostname, the scanner looks it up in its own table and attaches it to the originating request. Nuclei, which drives many out-of-band templates, embeds a placeholder in the outgoing request that is replaced at runtime with a unique interaction URL, and then matches on the interaction protocol that comes back, handling the correlation for the template author. The practitioner does not reconcile logs by hand; the unique-address-per-payload design does it.

What the payloads look like, briefly

The same callback machinery confirms several unrelated bugs, which is why it is worth building once.

A DNS interaction is weaker evidence than an HTTP one

Not all callbacks are equal, and conflating them is a common reporting error.

An HTTP interaction from the target is strong: something performed a full request to a unique URL that existed only inside one payload. A DNS-only interaction is weaker, for three reasons.

First, DNS resolution is recursive and indirect. The query that reaches your authoritative server usually comes from a resolver, not from the vulnerable host, so the source address of a DNS interaction is often the organisation's resolver or a cloud DNS service, not the machine that ran the payload. That is still evidence the name was resolved somewhere in the target's path, but it does not place the lookup on the vulnerable host.

Second, egress policy commonly allows DNS while blocking outbound HTTP. PortSwigger documents the exact pattern: it is common to see a DNS lookup for the callback domain but no subsequent HTTP request, because the application tried to make the request, which triggered the resolution, but the connection itself was blocked at the network layer. A DNS-only hit is consistent both with a real vulnerability behind an egress firewall and with something merely resolving a name it saw.

Third, names get resolved for reasons that have nothing to do with code execution, which is the subject of the next section. An HTTP interaction is much harder to produce by accident than a DNS lookup.

So a DNS-only callback should be reported as a weaker observation than an HTTP callback, and where it matters, the payload should try to force a full HTTP interaction to raise confidence before the finding is called confirmed.

False positives that are specific to OAST

The out-of-band channel has its own failure mode: something other than the vulnerability contacts the callback host. These do not look like ordinary false positives, and triaging them needs different signals.

Three signals separate these from a real finding, and they are general triage practice rather than any one product's method:

  1. A distinct correlation value per parameter. If every payload carries its own unique hostname, a callback names the exact request and parameter it belongs to, and a late or spurious interaction cannot be laundered onto an unrelated finding.
  2. Source address and network of the interaction. An interaction whose source resolves to a link-preview provider, a mail-security vendor or a scanning service's network is almost certainly a pre-fetch, not the target. Interactsh records the remote address and the client can add autonomous-system data for the source address; a hit from the target's own address space is a different claim from a hit originating at a third-party scanner.
  3. The timing window. An interaction arriving within the expected span after the request supports causation; one arriving much later, or at a regular interval unrelated to the scan, suggests a pipeline or a bot and should lower confidence rather than confirm.

False negatives: no callback is not proof of safety

The converse error is reading an absent callback as "not vulnerable". An out-of-band check only produces a negative you can trust when the target could actually have reached the callback host.

An out-of-band negative is therefore conditional. It is only meaningful alongside evidence that the callback path was open, which is one more reason a scan's coverage record matters as much as its findings.

Operational trade-offs: public versus self-hosted, and scope

Where the callback server lives is a data-handling decision. A public, shared interaction server is the zero-setup option, and it is the right one for a quick check. But the payloads, and anything the target concatenates into a callback, leave to a third party. For blind SQL injection the whole point can be to exfiltrate data within the DNS name, so a confirmed finding on a public server may have sent a fragment of production data through someone else's infrastructure. Interactsh encrypts interactions before handing them back to the client, but the server sees them in the clear on arrival, and the default public servers are documented as subject to rotation or downtime at any time, so they are explicitly not something to depend on. Self-hosting keeps the data in your own boundary, at the cost of running it: you need a domain whose nameservers are delegated to your server, the matching NS and address records, a reachable host, and a wildcard certificate if you want HTTPS interactions. Burp Collaborator's documentation describes the same private-server model and notes that using a bare IP instead of a delegated domain disables DNS-based detection and the interactions that depend on it.

Callbacks define scope, and scope must be enforced. An out-of-band payload is an instruction to make a third party, or the target, contact your server. Pointed at a host you are not authorised to test, that is unsolicited traffic you induced, and a callback hostname embedded in a payload can travel further than the system under test, into logs, mail and downstream services. The scope boundary for out-of-band checks is the same discipline argued for active external checks in why an EASM finding needs evidence: the hosts you probe, and the systems you induce to call home, should be inside an authorised scope that is explicit and enforced, not inferred.

Where AppSecD fits

AppSecD's dynamic testing uses Nuclei, Dalfox, SQLMap, Commix, Katana, Arjun, FFuf and Interactsh for out-of-band detection, alongside more than 150 native checks and API discovery across more than 50 web frameworks. It uses Interactsh as the out-of-band channel, and those findings go into the same deduplicated finding model and lifecycle as the rest of the platform. The mechanism, triage signals and checklist in this post, correlation per parameter, the source network of an interaction, the timing window, and treating a DNS-only hit as weaker than an HTTP one, are general out-of-band practice, not a description of how AppSecD generates payloads, correlates interactions or weighs a callback internally.

A triage checklist for an out-of-band hit

Whatever tooling produced the callback, these questions separate a finding from an artefact.

  1. Which protocol called back? An HTTP or HTTPS interaction is strong. A DNS-only interaction is weaker and should be labelled as such, not reported as a confirmed request.
  2. What was the source address, and whose network is it? The target's own address space supports the finding. A link-preview provider, a mail-security vendor or a scanning service's network points to a pre-fetch, not a vulnerability.
  3. Does a distinct correlation value tie it to one request and parameter? If the callback hostname was unique to one payload, trust the attribution. If it only carries a shared prefix, be suspicious of a late or misattributed interaction.
  4. Did it arrive inside the expected window? A callback close in time to the request supports causation. One arriving much later, or on a regular cadence, suggests a bot or a log pipeline.
  5. If it is DNS-only, can you force an HTTP interaction? Raising the payload from a lookup to a full request distinguishes an egress-filtered vulnerability from a name that was merely resolved.
  6. If there was no callback, could there have been one? Confirm the target's egress and DNS would have allowed the call, and that the payload reached the sink intact, before reading silence as safe.
  7. Did the payload or any exfiltrated data leave your boundary? If a public interaction server was used and data was concatenated into a callback, account for where that data went, and decide whether this class of check should run against a self-hosted server instead.

A blind vulnerability is defined by the response telling you nothing. The callback is the only thing that does, so the discipline is in reading it precisely: which channel, from where, tied to what, and when.

More about AppSecD · All posts