Command InjectionBlind

Blind Command Injection: Proving Execution Through Time and DNS

When a shell runs injected input but discards output, timing and authorized callback evidence can still prove execution. Learn how to separate a real signal from noise and remove the shell boundary.

SelfSec Team5 min read
Part 3 of 3from theCommand Injectionseries
; sleep 2

At a glance

CWE-78
Interpreter
Operating-system shell
Required condition
Injected commands execute but their standard output is not returned to the requester.
Potential impact
Command execution can still be confirmed through timing or authorized callback evidence.
Primary defense
Remove the shell boundary and pass allow-listed arguments to a fixed executable.

Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.

On this page

Classic command injection is easy to confirm: you append ;id, and the uid= line comes straight back in the response. Blind command injection is the same vulnerability with the feedback channel removed. The shell still runs your command — the application just discards stdout, swallows it into a log, or returns a fixed page no matter what happened. The execution is real; the evidence is missing.

That missing evidence is the whole problem to solve. You cannot read output that was never sent back, so you stop trying to read it and instead make the server do something observable: stall for a measurable interval, or reach out to infrastructure you control. Both turn an invisible execution into a signal you can see from the outside.

When this technique applies

Blind command injection has the same source and sink as visible command injection; only the feedback path differs. It commonly appears in queued jobs, converters, backup hooks and wrappers that ignore standard output. Timing works when the HTTP request waits for the process. A callback works when the job can reach an assessment-owned service, even if it runs asynchronously.

Neither channel is universal. An absent delay may mean the job detached, and an absent callback may mean egress is blocked. Use paired controls and treat missing evidence as inconclusive.

How it happens

The root cause is identical to the in-band case — untrusted input concatenated into a shell command — but the sink throws the result away. Consider a thumbnail worker that shells out to a converter and only cares about the exit status:

$file = $_POST['filename'];
exec("convert /uploads/" . $file . " -resize 200x200 /thumbs/out.png");
echo "Thumbnail queued.";

The response is always Thumbnail queued., regardless of what convert printed or whether the command ran at all. The injection works exactly as before — $file reaches a shell as syntax — but ;id produces no visible uid= line, because stdout goes nowhere. The vulnerability is present and exploitable; it is simply silent.

A safe proof through timing

With no output channel, the first move is a timing oracle. Inject a command that deliberately stalls and compare the response time against a baseline request:

POST /thumbnail HTTP/1.1
Host: app.example
Content-Type: application/x-www-form-urlencoded

filename=cat.png;sleep 2

If this request consistently takes about two seconds longer than filename=cat.png alone, the delay command likely ran. A single slow response proves nothing, so compare repeated baseline, ;sleep 0 and ;sleep 2 groups at low volume. Platform-specific delay commands differ; detection must choose the syntax for the actual shell rather than send a noisy collection of guesses.

The attacker never sees a single byte of output. They only need the server to spend time, or send a packet, in a way the outside world can measure.

The separators differ by platform. POSIX shells recognize ;, &&, | and substitutions, while cmd.exe has a different grammar and does not use semicolon as a normal separator. An authorized assessment should fingerprint the platform from existing evidence and use one minimal benign pair.

A safe proof through an authorized callback

When the process runs asynchronously, a unique DNS name such as scan-7f31.assessment.example can act as a correlation marker. If a benign lookup of that fixed hostname is observed by callback infrastructure operated for the assessment, it establishes that the worker executed the injected command path. The hostname should contain only the random token, never command output, file contents or production data.

Account for DNS caching, job delay and unrelated resolvers. Every probe needs a new token and timestamp, and the observed source must be consistent with the target environment.

Why it matters

Blind command injection is not a weaker bug than the in-band kind; it is the same execution boundary with a quieter discovery phase. Impact follows the worker's privileges, readable files, credentials and network access. The lack of echoed output makes exploitation less convenient, but it does not restore safety.

The fix: avoid the shell

The reliable defence is the same one that ends in-band injection: never let user input reach a shell as syntax. Run the binary directly and pass arguments as an explicit array, so there is no shell to parse separators and no string for ;sleep 5 to escape into.

The vulnerable converter, fixed in PHP by validating the filename and invoking the program with an argument array through proc_open:

$file = $_POST['filename'];
$name = basename($file);
if ($name !== $file || !preg_match('/\A[a-zA-Z0-9._-]{1,120}\z/', $name)) {
    http_response_code(400);
    exit;
}

$src = '/uploads/' . $name;
$spec = [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']];
$process = proc_open(['magick', $src, '-resize', '200x200', '/thumbs/out.png'], $spec, $pipes);
foreach ($pipes as $pipe) {
    fclose($pipe);
}
proc_close($process);
echo 'Thumbnail queued.';

Because the validated filename is now one argument and no shell parses the line, a separator cannot create a second command. In Node, the same principle means execFile or spawn with an argument array, never exec with an interpolated string:

const { execFile } = require("node:child_process");
execFile("convert", [src, "-resize", "200x200", "/thumbs/out.png"], () => {});

Layer these on top: allowlist the input where its shape is known, set process time and resource limits, and run the worker with least privilege.

Signals that are not proof

  • One slow response without repeated true and false controls.
  • A delay produced by upload size, image parsing or worker queue depth.
  • A DNS lookup for a reused hostname with no unique correlation token.
  • A callback generated by the scanner host rather than the target worker.
  • A WAF timeout or gateway retry that prevents the command from reaching the application.

How SelfSec finds it

Silent sinks are exactly where blind command injection hides, so SelfSec does not rely on echoed output alone. It uses split marker, adaptive timing, true/false differential and unique out-of-band DNS/HTTP evidence, then includes the reproduction request with a confirmed finding. Core scan processing runs locally on 127.0.0.1; managed OOB confirmation uses the documented remote callback flow when enabled.

References

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about Command Injection

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The Command Injection series

  1. 01OverviewCommand Injection: How One Semicolon Hands Over Your Server7 min
  2. 02Argument InjectionArgument Injection: Hijacking a Command Without a Shell6 min
  3. 03BlindBlind Command Injection: Proving Execution Through Time and DNSReading

Related reading