Lion Cipher Lab
Exploit and fix Local File Inclusion (LFI) — path traversal to code execution (in your own lab)
- Penetration Testing / Red Team
- Intermediate
- ~60 min
- Free · no account required
Why it matters
Local File Inclusion is a flagship injection-family web bug: the app builds a FILE PATH out of user input and then opens or include()s it, so the attacker chooses which file the server reads. In PHP the include() family makes the impact brutal because an included file is not just read, it is EXECUTED — which is why LFI so often becomes Remote Code Execution. The bug hides in ordinary features: page routers (?page=about), template loaders, language selectors (?lang=en), and file-download/view endpoints. In this lab you build exactly that mistake on your own machine — a page router that does…
What you'll learn
- explaining Local File Inclusion (LFI): a web app builds a filesystem PATH out of user input and then opens or include()s it, so whoever controls the input controls which file the server reads
- recognizing path traversal — how `../` (parent directory) climbs out of the intended folder, and why include($_GET['page']) lets ?page=../../../../etc/passwd return a system file
- building a tiny, deliberately vulnerable PHP page-router app on your own machine in a single Docker container (php:8.2-apache) and confirming normal use before you attack it
- using PHP stream wrappers as an attacker: php://filter/convert.base64-encode to exfiltrate the app's own source code without executing it
- escalating LFI to Remote Code Execution locally and benignly via LOG POISONING — writing a PHP payload into the Apache access log through the User-Agent header, then including that log so the payload runs (a benign `id`)
- fixing it properly: never pass user input to include(); map an OPAQUE page id through a strict ALLOWLIST to a fixed file path, disable allow_url_include, and — if a filename must be accepted — canonicalize with realpath() and verify the resolved path stays inside a base directory
- explaining why blocklisting `../` is a losing fix (URL/Unicode encodings, nested `....//`, absolute paths, and stream wrappers all bypass it) and why removing user-controlled paths entirely is the durable one
- distinguishing LFI, RFI (Remote File Inclusion), and pure path traversal, and naming the same bug class across languages: Node path.join()/fs without containment, Java new File(), Python open() built from request data
Steps in this lab
- What LFI and path traversal are — the vocabulary and the ground rules
- Set up: build the VULNERABLE app and run it in one container
- Confirm normal use, then exploit path traversal to read /etc/passwd
- Read the app's own source code with a PHP wrapper (php://filter)
- Escalate LFI to code execution — benign, local LOG POISONING
- Why blocklisting `../` is the wrong fix (and LFI vs RFI vs traversal)
- Fix it: map an opaque id through a strict allowlist
- What you did — and the reflex to keep