Server-side
OS Command Injection: The Complete Practical Guide
What OS command injection is, how to exploit it (in-band, blind, OOB, filter bypass, argument injection) and prevent it. With free labs to practice on.
Published

OS command injection is what happens when a web application builds a shell command out of your input and runs it. A feature that pings a host, converts an image, or makes a backup quietly hands your string to the system shell, and if it does that without care, you break out of the data and append commands of your own. They run with the web process's privileges, so it is an immediate jump to remote code execution and often the fastest path to owning the host. Most guides treat this as a one-liner, but real targets demand blind detection, out-of-band callbacks, and filter bypass. Want to feel it rather than read about it? Sign up free and try it on a live target.
What is OS command injection?
The mental model in one sentence: the server builds a shell string from your input, so you break out of the data and add a command. Somewhere in the code, a feature takes a value you control, like a hostname to ping, and drops it into a command line that gets executed by a shell. The shell does not know which part was meant to be data. If you include a shell metacharacter like a semicolon, the shell treats what follows as a new command and runs it.
How the bug gets built
It almost always looks like a convenience. The developer needs to call a system tool, so they build the command as a string and pass it to a shell:
# vulnerable: host flows straight into a shell command
host = request.args["host"]
output = subprocess.check_output(f"ping -c 1 {host}", shell=True)With shell=True (or PHP system(), or Node child_process.exec, or a backtick), the whole string is parsed by /bin/sh. Send host=127.0.0.1; id and the shell runs ping -c 1 127.0.0.1, then runs id. The bug is the shell parsing a string that contains your input.
Impact
Command injection is a direct remote code execution primitive. With it you read files, steal secrets and credentials, pivot to internal services the host can reach, establish persistence, and frequently escalate to full host takeover. Even when the web user is unprivileged, code execution on the box is the foothold every other step builds on. It is consistently one of the highest-severity findings on a real engagement.
How to find it
Look for features that are backed by a system tool:
- Network tools: ping, traceroute, nslookup, a "check host" or "test connection" button.
- Media and documents: image resize or convert (ImageMagick), PDF generation, thumbnailing, metadata reads.
- File and admin operations: backups, archive extraction, log processing, "export", git operations.
For each, inject a separator and a benign second command and watch for its effect. If you see command output in the response, you have in-band injection. If you do not, move to blind detection.
In-band vs blind vs out-of-band
Three detection modes, depending on what the app shows you:
- In-band: the command output appears in the response. The easiest case. See classic in-band injection.
- Blind: nothing is reflected, so you prove execution with a time delay or an out-of-band callback, and exfiltrate data through that channel. See blind and out-of-band detection.
- Filtered: the app blocks characters or keywords, so you bypass the filter with shell tricks. See filter and blacklist bypass.
Separators and chaining
The shell gives you several ways to add a command, and a filter that catches one often misses another:
; command # run after (Unix)
| command # pipe the first command's output into the second
|| command # run only if the first command fails
& command # run the first in the background, then the second
&& command # run only if the first command succeeds
$(command) # command substitution: run command, substitute its output
`command` # older backtick substitutionA URL-encoded newline (%0a) is also a separator and is valuable when ; is filtered. Command substitution with $(...) is special: it runs your command and splices its output into the surrounding argument, which is the key to out-of-band exfiltration.
A worked example
Say a diagnostics page posts a hostname to a ping endpoint. You send a normal value and get the ping output back, so the response reflects the command's result. Now you append a second command with a semicolon:
POST /api/diagnostics/ping HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
Cookie: session=...
host=127.0.0.1;idHTTP/1.1 200 OK
Content-Type: text/plain
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.034 ms
uid=33(www-data) gid=33(www-data) groups=33(www-data)The uid=33(www-data) line is id running on the server. In-band injection confirmed, and you are executing as the web user. From here you read a file (;cat /etc/passwd), and if a filter or a lack of output gets in the way, you move to the blind and filter-bypass techniques in the spokes. That is the loop: find a shell-backed feature, inject a separator, confirm execution, then adapt to whatever the app does or does not show you.
Prevention
- Do not call a shell. Use the language's process API with an argument array (
execFile,subprocess.run([...], shell=False)), so your input is passed as a single argument and never parsed as a command. - Prefer a native library (a DNS resolver, an image library, a PDF renderer) over shelling out at all.
- Allow-list the input against a strict pattern (a hostname, an integer) and reject everything else. Never try to blacklist shell characters.
- Never build the command by string concatenation.
- Harden the runtime: run the worker unprivileged, in a locked-down container with egress filtering, so even successful execution hits a dead end.
The full per-language detail is in preventing command injection.
Where it connects
Command injection is one of several roads to the same destination, remote code execution. A file upload that lands a web shell gets you command execution a different way, and an SSRF that reaches an exploitable internal service can be escalated into a command primitive too. Command injection is the most direct of the three: the shell is right there, waiting for a separator.
Where to go next
Work through the cluster in order, or jump to what you need. Start with classic in-band injection, then blind and out-of-band detection and filter and blacklist bypass. When you want every payload in one place, the command injection cheat sheet has them. And when you are ready to break something real, sign up free and start hacking.