php反序列化基础
目录
php反序列化基础
php类与对象
魔术方法
php序列化/反序列化
php反序列化漏洞(对象注入)
php反序列化基础
php类与对象
类是定义一系列属性和操作的模板,而对象,就是把属性进行实例化,完事交给类里面的方法,进行处理。
<?php class people{ //定义类属性(类似变量),public 代表可见性(公有) public $name = 'joker'; //定义类方法(类似函数) public function smile(){ echo $this->name." is smile...\n"; } } $psycho = new people(); //根据people类实例化对象 $psycho->smile(); ?>
上述代码定义了一个people类,并在在类中定义了一个public类型的变量$name和类方法smile。然后实例化一个对象$psycho,去调用people类里面的smile方法,打印出结果。
这就是php类与对象最基础的使用。
魔术方法
为什么被称为魔法方法呢?因为是在触发了某个事件之前或之后,魔法函数会自动调用执行,而其他的普通函数必须手动调用才可以执行。PHP 将所有以 (两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 为前缀。下表为php常见的魔术方法:
方法名 | 作用 |
---|---|
__construct | 构造函数,在创建对象时候初始化对象,一般用于对变量赋初值 |
__destruct | 析构函数,和构造函数相反,在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用 |
__toString | 当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串,例如echo打印出对象就会调用此方法 |
__wakeup() | 使用unserialize时触发,反序列化恢复对象之前调用该方法 |
__sleep() | 使用serialize时触发 ,在对象被序列化前自动调用,该函数需要返回以类成员变量名作为元素的数组(该数组里的元素会影响类成员变量是否被序列化。只有出现在该数组元素里的类成员变量才会被序列化) |
__destruct() | 对象被销毁时触发 |
__call() | 在对象中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法 |
__callStatic() | 在静态上下文中调用不可访问的方法时触发 |
__get() | 读取不可访问的属性的值时会被调用(不可访问包括私有属性,或者没有初始化的属性) |
__set() | 在给不可访问属性赋值时,即在调用私有属性的时候会自动执行 |
__isset() | 当对不可访问属性调用isset()或empty()时触发 |
__unset() | 当对不可访问属性调用unset()时触发 |
__invoke() | 当脚本尝试将对象调用为函数时触发 |
额外提一下__tostring的具体触发场景:
(1) echo($obj) / print($obj) 打印时会触发
(2) 反序列化对象与字符串连接时
(3) 反序列化对象参与格式化字符串时
(4) 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5) 反序列化对象参与格式化SQL语句,绑定参数时
(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8) 反序列化的对象作为 class_exists() 的参数的时候
举个例子:
<?php class animal { private $name = 'caixukun'; public function sleep(){ echo "<hr>"; echo $this->name . " is sleeping...\n"; } public function __wakeup(){ echo "<hr>"; echo "调用了__wakeup()方法\n"; } public function __construct(){ echo "<hr>"; echo "调用了__construct()方法\n"; } public function __destruct(){ echo "<hr>"; echo "调用了__destruct()方法\n"; } public function __toString(){ echo "<hr>"; echo "调用了__toString()方法\n"; } public function __set($key, $value){ echo "<hr>"; echo "调用了__set()方法\n"; } public function __get($key) { echo "<hr>"; echo "调用了__get()方法\n"; } } $ji = new animal(); $ji->name = 1; echo $ji->name; $ji->sleep(); $ser_ji = serialize($ji); //print_r($ser_ji); print_r(unserialize($ser_ji)) ?>
php序列化/反序列化
在开发的过程中常常遇到需要把对象或者数组进行序列号存储,反序列化输出的情况。特别是当需要把数组存储到mysql数据库中时,我们时常需要将数组进行序列号操作。
php序列化(serialize):是将变量转换为可保存或传输的字符串的过程
php反序列化(unserialize):就是在适当的时候把这个字符串再转化成原来的变量使用
这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。
常见的php系列化和反系列化方式主要有:serialize,unserialize;json_encode,json_decode。
序列化
举个序列化小栗子:
<?php class object{ public $team = 'joker'; private $team_name = 'hahaha'; protected $team_group = 'biubiu'; function hahaha(){ $this->$team_members = '奥力给'; } } $object = new object(); echo serialize($object); ?>
以上是序列化之后的结果,o代表是一个对象,6是对象object的长度,3的意思是有三个类属性,后面花括号里的是类属性的内容,s表示的是类属性team的类型,4表示类属性team的长度,后面的以此类推。值得一提的是,类方法并不会参与到实例化里面。
需要注意的是变量受到不同修饰符(public,private,protected)修饰进行序列化时,序列化后变量的长度和名称会发生变化。
-
使用public修饰进行序列化后,变量$team的长度为4,正常输出。
-
使用private修饰进行序列化后,会在变量$team_name前面加上类的名称,在这里是object,并且长度会比正常大小多2个字节,也就是9+6+2=17。
-
使用protected修饰进行序列化后,会在变量$team_group前面加上*,并且长度会比正常大小多3个字节,也就是10+3=13。
通过对比发现,在受保护的成员前都多了两个字节,受保护的成员在序列化时规则:
\1. 受Private修饰的私有成员,序列化时: \x00 + [私有成员所在类名] + \x00 [变量名]
\2. 受Protected修饰的成员,序列化时:\x00 + * + \x00 + [变量名]
其中,"\x00"代表ASCII为0的值,即空字节," * " 必不可少。
序列化格式中的字母含义:
a - array b - boolean d - double i - integer o - common object r - reference s - string C - custom object O - class N - null R - pointer reference U - unicode string
反序列化
反序列化的话,就依次根据规则进行反向复原。
这边定义一个字符串,然后使用反序列化函数unserialize进行反序列化处理,最后使用var_dump进行输出:
<?php $ser = 'O:6:"object":3:{s:1:"a";i:1;s:4:"team";s:6:"hahaha";}'; $ser = unserialize($ser); var_dump($ser); ?>
php反序列化漏洞(对象注入)
在反序列化过程中,其功能就类似于创建了一个新的对象(复原一个对象可能更恰当),并赋予其相应的属性值。如果让攻击者操纵任意反序列数据, 那么攻击者就可以实现任意类对象的创建,如果一些类存在一些自动触发的方法(魔术方法),那么就有可能以此为跳板进而攻击系统应用。
挖掘反序列化漏洞的条件是:
代码中有可利用的类,并且类中有wakeup(),sleep(),__destruct()这类特殊条件下可以自己调用的魔术方法。
unserialize()函数的参数可控。
php对象注入示例一:
<?php class A{ var $test = "demo"; function __destruct(){ @eval($this->test); } } $test = $_POST['test']; $len = strlen($test)+1; $p = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 构造序列化对象 $test_unser = unserialize($p); // 反序列化同时触发_destruct函数 ?>
如上代码,最终的目的是通过调用destruct()这个析构函数,将恶意的payload注入,导致代码执行。根据上面的魔术方法的介绍,当程序跑到unserialize()反序列化的时候,会触发destruct()方法,同时也可以触发__wakeup()方法。但是如果想注入恶意payload,还需要对$test的值进行覆盖,题目中已经给出了序列化链,很明显是对类A的$test变量进行覆盖。
可以看到当我们传入的参数为 phpinfo()
这样的话在调用__destruct方法执行eval之前就把变量$test的值替换成恶意payload。
php对象注入示例二:
这是来自bugku的一道题。题目地址
index.php
<?php $txt = $_GET["txt"]; $file = $_GET["file"]; $password = $_GET["password"]; if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")) { echo "hello friend!<br>"; if(preg_match("/flag/",$file)) { echo "不能现在就给你flag哦"; exit(); } else { include($file); $password = unserialize($password); echo $password; } } else { echo "you are not the number of bugku ! "; } ?>
hint.php
<?php class Flag{//flag.php public $file; public function __tostring(){ if(isset($this->file)){ echo file_get_contents($this->file); echo "<br>"; return ("good"); } } } ?>
hint.php文件中使用了魔术方法tostring()方法,当一个对象被当作一个字符串被调用时即可触发,方法的主要作用是读取并打印传进来的$file,估计是通过反序列化漏洞来读取flag.php的内容。追踪以下调用链,在index.php文件中发现使用echo将反序列化的对象当作字符串打印,此处就会触发tostring()方法,并且unserialize()内的变量可控,满足反序列化漏洞条件。直接构造payload:(关于使用php://filter进行任意文件的读取,参照p牛:《谈一谈php://filter的妙用》)
php对象注入示例三:
<?php class test{ var $test = '123'; function __wakeup(){ $fp = fopen("flag.php","w"); fwrite($fp,$this->test); fclose($fp); } } $a = $_GET['id']; print_r($a); echo "</br>"; $a_unser = unserialize($a); require "flag.php"; ?>
如上代码主要通过调用魔术方法wakeup将$test的值写入flag.php文件中,当调用unserialize()反序列化操作时会触发wakeup魔术方法,接下来就需要构造传进去的payload,先生成payload:
<?php class test{ var $test = "<?php phpinfo(); ?>"; } $test = new test(); echo serialize($test); ?>
传入payload:
在执行unserialize()方法时会触发__wakeup()方法执行,将传入的字符串反序列化后,会替换掉test类里面$test变量的值,将php探针写入flag.php文件中,并通过下面的require引用,导致命令执行。