Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🏠 Back to Blog

PHP filter wrappers for LFI (source disclosure)

Many PHP apps (and frameworks like Laravel or Symfony) expose Local File Inclusion (LFI) through unsafe include / require style patterns. PHP wrappers are stream handlers (php://…) that let code read I/O streams, memory, and more. Attackers abuse them to read source instead of executing it, and in other contexts (e.g. XXE) to widen impact toward RCE when combined with other primitives.

This note focuses on php://filter/ for source code disclosure during LFI. Other wrappers can enable RCE paths; treat that as a separate escalation step.

How php://filter works

Access filters with the php://filter/ wrapper under the general php:// scheme.

Important parameters:

  • resource — required; the stream to run the filter over (typically a local file path as the app resolves it).
  • read — which filter(s) apply to that resource on read.

PHP documents String, Conversion, Compression, and Encryption filter families. For LFI source disclosure, the usual choice is a conversion filter: convert.base64-encode, so the file bytes are emitted as Base64 instead of being interpreted as PHP.

Discovery: finding .php targets

Fuzz for PHP endpoints with tools like ffuf or gobuster (wordlists such as SecLists directory-list-2.3-small.txt), e.g.:

ffuf -w /opt/useful/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ \
  -u "http://<SERVER_IP>:<PORT>/FUZZ.php"

With LFI you are not limited to “interesting” 200 responses: 301, 302, 403, and other statuses can still name readable scripts whose source you pull via inclusion. After reading a file, grep it for include, require, and path strings to find more PHP files; alternately start from index.php and fan out. Fuzzing often surfaces scripts that are not linked from public pages.

Why plain LFI shows HTML (or “nothing”) instead of source

Including a .php file through a vulnerable include usually executes it. Configuration-only files may render empty HTML while still defining secrets or access checks—useful for behavior, bad for reading the literal source.

To get source, force the bytes through a filter that outputs safe, non-PHP text. convert.base64-encode is the standard trick: decode client-side with base64 -d (or CyberChef, Burp Decoder, etc.).

The same “execute vs read” distinction exists in other stacks: if the sink executes the language, you need a different primitive to leak source; if it only reads bytes, you may see source directly.

Payload shape (appended .php)

When the application does something like include($input . '.php'), put the basename without .php in resource, and build the filter URL so the appended suffix still lands on the intended file:

php://filter/read=convert.base64-encode/resource=config

Example over HTTP:

http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=config

The server turns resource=config into config.php when it adds the extension.

Operational tips

  • Copy the entire Base64 output; partial strings fail to decode. View page source if the browser truncates wrapping.
  • Decoding example:
echo 'PD9waHAK...SNIP...' | base64 -d
  • Mine disclosed PHP for credentials, DB keys, direct includes, and further php://filter targets to map the app.

References in this repo