File Inclusion: LFI, RFI, and the Road to RCE

LFI vs Path Traversal: Read Bytes or Run Code

The difference between local file inclusion and path traversal: reading bytes off disk versus loading and executing a file, and how to tell which you have.

Published

Path traversal and file inclusion look identical at the payload level and diverge completely at the impact level. The same ../../../../etc/passwd confirms both, but one returns a file and the other runs it. This page draws the line so you know which techniques to reach for. Practice on a free live target.

The one-line difference

Path traversal reads bytes. File inclusion loads and runs them.

A traversal sink does something like return the file's contents to you. An inclusion sink hands the file to an interpreter (include, require, a template loader) that parses and executes it. The payload that escapes the directory is the same. What the application does next is the entire difference.

When a traversal is actually an inclusion

The two are points on one line. If the file you reach is only read, you have traversal. If the file you reach hits include(), you have inclusion, and the bytes you can place in that file become code. That is why the escalation chapters in this cluster (logs, sessions, /proc, wrappers) all aim at the same thing: get attacker-controlled bytes into a file the include will run.

How to tell which one you have

Point the parameter at a file that contains server-side code (the app's own index is ideal) and read the response:

GET /viewer?doc=index.php HTTP/1.1
Host: target.example
HTTP/1.1 200 OK
Content-Type: text/html
 
<?php
$config = require("config.php");
render_home($config);

You got the source text back with the PHP tags intact, so the file was read as bytes, not executed. This is path traversal: the viewer streams files, it does not run them. Contrast that with an inclusion sink, where the same request runs the file and you see its rendered output (or a blank, if it only defines functions) rather than its source.

Edge variants

?page=index.php      -> renders / blank  = inclusion (the code ran)
?doc=index.php       -> raw <?php source = traversal (bytes returned)
?page=php://filter/convert.base64-encode/resource=index.php   = base64 source = inclusion sink confirmed

The wrapper form is the cleanest tell. A plain read endpoint does not understand php://filter, so if a filter wrapper returns base64-encoded source, you are hitting an include and should switch to the inclusion track.

Why inclusion is strictly worse

A read discloses whatever the web user can open: source, config, keys. An inclusion does all of that and runs code, because the same sink that reads your target will execute any PHP in it. Every read target is still worth grabbing (the cheat sheet lists them), but with an include sink the file read is the opening move, not the finish.

Which track to follow

Fixing it

  • Separate the two sinks in your head when defending too. A file-download feature needs containment. An include feature needs an allow-list of view names.
  • Never route user input into include/require. Map a key to a fixed path.
  • Canonicalize and confine read endpoints to their base directory.

Full guidance is in preventing file inclusion. Next, turn an include sink into source disclosure and code execution with PHP wrappers, or try it live on a free lab.

← Back to File Inclusion: LFI, RFI, and the Road to RCE