内容简介:可能最近工作比较忙吧,也可能是比较懒了,效率不太高,最近几个月里断断续续看了部分PHP内核源码。在今年的TCTF中,出现了攻击FPM绕过沙盒的场景。决定探究下FPM生命周期和disable_function源码实现,phpinfo不能准确显示。还很多地方还不熟悉,后面再慢慢补充,膜拜RR和P总,ORZ。安装调试工具gdb
可能最近工作比较忙吧,也可能是比较懒了,效率不太高,最近几个月里断断续续看了部分 PHP 内核源码。
在今年的TCTF中,出现了攻击FPM绕过沙盒的场景。决定探究下FPM生命周期和disable_function源码实现,phpinfo不能准确显示。还很多地方还不熟悉,后面再慢慢补充,膜拜RR和P总,ORZ。
调试环境
安装调试工具gdb
apt install gdb
下载php源码:
wget https://www.php.net/distributions/php-7.1.0.tar.gz
然后对 ./configure
的配置如下
./configure --prefix=/root/php7.1.0 --enable-phpdbg-debug --enable-debug --enable-fpm CFLAGS="-g3 -gdwarf-4"
查看 Makefile
文件如下:
CC = gcc CFLAGS = $(CFLAGS_CLEAN) -prefer-non-pic -static CFLAGS_CLEAN = -I/usr/include -g3 -gdwarf-4 -fvisibility=hidden -O0 -Wall -DZEND_SIGNALS $(PROF_FLAGS) CPP = gcc -E CPPFLAGS = CXX = CXXFLAGS = -g -O0 -prefer-non-pic -static $(PROF_FLAGS) CXXFLAGS_CLEAN = -g -O0 DEBUG_CFLAGS = -Wall
这里只安装必要的debug模块+fpm模块,其他模块视需求安装。
CFLAGS="-g3 -gdwarf-4"
是对编译参数进行额外配置,关闭所有的编译优化机制,产生 gdb所必要的符号信息(符号表),并设置dwarf调试信息格式。PHP内核中定义了很多宏,gdb调试中可以通过 macro expand xxxx
命令比较方便的展开宏。
编译安装php
make && make install
bin
目录下包含常用的php命令行解释器
sbin
目录下包含fpm,还需要运行的配置文件。
-
指定fpm的配置文件,从编译后的目录复制
php-fpm.conf.default
并重命名为php-fpm.conf
-
指定php的配置文件,从源码目录中复制
php.ini-development
并重命名为php.ini
自行配置php.ini,这里主要配置 php-fpm.conf
php-fpm为多进程模型,一个master进程,多个worker进程。
master进程负责管理调度,worker进程负责处理客户端(nginx)的请求。
master进程对work进程管理一共有三种模式:
-
ondemand
,按需模式,当有请求时才会启动worker -
static
,静态模式,启动采用固定大小数量的worker -
dynamic
,动态模式,初始化一些worker,运行过程中动态调整worker数量
让fpm的工作模式为 static
,并且work进程只有一个,方便进行调试,设置配置文件如下:
pm = static ; The number of child processes to be created when pm is set to 'static' and the ; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. ; This value sets the limit on the number of simultaneous requests that will be ; served. Equivalent to the ApacheMaxClients directive with mpm_prefork. ; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP ; CGI. The below defaults are based on a server without much resources. Don't ; forget to tweak pm.* to fit your needs. ; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' ; Note: This value is mandatory. pm.max_children = 1 ; The number of child processes created on startup. ; Note: Used only when pm is set to 'dynamic' ; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2 pm.start_servers = 1 ; The desired minimum number of idle server processes. ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' pm.min_spare_servers = 1 ; The desired maximum number of idle server processes. ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' pm.max_spare_servers = 1
运行fpm
./php-fpm -c php.ini -y php-fpm.conf
ps可以发现work进程如期只启动一个:
fastcgi和FPM
浏览器请求Web Server上的html静态资源时,相互是通过Http协议进行通信,Web Server直接返回结果。
当请求的资源为php等动态脚本资源时,Web Server根据配置会将请求文件交给后面的php解释器进行处理,然后返回处理结果给客户端。
Web Server和PHP解释器之间通过 CGI/FastCGI
协议进行通信,流程如图所示:
几者之间的关系:
-
Cgi
,通信协议,是Web Server 与 后端解释器之间数据交换的方式,但性能较差 -
FastCgi
,通信协议,FastCGI是早期通用网关接口(CGI)的增强版本,一种语言无关的协议,性能的更强 -
PHP-CGI
,PHP的CGI程序,接受来自web server的cgi/FastCgi通信数据,并使用php解释并返回结果,但性能较差 -
PHP-FPM
,PHP-CGI的管理调度程序,包含PHP的CGI程序,能对php进行解释,接受来自web server的cgi/FastCgi通信数据,性能更强
SAPI–Cli和FPM
PHP文件需要解释器才能执行,需要运行在不同的环境下,比如命令行,Web等,因此使用不同的SAPI对解释器进行封装,为内部的PHP提供一套固定统一的接口, 使得PHP自身实现能够不受外部环境影响,保持一定的独立性,完成对各类环境的适配。
常用的就是命令行的CLI-SAPI,Web的FPM-SAPI,SAPI目录结构如下:
在不同的SAPI中,有着不同但相似的生命周期,对比分析一下CLI-SAPI,FPM-SAPI
CLI生命周期
cli
的整个生命周期可以大致分为五个步骤
在 cli
模式下,每次脚本执行都会完整经历 5
个步骤。
FPM生命周期
FPM
的整个生命周期相对于 cli
更细化也有一定的变化
在FPM中 模块初始化阶段
仅在启动时运行一次,其他步骤会多次执行,循环处理每个FastCgi请求。
php_disable_functions
php中对于php.ini中设置的 disable_functions
禁用函数列表的处理在 php_module_startup
阶段中的 php_disable_functions
函数。
static void php_disable_functions(void) { char *s = NULL, *e; if (!*(INI_STR("disable_functions"))) { return; } e = PG(disable_functions) = strdup(INI_STR("disable_functions")); if (e == NULL) { return; } while (*e) { switch (*e) { case ' ': case ',': if (s) { *e = '\0'; zend_disable_function(s, e-s); s = NULL; } break; default: if (!s) { s = e; } break; } e++; } if (s) { zend_disable_function(s, e-s); } }
首先通过 INI_STR("disable_functions")
这个宏获取disable_functions的字符串,如果没有设置则返回
#define INI_STR(name) zend_ini_string_ex((name), sizeof(name)-1, 0, NULL)
跟进zend_ini_string_ex
ZEND_API char *zend_ini_string_ex(char *name, uint name_length, int orig, zend_bool *exists) /* {{{ */ { zend_ini_entry *ini_entry; ini_entry = zend_hash_str_find_ptr(EG(ini_directives), name, name_length); if (ini_entry) { if (exists) { *exists = 1; } if (orig && ini_entry->modified) { return ini_entry->orig_value ? ZSTR_VAL(ini_entry->orig_value) : NULL; } else { return ini_entry->value ? ZSTR_VAL(ini_entry->value) : NULL; } } else { if (exists) { *exists = 0; } return NULL; } }
这里有个关键宏 EG(ini_directives)
,可以访问全局变量 executor_globals
中的成员
# define EG(v) (executor_globals.v) extern ZEND_API zend_executor_globals executor_globals;
executor_globals
是一个 zend_executor_globals
类型的结构体, executor_globals.ini_directives
是存放着php.ini信息的 HashTable
类型成员。
HashTable *ini_directives;
通过 zend_hash_str_find_ptr
函数从 EG(ini_directives)
中获取 disable_functions
并返回。
如果定义了 disable_functions
,通过 while
循环读取, swtich
分割函数名,将禁用函数名传入 zend_disable_function
函数
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))) { func->fn_flags &= ~(ZEND_ACC_VARIADIC | ZEND_ACC_HAS_TYPE_HINTS); func->num_args = 0; func->arg_info = NULL; func->handler = ZEND_FN(display_disabled_function); return SUCCESS; } return FAILURE; }
CG(function_table)也是一个重要的宏,可以访问全局变量compiler_globals中的成员
# define CG(v) (compiler_globals.v) extern ZEND_API struct _zend_compiler_globals compiler_globals;
compiler_globals.function_table
是存放函数信息的 HashTable
HashTable *function_table;
通过 zend_hash_str_find_ptr
函数从 CG(function_table)
中根据 function_name
获取函数指针,然后直接修改 func->handler = ZEND_FN(display_disabled_function);
,如果传入的 function_name
不是函数,不在函数表中就不操作直接返回。
ZEND_API ZEND_FUNCTION(display_disabled_function) { zend_error(E_WARNING, "%s() has been disabled for security reasons", get_active_function_name()); }
每次调用禁用函数的时候都会进入这里。
phpinfo
phpinfo一直是查看服务器php信息的可靠方式,但是在包含修改 disable_function
的参数攻击FPM后,phpinfo已经显示修改,但是测试函数仍然禁用。
PHP_FUNCTION(phpinfo) { zend_long flag = PHP_INFO_ALL; if (zend_parse_parameters(ZEND_NUM_ARGS(), "|l", &flag) == FAILURE) { return; } /* Andale! Andale! Yee-Hah! */ php_output_start_default(); php_print_info((int)flag); php_output_end(); RETURN_TRUE; }
进入 php_print_info
函数,只保留关键部分:
PHPAPI void php_print_info(int flag) { //........ zend_ini_sort_entries(); if (flag & PHP_INFO_CONFIGURATION) { php_info_print_hr(); if (!sapi_module.phpinfo_as_text) { php_info_print("<h1>Configuration</h1>\n"); } else { SECTION("Configuration"); } if (!(flag & PHP_INFO_MODULES)) { SECTION("PHP Core"); display_ini_entries(NULL); } } if (flag & PHP_INFO_MODULES) { HashTable sorted_registry; zend_hash_init(&sorted_registry, zend_hash_num_elements(&module_registry), NULL, NULL, 1); zend_hash_copy(&sorted_registry, &module_registry, NULL); zend_hash_sort(&sorted_registry, module_name_cmp, 0); zend_hash_apply(&sorted_registry, _display_module_info_func); //........ } //........ } /* }}} */
sapi_module.phpinfo_as_text
在FPM下为 0
,会调用 php_info_print
这类封装的输出函数输出带html标签的信息,然后在 zend_hash_apply
函数中继续调用传入的 _display_module_info_func
函数:
ZEND_API void ZEND_FASTCALL zend_hash_apply(HashTable *ht, apply_func_t apply_func) { uint32_t idx; Bucket *p; int result; IS_CONSISTENT(ht); HASH_PROTECT_RECURSION(ht); for (idx = 0; idx < ht->nNumUsed; idx++) { p = ht->arData + idx; if (UNEXPECTED(Z_TYPE(p->val) == IS_UNDEF)) continue; result = apply_func(&p->val); //........ }
_display_module_info_func
函数如下:
static int _display_module_info_func(zval *el) /* {{{ */ { zend_module_entry *module = (zend_module_entry*)Z_PTR_P(el); if (module->info_func || module->version) { php_info_print_module(module); } return ZEND_HASH_APPLY_KEEP; }
将 (zend_module_entry*)
类型的 module
变量传入 php_info_print_module
函数:
PHPAPI void php_info_print_module(zend_module_entry *zend_module) /* {{{ */ { if (zend_module->info_func || zend_module->version) { if (!sapi_module.phpinfo_as_text) { zend_string *url_name = php_url_encode(zend_module->name, strlen(zend_module->name)); php_strtolower(ZSTR_VAL(url_name), ZSTR_LEN(url_name)); php_info_printf("<h2><a name=\"module_%s\">%s</a></h2>\n", ZSTR_VAL(url_name), zend_module->name); efree(url_name); } else { php_info_print_table_start(); php_info_print_table_header(1, zend_module->name); php_info_print_table_end(); } if (zend_module->info_func) { zend_module->info_func(zend_module); } //......... }
此时的 zend_module
是 cgi-fcgi
cgi-fcgi
是phpinfo中的第一个module, disable_functions
在Core中
放行至正确的 Core
,对应 zend_module
如下
然后调用 zend_module->info_func(zend_module)
,也就是进入 zm_info_php_core
函数:
PHP_MINFO_FUNCTION(php_core) { /* {{{ */ php_info_print_table_start(); php_info_print_table_row(2, "PHP Version", PHP_VERSION); php_info_print_table_end(); DISPLAY_INI_ENTRIES(); }
PHP_MINFO_FUNCTION(php_core)
也是一个宏,展开就是 zm_info_php_core
进入 DISPLAY_INI_ENTRIES()
展开后的 display_ini_entries(zend_module)
函数:
PHPAPI void display_ini_entries(zend_module_entry *module) { int module_number, module_number_available; if (module) { module_number = module->module_number; } else { module_number = 0; } module_number_available = module_number; zend_hash_apply_with_argument(EG(ini_directives), php_ini_available, &module_number_available); if (module_number_available == -1) { php_info_print_table_start(); php_info_print_table_header(3, "Directive", "Local Value", "Master Value"); zend_hash_apply_with_argument(EG(ini_directives), php_ini_displayer, (void *)&module_number); php_info_print_table_end(); } }
module_number_available == -1
条件满足,进入 zend_hash_apply_with_argument(EG(ini_directives), php_ini_displayer, (void *)&module_number);
函数。
ZEND_API void ZEND_FASTCALL zend_hash_apply_with_argument(HashTable *ht, apply_func_arg_t apply_func, void *argument) { uint32_t idx; Bucket *p; int result; IS_CONSISTENT(ht); HASH_PROTECT_RECURSION(ht); for (idx = 0; idx < ht->nNumUsed; idx++) { p = ht->arData + idx; if (UNEXPECTED(Z_TYPE(p->val) == IS_UNDEF)) continue; result = apply_func(&p->val, argument); if (result & ZEND_HASH_APPLY_REMOVE) { HT_ASSERT(GC_REFCOUNT(ht) == 1); _zend_hash_del_el(ht, HT_IDX_TO_HASH(idx), p); } if (result & ZEND_HASH_APPLY_STOP) { break; } } HASH_UNPROTECT_RECURSION(ht); }
从 EG(ini_directives)
中获取 (zend_ini_entry*)
类型的配置参数结构成员,在for循环中遍历并作为参数传入 result = apply_func(&p->val, argument)
,实际进入 php_ini_displayer
函数,继续跟进:
static int php_ini_displayer(zval *el, void *arg) { zend_ini_entry *ini_entry = (zend_ini_entry*)Z_PTR_P(el); int module_number = *(int *)arg; if (ini_entry->module_number != module_number) { return 0; } if (!sapi_module.phpinfo_as_text) { PUTS("<tr>"); PUTS("<td class=\"e\">"); PHPWRITE(ZSTR_VAL(ini_entry->name), ZSTR_LEN(ini_entry->name)); PUTS("</td><td class=\"v\">"); php_ini_displayer_cb(ini_entry, ZEND_INI_DISPLAY_ACTIVE); PUTS("</td><td class=\"v\">"); php_ini_displayer_cb(ini_entry, ZEND_INI_DISPLAY_ORIG); PUTS("</td></tr>\n"); } else { PHPWRITE(ZSTR_VAL(ini_entry->name), ZSTR_LEN(ini_entry->name)); PUTS(" => "); php_ini_displayer_cb(ini_entry, ZEND_INI_DISPLAY_ACTIVE); PUTS(" => "); php_ini_displayer_cb(ini_entry, ZEND_INI_DISPLAY_ORIG); PUTS("\n"); } return 0; }
进入 if (!sapi_module.phpinfo_as_text)
分支后就是每项配置的输出, php_ini_displayer_cb
是封装的输出函数,大致顺序就是这样,具体调用栈如下:
phpinfo根据 EG(ini_directives)
中获取信息并打印。
攻击PHP-FPM分析
跟踪 PHP-FPM
接受恶意的FastCgi协议并解析根据 PHP_VALUE
设置 disable_functions=
。
fcgi_accept_request
函数中通过 accept
函数接受来自客户端的socket连接,并赋给 req->fd
。
会通过外层 while
循环不停地接受连接
同时将包含请求句柄的 request
变量存到全局变量, SG(server_context)
中,宏定义如下:
# define SG(v) (sapi_globals.v) extern SAPI_API sapi_globals_struct sapi_globals;
然后进入 init_request_info
函数:
static void init_request_info(void) { fcgi_request *request = (fcgi_request*) SG(server_context); char *env_script_filename = FCGI_GETENV(request, "SCRIPT_FILENAME"); char *env_path_translated = FCGI_GETENV(request, "PATH_TRANSLATED"); char *script_path_translated = env_script_filename; char *ini; int apache_was_here = 0; //.......... //.......... //.......... /* INI stuff */ ini = FCGI_GETENV(request, "PHP_VALUE"); if (ini) { int mode = ZEND_INI_USER; char *tmp; spprintf(&tmp, 0, "%s\n", ini); zend_parse_ini_string(tmp, 1, ZEND_INI_SCANNER_NORMAL, (zend_ini_parser_cb_t)fastcgi_ini_parser, &mode); efree(tmp); } ini = FCGI_GETENV(request, "PHP_ADMIN_VALUE"); if (ini) { int mode = ZEND_INI_SYSTEM; char *tmp; spprintf(&tmp, 0, "%s\n", ini); zend_parse_ini_string(tmp, 1, ZEND_INI_SCANNER_NORMAL, (zend_ini_parser_cb_t)fastcgi_ini_parser, &mode); efree(tmp); } }
通过 FCGI_GETENV
宏获取FastCgi请求中的 PHP_VALUE
:
fcgi_quick_getenv(request, name, sizeof(name)-1, FCGI_HASH_FUNC(name, sizeof(name)-1))
接着进入 zend_parse_ini_string
函数:
ZEND_API int zend_parse_ini_string(char *str, zend_bool unbuffered_errors, int scanner_mode, zend_ini_parser_cb_t ini_parser_cb, void *arg) { int retval; zend_ini_parser_param ini_parser_param; ini_parser_param.ini_parser_cb = ini_parser_cb; ini_parser_param.arg = arg; CG(ini_parser_param) = &ini_parser_param; if (zend_ini_prepare_string_for_scanning(str, scanner_mode) == FAILURE) { return FAILURE; } CG(ini_parser_unbuffered_errors) = unbuffered_errors; retval = ini_parse(); shutdown_ini_scanner(); if (retval == 0) { return SUCCESS; } else { return FAILURE; } }
ini_parser_param.ini_parser_cb = ini_parser_cb
实赋值的 fastcgi_ini_parser
函数,然后进入 ini_parse
进行解析,然后又使用了 ZEND_INI_PARSER_CB
宏
查看定义:
#define ZEND_INI_PARSER_CB (CG(ini_parser_param))->ini_parser_cb
实际调用 fastcgi_ini_parser
函数:
继续进入 fpm_php_apply_defines_ex
函数:
此时的调用信息如下:
继续进入 fpm_php_zend_ini_alter_master
函数
这里从 EG(ini_directives)
找到表示 disable_functions
的 ini_entry
,然后修改值为我们传入的内容,而phpinfo展示的值就源于这里。
还会将要禁用的函数字符串传入 fpm_php_disable
函数:
再调用 zend_disable_function
函数修改 func->handler
完成禁用。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 内核通信之 Netlink 源码分析和实例分析
- 内核通信之 Netlink 源码分析和实例分析
- sqlmap 内核分析 III: 核心逻辑
- 针对多个DirectX内核漏洞的分析
- Linux 系统内核崩溃分析处理简介
- Linux内核提权 CVE-2018-13405 分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
分布式机器学习:算法、理论与实践
刘铁岩、陈薇、王太峰、高飞 / 机械工业出版社 / 2018-10-20 / 89.00
人工智能和大数据时代,解决最有挑战性问题的主流方案是分布式机器学习!本书旨在全面介绍分布式机器学习的现状,深入分析其中的核心技术问题,并且讨论该领域未来的发展方向。 由微软亚洲研究院机器学习核心团队潜心力作!鄂维南院士、周志华教授倾心撰写推荐序! 本书旨在全面介绍分布式机器学习的现状,深入分析其中的核心技术问题,并且讨论该领域未来的发展方向。 全书共12章。第1章是绪论,向大家展......一起来看看 《分布式机器学习:算法、理论与实践》 这本书的介绍吧!