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

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

内容简介:可能最近工作比较忙吧,也可能是比较懒了,效率不太高,最近几个月里断断续续看了部分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 完成禁用。


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

查看所有标签

猜你喜欢:

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

Ajax for Web Application Developers

Ajax for Web Application Developers

Kris Hadlock / Sams / 2006-10-30 / GBP 32.99

Book Description Reusable components and patterns for Ajax-driven applications Ajax is one of the latest and greatest ways to improve users’ online experience and create new and innovative web f......一起来看看 《Ajax for Web Application Developers》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具