读懂PHP反序列化

2019-06-22  本文已影响0人  Visianlee

主要目的

借着本次机会系统的学习反序列化漏洞,和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=new Person()“中,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

上一篇下一篇

猜你喜欢

热点阅读