0x00 简介

曾遇到一个反序列化漏洞,没有找到利用链,但是存在DOMDocument类的加载

当我们实例化一个新的DOMDocument类的时候,通过var_dump,可以看到类的成员变量

但是当使用序列化函数,进行序列化的时候,无法将成员变量进行序列化

当然我们知道肯定不是继承自不允许反序列化的接口,比如大文件操作类SplFileObject

区别如下

image-20220420190209176

但是,其他的内置类是可以序列化成员变量的

比如DateTime,比如Exception,当然这其中又存在一些差别

本文就以这三个类,进行分析

  • DOMDocument
  • DateTime
  • Exception

0x01 前置知识

只读属性

这里只有dom类型的类才存在只读属性,但是在php中,是php8才具有的性质

不知道怎么设置的,在php7中也存在关键字readonly,但是没找到怎么设置参数

PHP 8.1 正式发布,来看下有哪些新功能

但是在dom类中,不仅仅存在只读属性,常规属性同样不能被序列化,这里猜测,和只读没关系

image-20220421101932287

看到一个个人设置只读属性的例子,并不是通过什么关键字来设置的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Example{
private $__readOnly = 'hello world';
function __get($name) {
if($name === 'readOnly')
return $this->__readOnly;
echo("Invalid property: " . __CLASS__ . "->$name");
}

function __set($name, $value) {
echo("Can't set property: " . __CLASS__ . "->$name");
}
}
?>

当然,存在这个属性的参数是没办法通过反序列化赋值的,但是其他的参数可以

image-20220421170910372

image-20220421170825293

这里还需要一些关于C语言的前置知识

宏定义

定义方式如下,简单说调用宏定义的时候,就是替换成定义的内容

image-20220420191339504

结构体

借用key师傅的文档一下

PHP7中存在一个zval的结构体,可以用来表示任意变量类型

1
2
3
4
5
struct _zval_struct {
zend_value value;
union u1;
union u2;
};

u1

1
2
3
4
5
6
7
8
9
10
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type, /* active type */
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved) /* call info for EX(This) */
} v;
uint32_t type_info;
} u1;

u2

1
2
3
4
5
6
7
8
9
10
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* literal cache slot */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
uint32_t access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
} u2;

u1中的type表示的数据类型

image-20220420192131809

值一半存储在value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef union _zend_value {
zend_long lval; //整型
double dval; // 浮点
zend_refcounted *counted;
zend_string *str; //字符串
zend_array *arr; // 数组
zend_object *obj; // 对象
zend_resource *res; // 资源
zend_reference *ref; // 引用
zend_ast_ref *ast; // ast
zval *zv; // zval
void *ptr; // 空
zend_class_entry *ce; // 类
zend_function *func; // 函数
...
} zend_value;

0x02 调试

这里为了实现对比,实验数据用四个类

  • DOMDocument:无法序列化
  • DateTime:可以序列化
  • Exception:可以序列化
  • 自定义类:可以序列化

公共流程

走一下一个类的序列化的基本流程,测试代码

1
2
$now = new DateTime();
echo serialize($now);

在序列化函数处下断点,经过一番调试,这里直接给出正确的数据获取路径了

image-20220421104543447

跟进 php_var_serialize函数,然后继续跟进php_var_serialize_intern函数

此时的buf中都是空

image-20220421104716304

此时的结构体的type8,就是我们序列化的类代表是obj

image-20220421104932802

然后会进入对应的IS_OBJECT分支,此分支用来处理待序列化的类中是否存在__sleep魔术方法,或者是否重写了serialize方法

image-20220421105019168

都不存在的化,就没return,会进入IS_ARRAY分支

由于我们的数据类型并不是数组,所以进入else分支,通过php_var_serialize_class_name函数完成类名的序列化操作

此时的buf中已经有了数据

image-20220421105351051

差异流程

走到这里差异就逐步体现出来了,这里需要提到两个关键参数

default_properties_countdefault_properties_tablenNumOfElements

是否存在默认值

对比对象

  • DOMDocument
  • Exception

主要就是对这个default_properties_countdefault_properties_table

定义的类的结构体_zend_class_entry,第六个和第八个参数

image-20220421111711622

image-20220421111827904

查找一下类的定义,先定义类,在定义7个参数,这些都会在编译时进入HashTable(EG)

image-20220421115645029

看一下调试

1
2
$e = new Exception('aaaa');
echo serialize($e);

image-20220421120036689

这也就说明可以序列化的普通属性有7

image-20220421120119470

而DOM类型的类中并没有定义默认值

1
2
3
$encoding = "UTF-8";
$c = new DOMDocument("1.0", $encoding);
echo serialize($c);

image-20220421120237177

是否自定义了获取properties

对比对象

  • DOMDocument
  • DateTime

这里的关键点就是Z_OBJPROP_P,这个宏定义

我们去查看其到底替换成了什么

(zval).value.obj->handlers->get_properties(&(zval))

image-20220421105925572

而这个DateTime类对这个函数进行了定义

对应的就是date_object_get_properties函数

image-20220421110213040

看调试,成功进入此方法

image-20220421110258121

而此处的zend_std_get_properties方法,就是在未定义的时候直接进入的

image-20220421110405286

在这里面出现了第一个关键参数default_properties_count

但是date的这个参数为0,所以并不能将所有的类成员变量都序列化

image-20220421110553067

return之后,存在自定义方法的开始对开始对props,进行更新

这里的更新主要是三个参数

image-20220421110809865

这里是第二个关键参数nNumOfElements

image-20220421110909434

在获取个数的时候,涉及到了另外一个宏定义zend_hash_num_elements

image-20220421111031355

就是直接获取之前的参数值

image-20220421111132997

然后就开始了正常的拼接流程

image-20220421111236898

对吼序列化出来的数据,也只是会有之前的三个

image-20220421111416152

dom类型的类中并没有定义get_properties

image-20220421120346991

所以最终获取到的参数个数是0

image-20220421120517159

自定义的类

1
2
3
4
class A{
public $a = "aaaa";
}
echo serialize(new A());

自定义的类的参数,就相当于是普通属性

image-20220421120723354

0x03 end

但是反序列化的时候是可以的,需要自己写一个脚本