Local File Inclusion (LFI) — basic bypasses
When an app tries to block LFI, weak filters and normalizers often fail in predictable ways. The patterns below apply to any stack where user input becomes part of a filesystem path—the examples use PHP because labs often do, but the logic (single-pass replacement, regex anchors, decoding order) is universal.
Non-recursive ../ removal
A naive defense deletes the literal substring ../ once:
$language = str_replace('../', '', $_GET['language']);
Input like ../../../../etc/passwd becomes ./languages/etc/passwd after stripping—traversal is “neutralized” only on paper. Because the replacement runs once on the original string (not repeatedly on the result), you can rebuild ../ after the filter runs. Example: ....// loses the inner ../ and leaves ../. Chaining gives real traversal again, e.g. ....//....//....//....//etc/passwd.
Similar shapes include ..././, ....\/, extra slashes (....////), and other encodings that collapse or re-form traversal after a single pass.
URL encoding (and double encoding)
Filters that look for raw . or / may miss the same bytes after decoding. Encoding ../ as %2e%2e%2f (encode both dots—some encoders skip dots) can slip past character blacklists. Older PHP (around 5.3.4 and earlier) had known issues around decoding and path handling; modern PHP is stricter, but custom WAFs and app-layer checks still mis-order “block” vs “decode,” so double encoding (encode the already-encoded string again) sometimes clears a second line of defense.
Treat this like other injection families: the same blacklist/encoding games used in command injection often transfer to LFI. Burp Decoder is a practical way to iterate encodings in testing.
Approved path prefixes (regex allowlists)
An app may require paths under a fixed prefix, e.g.:
if (preg_match('/^\.\/languages\/.+$/', $_GET['language'])) {
include($_GET['language']);
}
Discovery: watch normal traffic from forms and fuzz sibling directories under the same prefix until something matches the pattern.
Bypass: satisfy the regex then traverse out, e.g. ./languages/../../../../etc/passwd. Combine with encoding or non-recursive tricks when multiple defenses are stacked.
Appended extension (e.g. forced .php)
If the server does include($input . '.php'), modern PHP usually keeps the suffix—so you may be limited to disclosure of same-extension files (still valuable for source and secrets). Two legacy PHP angles matter only on old runtimes but are worth knowing for rare targets.
Path truncation (pre–5.3 / 5.4 era behavior)
Historically, very long path strings could hit length limits (often cited around 4096 characters on 32-bit-era PHP), with trailing normalization quirks (extra /, lone . segments, multiple slashes) behaving like typical filesystem rules. The idea: craft an enormous padding path so that, after truncation and normalization, the appended .php falls off or the resolved path ends where you need it—often starting with a non-existent leading directory segment and repeating ./ (or similar) many times so only the intended file survives the truncation math.
Example pattern to generate padding (adjust counts to hit the length you need after measuring the full string):
echo -n "non_existing_directory/../../../etc/passwd/" && for i in {1..2048}; do echo -n "./"; done
Always measure total length so truncation removes the extension (or junk) and not your target filename.
Null bytes (PHP before 5.5)
Older PHP treated %00 as a C-style string terminator in some path contexts. Payload shape: /etc/passwd%00 so the effective path becomes /etc/passwd before the appended .php is considered—obsolete on current PHP but still relevant when auditing ancient servers.
Takeaways
- Single-pass
../stripping is bypassable with residual../after substitution. - Encoding and double encoding exploit “filter before decode” mistakes; dots must be encoded when the defense keys on literal dots.
- Regex-approved prefixes are not roots: traversal after the allowed prefix often still reaches
/etc/passwdand friends. - Forced extensions narrow modern PHP LFI; truncation and null bytes are historical PHP bypasses for very old versions only.