读懂PHP反序列化
主要目的
借着本次机会系统的学习反序列化漏洞,和PHP的一些语句的具体用法
问题原因:
漏洞的根源在于unserialize()函数的参数可控。如果反序列化对象中存在魔术方法,而且魔术方法中的代码或变量用户可控,就可能产生反序列化漏洞,根据反序列化后不同的代码可以导致各种攻击,如代码注入、SQL注入、目录遍历等等。
魔术方法:PHP的类中可能会包含一些特殊的函数叫魔术函数,魔术函数命名是以符号__开头的; 有以下的魔术方法: __construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set(), _state(), __clone(), __debugInfo()
__sleep 和__wakeup
序列化serialize可以把变量包括对象,转化成连续bytes数据. 你可以将序列化后的变量存在一个文件里或在网络上传输. 然后再反序列化还原为原来的数据. 你在反序列化类的对象之前定义的类,PHP可以成功地存储其对象的属性和方法. 有时你可能需要一个对象在反序列化后立即执行. 为了这样的目的,PHP会自动寻找__sleep和__wakeup方法.
当一个对象被序列化,PHP会调用__sleep方法(如果存在的话). 在序列行化一个对象后,PHP 会调用__wakeup方法. 这两个方法都不接受参数. __sleep方法必须返回一个数组,包含需要序列化化的属性.PHP会抛弃其它属性的值. 如果没有__sleep方法,PHP将保存所有属性.
在程序执行前,serialize() 函数会首先检查是否存在一个魔术方法 __sleep.如果存在,__sleep()方法会先被调用, 然后才执行串行化(序列化)操作。这个功能可以用于清理对象,并返回一个包含对象中所有变量名称的数组。如果该方法不返回任何内容,则NULL被序列化,导致 一个E_NOTICE错误。与之相反,unserialize()会检查是否存在一个__wakeup方法。如果存在,则会先调用 __wakeup方法,预先准备对象数据。
__sleep() 方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。
与之相反,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。
__wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。
<?php
class Connection
{
protected $link;
private $server, $username, $password, $db;
public function __construct($server, $username, $password, $db)
{
$this->server = $server;
$this->username = $username;
$this->password = $password;
$this->db = $db;
$this->connect();
}
private function connect()
{
$this->link = mysql_connect($this->server, $this->username, $this->password);
mysql_select_db($this->db, $this->link);
}
public function __sleep()
{
return array('server', 'username', 'password', 'db');
}
public function __wakeup()
{
$this->connect();
}
}
?>
下面例子显示了如何用__sleep和 __wakeup方法来序列化一个对象. Id属性是一个不打算保留在对象中的临时属性. __sleep方法保证在串行化的对象中不包含id属性. 当反串行化一个User对象,__wakeup方法建立id属性的新值. 这个例子被设计成自我保持. 在实际开发中,你可能发现包含资源(如图像或数据流)的对象需要这些方法。
<?php
class user {
public $name;
public $id;
function __construct() { // 给id成员赋一个uniq id
$this->id = uniqid();
}
function __sleep() { //此处不串行化id成员
return(array('name'));
}
function __wakeup() {
$this->id = uniqid();
}
}
$u = new user();
$u->name = "Leo";
$s = serialize($u); //serialize串行化对象u,此处不串行化id属性,id值被抛弃
$u2 = unserialize($s); //unserialize反串行化,id值被重新赋值
//对象u和u2有不同的id赋值
print_r($u);
print_r($u2);
?>
user Object
(
[name] => Leo
[id] => 5d0d9c66b9a03
)
user Object
(
[name] => Leo
[id] => 5d0d9c66b9a45
)
<?php
class Person
{
private $name, $age, $sex, $info;
public function __construct( $name, $age, $sex )
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
$this->info = sprintf("prepared by construct magic functionname: %s age: %d sex: %s",
$this->name, $this->age, $this->sex);
}
public function getInfo()
{
echo $this->info . PHP_EOL;
}
/**
* serialize前调用 用于删选需要被序列化存储的成员变量
* @return array [description]
*/
public function __sleep()
{
echo __METHOD__ . PHP_EOL;
//序列化时只会存储 name age sex, info 不会被序列化
return ['name', 'age', 'sex'];
}
/**
* unserialize前调用 用于预先准备对象资源
*/
public function __wakeup()
{
echo __METHOD__ . PHP_EOL;
$this->info = sprintf("prepared by wakeup magic function name: %s age: %d sex: %s",
$this->name, $this->age, $this->sex);
}
}
$boy = new Person( 'sallency', 25, 'male' );
//构造函数组装的 $info
$boy->getInfo();
echo "<hr>";
//序列化时并不会存储 $info 属性
$temp = serialize($boy);
echo $temp . PHP_EOL;
echo "<hr>";
//反序列化时会调用 __wakeup() 函数
$boy = unserialize($temp);
//__wakeup() 组装的 $info
$boy->getInfo();
echo "<hr>";
?>
prepared by construct magic functionname: sallency age: 25 sex: male
<hr>Person::__sleep
O:6:"Person":3:{s:12:"Personname";s:8:"sallency";s:11:"Personage";i:25;s:11:"Personsex";s:4:"male";}
<hr>Person::__wakeup
prepared by wakeup magic function name: sallency age: 25 sex: male
<hr>
先写一段代码
class myClass{
public $myContent;
function outMycontent(){
//dosomething
}
}
$content = new myClass();
echo serialize($content);
输出的结果是O:7:”myClass”:1:{s:9:”myContent”;N;}
它竟然把一个类的给序列化了,也就是把一个类转换成了一个字符串,可以传输或者保存下来。
下面我修改一下上面的代码
class myClass{
public $myContent;
function __construct($string){
$this->myContent = $string;
}
}
$content = new myClass('my china');
echo serialize($content);
输出的结果是O:7:”myClass”:1:{s:9:”myContent”;s:8:”my china”;}
序列化后也对应了相应的值,但是现在有个问题,比如我这个变量是个秘密呢?而且我又得把这个类序列化传给别的地方呢?
看下面的代码
class myClass{
public $myContent;
function __construct($string){
$this->myContent = $string;
}
}
$content = new myClass('我爱宋祖英,这是一个秘密');
echo serialize($content);
输出的结果是O:7:”myClass”:1:{s:9:”myContent”;s:36:”我爱宋祖英,这是一个秘密”;}
我的秘密序列化后还是存在的,可是我不想我的心里话被别人看到。这个时候PHP很贴心,她知道你的问题,所以设置了魔术方法。
__sleep() 就表示当你执行serialize()这个序列化函数之前时的事情,就像一个回调函数,所以在这个回调函数里面我们就可以做点事情,来隐藏我的秘密。
class myClass{
public $myContent;
function __construct($string){
$this->myContent = $string;
}
public function __sleep(){
$this->myContent = '这是我的秘密';
return array('myContent');
}
}
$content = new myClass('我爱宋祖英,这是一个秘密');
echo serialize($content);
输出的结果是:O:7:”myClass”:1:{s:9:”myContent”;s:18:”这是我的秘密”;}
我的心里话被加密了,这个就是__sleep()的作用。至于__wakeup()和__sleep()大同小异,只不过是反序列化之前进行的回调函数。我不详细说了,大家看下下面的代码就明白了。
class myClass{
public $myContent;
function __construct($string){
$this->myContent = $string;
}
public function __sleep(){
$this->myContent = '这是我的秘密';
return array('myContent');
}
public function __wakeup(){
$this->myContent = '我的秘密又回来了';
//反序列化就不用返回数组了,就是对应的字符串的解密,字符串已经有了就不用其他的了
}
}
$content = new myClass('我爱宋祖英,这是一个秘密');
print_r(unserialize(serialize($content)));
输出的内容为:myClass Object ( [myContent] => 我的秘密有回来了 )
__toString
__toString() 方法用于定义一个类被当成字符串时该如何处理。
__toString是在直接输出对象引用时自动调用的, 前面我们讲过对象引用是一个指针,比如说:“p就是一个引用,我们不能使用echo 直接输出$p, 这样会输出”Catchable fatal error: Object of class Person could not be converted to string“这样的错误,如果你在类里面定义了“__toString()”方法,在直接输出对象引用的时候,就不会产生错误,而是自动调用了”__toString()”方法, 输出“__toString()”方法中返回的字符,所以“__toString()”方法一定要有个返回值(return 语句).
<?php
class Person{
private $name = "";
function __construct($name = ""){
$this->name = $name;
}
function say(){
echo "Hello,".$this->name."!<br/>";
}
function __tostring(){//在类中定义一个__toString方法
return "Hello,".$this->name."!<br/>";
}
}
$WBlog = new Person('WBlog');
echo $WBlog;//直接输出对象引用则自动调用了对象中的__toString()方法
$WBlog->say();//试比较一下和上面的自动调用有什么不同
?>
程序输出:
Hello,WBlog!
Hello,WBlog!
如果不定义“__tostring()”方法会怎么样呢?例如在上面代码的基础上,把“ __tostring()”方法屏蔽掉,再看一下程序输出结果:
Catchable fatal error: Object of class Person could not be converted to string
由此可知如果在类中没有定义“__tostring()”方法,则直接输出以象的引用时就会产生误法错误,另外__tostring()方法体中需要有一个返回值。
__invoke
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。(本特性只在 PHP 5.3.0 及以上版本有效。)
<?php
class Demo{
public function __invoke(){
echo "测试";
}
}
$demo = new Demo;
$demo();
?>
这样的话,直接用对象名就当函数使用了,调用的是_invoke的方法;
输出
测试
__construct 构造方法 __destruct 析构方法
__construct()在每次创建新对象时先调用此方法
__destruct() 允许在销毁一个类之前执行执行析构方法。
<?php
/**
* 清晰的认识__construct() __destruct()
*/
class Example {
public static $link;
//在类实例化的时候自动加载__construct这个方法
public function __construct($localhost, $username, $password, $db) {
self::$link = mysql_connect($localhost, $username, $password);
if (mysql_errno()) {
die('错误:' . mysql_error());
}
mysql_set_charset('utf8');
mysql_select_db($db);
}
/**
* 通过__construct链接好数据库然后执行sql语句......
*/
//当类需要被删除或者销毁这个类的时候自动加载__destruct这个方法
public function __destruct() {
echo '<pre>';
var_dump(self::$link);
mysql_close(self::$link);
var_dump(self::$link);
}
}
$mysql = new Example('localhost', 'root', 'root', 'test');
结果:
resource(2) of type (mysql link)
resource(2) of type (Unknown)
__set __get
__get()方法:这个方法用来获取私有成员属性值的,有一个参数,参数传入你要获取的成员属性的名称,返回获取的属性值。如果成员属性不封装成私有的,对象本身就不会去自动调用这个方法。
__set()方法:这个方法用来为私有成员属性设置值的,有两个参数,第一个参数为你要为设置值的属性名,第二个参数是要给属性设置的值,没有返回值。(key=>value)
__set() 方法用于设置私有属性值:
function __set($property_name, $value)
{
$this->$property_name = $value;
}
在类里面使用了 __set() 方法后,当使用 $p1->name = "张三"; 这样的方式去设置对象私有属性的值时,就会自动调用 __set() 方法来设置私有属性的值。
__get()
__get() 方法用于获取私有属性值:
function __set($property_name, $value)
{
return isset($this->$property_name) ? $this->$property_name : null;
}
例子:
<?php
class Person {
private $name;
private $sex;
private $age;
//__set()方法用来设置私有属性
function __set($property_name, $value) {
echo "在直接设置私有属性值的时候,自动调用了这个 __set() 方法为私有属性赋值<br />";
$this->$property_name = $value;
}
//__get()方法用来获取私有属性
function __get($property_name) {
echo "在直接获取私有属性值的时候,自动调用了这个 __get() 方法<br />";
return isset($this->$property_name) ? $this->$property_name : null;
}
}
$p1=new Person();
//直接为私有属性赋值的操作, 会自动调用 __set() 方法进行赋值
$p1->name = "张三";
//直接获取私有属性的值, 会自动调用 __get() 方法,返回成员属性的值
echo "我的名字叫:".$p1->name;
?>
运行该例子,输出:
在直接设置私有属性值的时候,自动调用了这个 __set() 方法为私有属性赋值
在直接获取私有属性值的时候,自动调用了这个 __get() 方法
我的名字叫:张三
__isset() __unset()
__isset()
__isset() 方法用于检测私有属性值是否被设定。
如果对象里面成员是公有的,可以直接使用 isset() 函数。如果是私有的成员属性,那就需要在类里面加上一个 __isset() 方法:
private function __isset($property_name)
{
return isset($this->$property_name);
}
这样当在类外部使用 isset() 函数来测定对象里面的私有成员是否被设定时,就会自动调用 __isset() 方法来检测。
__unset()
__unset() 方法用于删除私有属性。
同 isset() 函数一样,unset() 函数只能删除对象的公有成员属性,当要删除对象内部的私有成员属性时,需要使用__unset() 方法:
private function __unset($property_name)
{
unset($this->$property_name);
}
### __call __callStatic
1.__call()方法。当调用一个没有在类中声明的方法时,可以调用__call()方法代替声明一个方法。接受方法名和数组作为参数。
代码实例:
<?php
class test{
//魔术方法__call
/*
$method 获得方法名
$arg 获得方法的参数集合
*/
public function __call($method,$arg){
echo '你想调用我不存在的方法',$method,'方法<br/>';
echo '还传了一个参数<br/>';
echo print_r($arg),'<br/>';
}
$list=new test();
$list->say(1,2,3);
?>
执行结果:
你想调用我不存在的方法say方法
还传了一个参数
Array ( [0] => 1 [1] => 2 [2] => 3 )
2.__callStatic()方法。从PHP5.3开始出现此方法,当创建一个静态方法以调用该类中不存在的一个方法时使用此函数。与__call()方法相同,接受方法名和数组作为参数。
代码实例:
<?php
class test{
//魔术方法__callStatic
/*
$method 获得方法名
$arg 获得方法的参数集合
*/
//魔术方法__callStatic
public static function __callStatic($method,$arg){
echo '你想调用我不存在的',$method,'静态方法<br/>';
echo '还传了一个参数<br/>';
echo print_r($arg),'<br/>';
}
}
test::cry('痛哭','鬼哭','号哭');
?>
执行结果:
你想调用我不存在的cry静态方法
还传了一个参数
Array ( [0] => 痛哭 [1] => 鬼哭 [2] => 号哭 )
参考:
https://www.cnblogs.com/uduemc/p/4122156.html
https://blog.csdn.net/guiyecheng/article/details/60590646
https://blog.csdn.net/wong_gilbert/article/details/76679108
http://www.5idev.com/p-php_member_overloading.shtml
https://blog.csdn.net/sunyinggang/article/details/78906048