HackproofHacks
Web App Security 15 min read

Command Injection Explained: How OS Command Injection Works and How to Prevent It

OS command injection is one of the most dangerous web vulnerabilities, often leading straight to full server compromise. Learn how it works, how attackers detect it, and how to prevent it properly.

Hassan Ansari

Hassan Ansari

A code card showing the command-injection payload ping -c 4 8.8.8.8; whoami

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:

  1. Find features that likely shell out, anything invoking system utilities: network tools, file conversion, image processing, backup or export functions, admin diagnostics.
  2. Inject a separator plus a benign command and look for its effect in the response.
  3. If nothing shows, go blind, use a time delay and watch response timing, or an out-of-band callback to a server you control.
  4. 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.

#command injection #RCE #web security #injection #OWASP #penetration testing
Free newsletter

Liked this? I write one like it every week.

One practical security lesson in your inbox each week, explained the same simple way. Join 10,000+ readers. Unsubscribe anytime.

From the article

Need a security assessment?

HackproofHacks provides web application and API penetration testing — using the same techniques covered in this article, with your explicit authorisation.

Book a free scoping call

More on Web App Security.

All articles →
FAQ

Questions about this topic.

What is OS command injection?

OS command injection is a vulnerability where an application passes untrusted user input into a system shell command without proper handling, allowing an attacker to run their own operating-system commands on the server. Imagine a form that pings an address you type by running a shell command with your input inside it. If the application doesn't sanitise that input, you can append your own command using shell characters, and the server runs it. Because it executes commands directly on the host, command injection frequently leads to complete server compromise.

What is the difference between command injection and code injection?

Command injection targets the operating system's command shell — the attacker injects OS commands like those you'd type in a terminal. Code injection targets the application's own programming language — the attacker injects code that the application interpreter executes, such as injected Python, PHP, or JavaScript. They're related and both extremely serious, but the layer differs: command injection abuses a call out to the system shell, while code injection abuses functions that evaluate code within the application itself.

What is blind command injection?

Blind command injection is when the attacker can inject and execute a command but can't see its output in the application's response. It's harder to exploit but just as dangerous. Testers confirm it using indirect signals: injecting a command that causes a measurable time delay and observing whether the response is delayed, or injecting a command that makes the server connect back to a system the attacker controls (an out-of-band interaction). Even without seeing output, blind command injection still allows arbitrary command execution and full compromise.

How dangerous is command injection?

It's among the most severe web vulnerabilities because it gives the attacker direct code execution on the server. From there they can read and modify files, steal credentials and data, install malware, use the server to pivot deeper into the internal network, and often take over the host entirely. Unlike vulnerabilities limited to a single user's data, command injection typically compromises the whole application environment, which is why it consistently earns critical severity ratings and high bug bounty payouts.

How do you prevent command injection?

The best prevention is to avoid calling the operating-system shell altogether — use your programming language's built-in libraries and APIs to perform the task instead of shelling out. When you genuinely must run an external program, use a parameterised interface that passes the command and its arguments as a separate list rather than building one big shell string, so user input is treated strictly as data and never interpreted as shell syntax. Add strict allowlist input validation and run the application with the least privilege necessary as defence in depth.

Why is input validation alone not enough to stop command injection?

Blocklist-style input validation — trying to strip or reject 'dangerous' characters — is fragile because shells offer many ways to achieve the same result, and attackers are skilled at finding encodings and syntax that bypass filters. A blocklist that forgets one metacharacter or one encoding leaves a hole. Input validation is a useful defence-in-depth layer, especially strict allowlists, but the reliable fix is architectural: don't pass user input to a shell at all, and use parameterised execution so the input can never be interpreted as a command.