Command injection explained: from input box to server takeover
Command injection (OWASP: Command Injection) is one of those vulnerabilities that makes people who understand it wince, because the impact is so immediate. A single unprotected input can hand an attacker the ability to run commands directly on your server: read files, steal data, install malware, pivot into your network. It’s a short trip from a text box to total compromise.
This guide explains how OS command injection works, how attackers find it (including when they can’t see the output), and how to prevent it properly, which, as you’ll see, is more about architecture than filtering.
What is OS command injection?
Web applications sometimes need to use functionality that lives in operating-system programs: pinging a host, converting a file, resizing an image, running a system utility. To do this, an application may build a shell command as a string and hand it to the operating system to execute.
Command injection happens when untrusted user input becomes part of that command string without being properly separated from the command itself. The attacker smuggles in extra commands, and the server’s shell runs them.
Consider a network diagnostics feature that pings an address the user provides. Under the hood it runs something like:
ping -c 4 <user_input>
If a user enters 8.8.8.8, the server runs ping -c 4 8.8.8.8. Harmless. But shells treat certain characters as command separators. What if the attacker enters this instead?
8.8.8.8; whoami
The server builds and runs:
ping -c 4 8.8.8.8; whoami
The shell sees a semicolon, finishes the ping, and then runs whoami as a second command, returning the account the server runs as. Swap whoami for something that reads sensitive files or opens a reverse connection, and the attacker owns the server.
The shell metacharacters that make it work
Command injection relies on characters that shells interpret as control syntax rather than literal text. The important families are:
;: run one command, then the next.|: pipe the first command’s output into the second.&&and||: run the next command conditionally, on success or failure.`backticks and$( ): command substitution, running a command and inserting its output.&: run a command in the background.
When user input containing any of these ends up in a shell string, the shell stops treating the input as a single value and starts interpreting the attacker’s syntax. The entire vulnerability is the loss of the boundary between the command and the data.
Blind command injection: when you can’t see the output
Often the application doesn’t show the command’s output in its response. This is blind command injection, harder to exploit, but just as severe. Attackers confirm it through indirect signals rather than reading results directly.
Time-based detection. Inject a command that causes a deliberate delay and watch whether the response takes longer:
8.8.8.8 & sleep 10
If the response consistently takes about ten seconds longer, the injected sleep executed, proving command execution even though nothing is displayed.
Out-of-band detection. Inject a command that makes the server contact a system the attacker controls: a DNS lookup or HTTP request to their server. When that interaction arrives, it confirms execution and can even carry stolen data out. This out-of-band technique overlaps conceptually with how testers confirm blind SSRF.
The lesson: not seeing output doesn’t mean the vulnerability isn’t there. Blind command injection still means arbitrary code execution.
Why command injection is so severe
Command injection sits at the top of the severity scale because it grants direct execution on the host. Realistic outcomes:
- Full server compromise: the attacker runs commands as the application’s user, often with broad access.
- Data theft: reading databases, configuration files, credentials, and secrets on the host.
- Persistence and malware: installing backdoors, web shells, or crypto-miners.
- Lateral movement: using the compromised server as a foothold to attack internal systems the public internet can’t reach.
Unlike a vulnerability that leaks one user’s data, command injection typically compromises the entire application environment. It belongs to the Injection family in the OWASP Top 10, alongside its cousin SQL injection: same root cause (untrusted data interpreted as commands), different interpreter.
How to prevent command injection
Here’s the key insight: you cannot reliably filter your way to safety. Shells have too many ways to express the same thing, and blocklists always miss something. Real prevention is architectural.
1. Don’t call the shell at all (the best fix)
The strongest defence is to avoid invoking the operating-system shell entirely. Nearly everything teams reach for a shell command to do (reading files, making network requests, manipulating images) has a native library or API in the application’s language. Using those built-in functions means there’s no shell to inject into in the first place. If you never build a command string from user input, command injection can’t happen.
2. Use parameterised execution (when you must run a program)
Sometimes you genuinely need to run an external program. When you do, never build a single shell string. Instead use the parameterised interface almost every language offers, which takes the program and its arguments as a separate list:
# Dangerous: one shell string, input is interpreted as syntax
run("ping -c 4 " + user_input)
# Safe: program and arguments passed as a list, input is pure data
run(["ping", "-c", "4", user_input])
Passing arguments as a list (and not through a shell) means the operating system treats user_input strictly as a single argument value. Shell metacharacters inside it are just literal characters. There’s no shell parsing them as commands. This is the command-injection equivalent of parameterised database queries.
3. Validate input with strict allowlists
As defence in depth, validate input against a strict allowlist of what’s acceptable: for an IP address, only digits and dots in the right structure; for a filename, only expected characters. Allowlisting (“accept only known-good”) is far more robust than blocklisting (“reject known-bad”), because you don’t have to anticipate every dangerous character and encoding. Treat this as a second layer, not the primary control.
4. Apply least privilege
Run the application with the minimum operating-system privileges it needs. If command injection does occur, least privilege limits what the attacker can reach: they can’t read files or touch systems the application account has no access to. Combined with network segmentation, it contains the blast radius.
How testers find it
When testing with authorisation, the approach mirrors other injection hunting:
- Find features that likely shell out, anything invoking system utilities: network tools, file conversion, image processing, backup or export functions, admin diagnostics.
- Inject a separator plus a benign command and look for its effect in the response.
- If nothing shows, go blind, use a time delay and watch response timing, or an out-of-band callback to a server you control.
- Confirm carefully and minimally. Prove execution without running anything destructive or exceeding scope.
The tell-tale sign is always the boundary failing: your input stops being treated as a value and starts being treated as syntax.
Key takeaways
- Command injection lets an attacker run operating-system commands on the server by breaking out of a shell command string with metacharacters like
;,|, and$(). - Blind command injection (no visible output) is confirmed via time delays or out-of-band callbacks and is just as dangerous.
- The impact is typically full server compromise: data theft, malware, lateral movement.
- Prevention is architectural: don’t call the shell, use parameterised execution when you must, and add allowlist validation and least privilege as layers. Filtering alone is not enough.
Command injection follows the golden rule of injection vulnerabilities: never let untrusted data be interpreted as a command. Keep data and commands strictly separate, and the entire class disappears.
To practise this legally, PortSwigger’s free OS command injection labs walk through both the visible and blind cases, and DVWA ships a command injection module you can run locally. If you reach a shell on an engagement, GTFOBins catalogues which ordinary Unix binaries can be abused to escalate from there.
Keep going with the SQL injection & SQLmap guide, the OWASP Top 10 breakdown, and the SSRF explainer. Practise safely with our tools and training.