PHP内核分析-FPM和disable_function安全问题

栏目: PHP · 发布时间: 4年前

内容简介:可能最近工作比较忙吧,也可能是比较懒了,效率不太高,最近几个月里断断续续看了部分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命令行解释器

PHP内核分析-FPM和disable_function安全问题

sbin 目录下包含fpm,还需要运行的配置文件。

  • 指定fpm的配置文件,从编译后的目录复制 php-fpm.conf.default 并重命名为 php-fpm.conf

  • 指定php的配置文件,从源码目录中复制 php.ini-development 并重命名为 php.ini

PHP内核分析-FPM和disable_function安全问题

自行配置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进程如期只启动一个:

PHP内核分析-FPM和disable_function安全问题

fastcgi和FPM

浏览器请求Web Server上的html静态资源时,相互是通过Http协议进行通信,Web Server直接返回结果。

当请求的资源为php等动态脚本资源时,Web Server根据配置会将请求文件交给后面的php解释器进行处理,然后返回处理结果给客户端。

Web Server和PHP解释器之间通过 CGI/FastCGI 协议进行通信,流程如图所示:

PHP内核分析-FPM和disable_function安全问题

几者之间的关系:

  • 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自身实现能够不受外部环境影响,保持一定的独立性,完成对各类环境的适配。

PHP内核分析-FPM和disable_function安全问题

常用的就是命令行的CLI-SAPI,Web的FPM-SAPI,SAPI目录结构如下:

PHP内核分析-FPM和disable_function安全问题

在不同的SAPI中,有着不同但相似的生命周期,对比分析一下CLI-SAPI,FPM-SAPI

CLI生命周期

cli 的整个生命周期可以大致分为五个步骤

PHP内核分析-FPM和disable_function安全问题

cli 模式下,每次脚本执行都会完整经历 5 个步骤。

FPM生命周期

FPM 的整个生命周期相对于 cli 更细化也有一定的变化

PHP内核分析-FPM和disable_function安全问题

在FPM中 模块初始化阶段 仅在启动时运行一次,其他步骤会多次执行,循环处理每个FastCgi请求。

php_disable_functions

php中对于php.ini中设置的 disable_functions 禁用函数列表的处理在 php_module_startup 阶段中的 php_disable_functions 函数。

PHP内核分析-FPM和disable_function安全问题

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_modulecgi-fcgi

PHP内核分析-FPM和disable_function安全问题

cgi-fcgi 是phpinfo中的第一个module, disable_functions 在Core中

PHP内核分析-FPM和disable_function安全问题

放行至正确的 Core ,对应 zend_module 如下

PHP内核分析-FPM和disable_function安全问题

然后调用 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

PHP内核分析-FPM和disable_function安全问题

进入 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 是封装的输出函数,大致顺序就是这样,具体调用栈如下:

PHP内核分析-FPM和disable_function安全问题

phpinfo根据 EG(ini_directives) 中获取信息并打印。

攻击PHP-FPM分析

跟踪 PHP-FPM 接受恶意的FastCgi协议并解析根据 PHP_VALUE 设置 disable_functions=

fcgi_accept_request 函数中通过 accept 函数接受来自客户端的socket连接,并赋给 req->fd

PHP内核分析-FPM和disable_function安全问题

会通过外层 while 循环不停地接受连接

PHP内核分析-FPM和disable_function安全问题

同时将包含请求句柄的 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

PHP内核分析-FPM和disable_function安全问题

查看定义:

#define ZEND_INI_PARSER_CB	(CG(ini_parser_param))->ini_parser_cb

实际调用 fastcgi_ini_parser 函数:

PHP内核分析-FPM和disable_function安全问题

继续进入 fpm_php_apply_defines_ex 函数:

PHP内核分析-FPM和disable_function安全问题

此时的调用信息如下:

PHP内核分析-FPM和disable_function安全问题

继续进入 fpm_php_zend_ini_alter_master 函数

PHP内核分析-FPM和disable_function安全问题

这里从 EG(ini_directives) 找到表示 disable_functionsini_entry ,然后修改值为我们传入的内容,而phpinfo展示的值就源于这里。

还会将要禁用的函数字符串传入 fpm_php_disable 函数:

PHP内核分析-FPM和disable_function安全问题

再调用 zend_disable_function 函数修改 func->handler 完成禁用。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

博客秘诀:超人气博客是怎样炼成的

博客秘诀:超人气博客是怎样炼成的

Darren Rowse、Chris Garrett / 向怡宁 / 人民邮电出版社 / 201005 / 39.00元

作为Web 2.0的新生事物的博客,如今已蓬勃发展,呈燎原之势,业已成为许多人的一种生活方式。中国从事博客写作的人数已达千万级,各类博客网站不可胜数。 然而,为什么有的博客人气鼎盛,拥趸众多,有的博客却门前冷落,少人问津呢?究竟应该怎样写好自己的博客,才能让它吸引更多访客的关注呢?博客网站还能为我做什么呢? 本书的两位作者长期主持知名博客站点ProBlogger.net,指导了成千上万......一起来看看 《博客秘诀:超人气博客是怎样炼成的》 这本书的介绍吧!

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具