讀懂PHP反序列化

主要目的

借著本次機(jī)會(huì)系統(tǒng)的學(xué)習(xí)反序列化漏洞,和PHP的一些語句的具體用法

問題原因:

漏洞的根源在于unserialize()函數(shù)的參數(shù)可控。如果反序列化對(duì)象中存在魔術(shù)方法,而且魔術(shù)方法中的代碼或變量用戶可控,就可能產(chǎn)生反序列化漏洞,根據(jù)反序列化后不同的代碼可以導(dǎo)致各種攻擊,如代碼注入、SQL注入、目錄遍歷等等。
魔術(shù)方法:PHP的類中可能會(huì)包含一些特殊的函數(shù)叫魔術(shù)函數(shù),魔術(shù)函數(shù)命名是以符號(hào)__開頭的; 有以下的魔術(shù)方法: __construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set(), _state(), __clone(), __debugInfo()

__sleep 和__wakeup

序列化serialize可以把變量包括對(duì)象,轉(zhuǎn)化成連續(xù)bytes數(shù)據(jù). 你可以將序列化后的變量存在一個(gè)文件里或在網(wǎng)絡(luò)上傳輸. 然后再反序列化還原為原來的數(shù)據(jù). 你在反序列化類的對(duì)象之前定義的類,PHP可以成功地存儲(chǔ)其對(duì)象的屬性和方法. 有時(shí)你可能需要一個(gè)對(duì)象在反序列化后立即執(zhí)行. 為了這樣的目的,PHP會(huì)自動(dòng)尋找__sleep和__wakeup方法.

當(dāng)一個(gè)對(duì)象被序列化,PHP會(huì)調(diào)用__sleep方法(如果存在的話). 在序列行化一個(gè)對(duì)象后,PHP 會(huì)調(diào)用__wakeup方法. 這兩個(gè)方法都不接受參數(shù). __sleep方法必須返回一個(gè)數(shù)組,包含需要序列化化的屬性.PHP會(huì)拋棄其它屬性的值. 如果沒有__sleep方法,PHP將保存所有屬性.

在程序執(zhí)行前,serialize() 函數(shù)會(huì)首先檢查是否存在一個(gè)魔術(shù)方法 __sleep.如果存在,__sleep()方法會(huì)先被調(diào)用, 然后才執(zhí)行串行化(序列化)操作。這個(gè)功能可以用于清理對(duì)象,并返回一個(gè)包含對(duì)象中所有變量名稱的數(shù)組。如果該方法不返回任何內(nèi)容,則NULL被序列化,導(dǎo)致 一個(gè)E_NOTICE錯(cuò)誤。與之相反,unserialize()會(huì)檢查是否存在一個(gè)__wakeup方法。如果存在,則會(huì)先調(diào)用 __wakeup方法,預(yù)先準(zhǔn)備對(duì)象數(shù)據(jù)。

__sleep() 方法常用于提交未提交的數(shù)據(jù),或類似的清理操作。同時(shí),如果有一些很大的對(duì)象,但不需要全部保存,這個(gè)功能就很好用。
與之相反,unserialize() 會(huì)檢查是否存在一個(gè) __wakeup() 方法。如果存在,則會(huì)先調(diào)用 __wakeup 方法,預(yù)先準(zhǔn)備對(duì)象需要的資源。
__wakeup() 經(jīng)常用在反序列化操作中,例如重新建立數(shù)據(jù)庫(kù)連接,或執(zhí)行其它初始化操作。

<?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方法來序列化一個(gè)對(duì)象. Id屬性是一個(gè)不打算保留在對(duì)象中的臨時(shí)屬性. __sleep方法保證在串行化的對(duì)象中不包含id屬性. 當(dāng)反串行化一個(gè)User對(duì)象,__wakeup方法建立id屬性的新值. 這個(gè)例子被設(shè)計(jì)成自我保持. 在實(shí)際開發(fā)中,你可能發(fā)現(xiàn)包含資源(如圖像或數(shù)據(jù)流)的對(duì)象需要這些方法。

<?php
class user {
    public $name;
    public $id;
     
    function __construct() {    // 給id成員賦一個(gè)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串行化對(duì)象u,此處不串行化id屬性,id值被拋棄
$u2 = unserialize($s); //unserialize反串行化,id值被重新賦值
//對(duì)象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前調(diào)用 用于刪選需要被序列化存儲(chǔ)的成員變量
     * @return array [description]
     */
    public function __sleep()
    {
        echo __METHOD__ . PHP_EOL;
        //序列化時(shí)只會(huì)存儲(chǔ) name age sex, info 不會(huì)被序列化
        return ['name', 'age', 'sex'];
    }
    /**
     * unserialize前調(diào)用 用于預(yù)先準(zhǔn)備對(duì)象資源
     */
    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' );
//構(gòu)造函數(shù)組裝的 $info
$boy->getInfo();
echo "<hr>";
//序列化時(shí)并不會(huì)存儲(chǔ) $info 屬性
$temp = serialize($boy);
echo $temp . PHP_EOL;
echo "<hr>";
//反序列化時(shí)會(huì)調(diào)用 __wakeup() 函數(shù)
$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);

輸出的結(jié)果是O:7:”myClass”:1:{s:9:”myContent”;N;}

它竟然把一個(gè)類的給序列化了,也就是把一個(gè)類轉(zhuǎn)換成了一個(gè)字符串,可以傳輸或者保存下來。

下面我修改一下上面的代碼

class myClass{
    public $myContent;
    function __construct($string){
        $this->myContent = $string;
    }
}
$content = new myClass('my china');
echo serialize($content);

輸出的結(jié)果是O:7:”myClass”:1:{s:9:”myContent”;s:8:”my china”;}

序列化后也對(duì)應(yīng)了相應(yīng)的值,但是現(xiàn)在有個(gè)問題,比如我這個(gè)變量是個(gè)秘密呢?而且我又得把這個(gè)類序列化傳給別的地方呢?
看下面的代碼

class myClass{
    public $myContent;
    function __construct($string){
        $this->myContent = $string;
    }
}
$content = new myClass('我愛宋祖英,這是一個(gè)秘密');
echo serialize($content);

輸出的結(jié)果是O:7:”myClass”:1:{s:9:”myContent”;s:36:”我愛宋祖英,這是一個(gè)秘密”;}

我的秘密序列化后還是存在的,可是我不想我的心里話被別人看到。這個(gè)時(shí)候PHP很貼心,她知道你的問題,所以設(shè)置了魔術(shù)方法。

__sleep() 就表示當(dāng)你執(zhí)行serialize()這個(gè)序列化函數(shù)之前時(shí)的事情,就像一個(gè)回調(diào)函數(shù),所以在這個(gè)回調(diào)函數(shù)里面我們就可以做點(diǎn)事情,來隱藏我的秘密。

class myClass{
    public $myContent;
    function __construct($string){
        $this->myContent = $string;
    }
    public function __sleep(){
        $this->myContent = '這是我的秘密';
        return array('myContent');

    }
}
$content = new myClass('我愛宋祖英,這是一個(gè)秘密');
echo serialize($content);

輸出的結(jié)果是:O:7:”myClass”:1:{s:9:”myContent”;s:18:”這是我的秘密”;}

我的心里話被加密了,這個(gè)就是__sleep()的作用。至于__wakeup()和__sleep()大同小異,只不過是反序列化之前進(jìn)行的回調(diào)函數(shù)。我不詳細(xì)說了,大家看下下面的代碼就明白了。

class myClass{
    public $myContent;
    function __construct($string){
        $this->myContent = $string;
    }
    public function __sleep(){
        $this->myContent = '這是我的秘密';
        return array('myContent');

    }
    public function __wakeup(){
        $this->myContent = '我的秘密又回來了';
        //反序列化就不用返回?cái)?shù)組了,就是對(duì)應(yīng)的字符串的解密,字符串已經(jīng)有了就不用其他的了
    }
}
$content = new myClass('我愛宋祖英,這是一個(gè)秘密');
print_r(unserialize(serialize($content)));

輸出的內(nèi)容為:myClass Object ( [myContent] => 我的秘密有回來了 )

__toString

__toString() 方法用于定義一個(gè)類被當(dāng)成字符串時(shí)該如何處理。

__toString是在直接輸出對(duì)象引用時(shí)自動(dòng)調(diào)用的, 前面我們講過對(duì)象引用是一個(gè)指針,比如說:“p=new Person()“中,p就是一個(gè)引用,我們不能使用echo 直接輸出$p, 這樣會(huì)輸出”Catchable fatal error: Object of class Person could not be converted to string“這樣的錯(cuò)誤,如果你在類里面定義了“__toString()”方法,在直接輸出對(duì)象引用的時(shí)候,就不會(huì)產(chǎn)生錯(cuò)誤,而是自動(dòng)調(diào)用了”__toString()”方法, 輸出“__toString()”方法中返回的字符,所以“__toString()”方法一定要有個(gè)返回值(return 語句).

<?php 
class Person{ 
    private $name = ""; 
    function __construct($name = ""){ 
        $this->name = $name; 
    } 
    function say(){ 

        echo "Hello,".$this->name."!<br/>";   
    } 
    function __tostring(){//在類中定義一個(gè)__toString方法 

        return  "Hello,".$this->name."!<br/>";     
    } 
} 
$WBlog = new Person('WBlog'); 

echo $WBlog;//直接輸出對(duì)象引用則自動(dòng)調(diào)用了對(duì)象中的__toString()方法 

$WBlog->say();//試比較一下和上面的自動(dòng)調(diào)用有什么不同 
?>

程序輸出:
Hello,WBlog!
Hello,WBlog!
如果不定義“__tostring()”方法會(huì)怎么樣呢?例如在上面代碼的基礎(chǔ)上,把“ __tostring()”方法屏蔽掉,再看一下程序輸出結(jié)果:
Catchable fatal error: Object of class Person could not be converted to string
由此可知如果在類中沒有定義“__tostring()”方法,則直接輸出以象的引用時(shí)就會(huì)產(chǎn)生誤法錯(cuò)誤,另外__tostring()方法體中需要有一個(gè)返回值。

__invoke

當(dāng)嘗試以調(diào)用函數(shù)的方式調(diào)用一個(gè)對(duì)象時(shí),__invoke() 方法會(huì)被自動(dòng)調(diào)用。(本特性只在 PHP 5.3.0 及以上版本有效。)

<?php 
class Demo{ 
        public function __invoke(){ 
                echo "測(cè)試"; 
        } 
} 
$demo = new Demo; 
$demo(); 
?> 

這樣的話,直接用對(duì)象名就當(dāng)函數(shù)使用了,調(diào)用的是_invoke的方法;
輸出
測(cè)試

__construct 構(gòu)造方法 __destruct 析構(gòu)方法

__construct()在每次創(chuàng)建新對(duì)象時(shí)先調(diào)用此方法
__destruct() 允許在銷毀一個(gè)類之前執(zhí)行執(zhí)行析構(gòu)方法。

<?php
 
/**
 * 清晰的認(rèn)識(shí)__construct() __destruct()
 */
class Example {
 
    public static $link;
    //在類實(shí)例化的時(shí)候自動(dòng)加載__construct這個(gè)方法
    public function __construct($localhost, $username, $password, $db) {
        self::$link = mysql_connect($localhost, $username, $password);
        if (mysql_errno()) {
            die('錯(cuò)誤:' . mysql_error());
        }
        mysql_set_charset('utf8');
        mysql_select_db($db);
    }
 
    /**
     * 通過__construct鏈接好數(shù)據(jù)庫(kù)然后執(zhí)行sql語句......
     */
     
    //當(dāng)類需要被刪除或者銷毀這個(gè)類的時(shí)候自動(dòng)加載__destruct這個(gè)方法
    public function __destruct() {
        echo '<pre>';
        var_dump(self::$link);
        mysql_close(self::$link);
        var_dump(self::$link);
    }
 
}
 
$mysql = new Example('localhost', 'root', 'root', 'test');

結(jié)果:

resource(2) of type (mysql link)
resource(2) of type (Unknown)

__set __get

__get()方法:這個(gè)方法用來獲取私有成員屬性值的,有一個(gè)參數(shù),參數(shù)傳入你要獲取的成員屬性的名稱,返回獲取的屬性值。如果成員屬性不封裝成私有的,對(duì)象本身就不會(huì)去自動(dòng)調(diào)用這個(gè)方法。
__set()方法:這個(gè)方法用來為私有成員屬性設(shè)置值的,有兩個(gè)參數(shù),第一個(gè)參數(shù)為你要為設(shè)置值的屬性名,第二個(gè)參數(shù)是要給屬性設(shè)置的值,沒有返回值。(key=>value)
__set() 方法用于設(shè)置私有屬性值:

function __set($property_name, $value)
{ 
    $this->$property_name = $value; 
}

在類里面使用了 __set() 方法后,當(dāng)使用 $p1->name = "張三"; 這樣的方式去設(shè)置對(duì)象私有屬性的值時(shí),就會(huì)自動(dòng)調(diào)用 __set() 方法來設(shè)置私有屬性的值。

__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()方法用來設(shè)置私有屬性
    function __set($property_name, $value) { 
        echo "在直接設(shè)置私有屬性值的時(shí)候,自動(dòng)調(diào)用了這個(gè) __set() 方法為私有屬性賦值<br />";
        $this->$property_name = $value; 
    }
    //__get()方法用來獲取私有屬性
    function __get($property_name) {  
        echo "在直接獲取私有屬性值的時(shí)候,自動(dòng)調(diào)用了這個(gè) __get() 方法<br />";
        return isset($this->$property_name) ? $this->$property_name : null;
    }
}

$p1=new Person();
//直接為私有屬性賦值的操作, 會(huì)自動(dòng)調(diào)用 __set() 方法進(jìn)行賦值
$p1->name = "張三";
//直接獲取私有屬性的值, 會(huì)自動(dòng)調(diào)用 __get() 方法,返回成員屬性的值
echo "我的名字叫:".$p1->name;    
?>

運(yùn)行該例子,輸出:

在直接設(shè)置私有屬性值的時(shí)候,自動(dòng)調(diào)用了這個(gè) __set() 方法為私有屬性賦值
在直接獲取私有屬性值的時(shí)候,自動(dòng)調(diào)用了這個(gè) __get() 方法

我的名字叫:張三

__isset() __unset()

__isset()
__isset() 方法用于檢測(cè)私有屬性值是否被設(shè)定。

如果對(duì)象里面成員是公有的,可以直接使用 isset() 函數(shù)。如果是私有的成員屬性,那就需要在類里面加上一個(gè) __isset() 方法:

private function __isset($property_name)
{
    return isset($this->$property_name);
}

這樣當(dāng)在類外部使用 isset() 函數(shù)來測(cè)定對(duì)象里面的私有成員是否被設(shè)定時(shí),就會(huì)自動(dòng)調(diào)用 __isset() 方法來檢測(cè)。

__unset()
__unset() 方法用于刪除私有屬性。

同 isset() 函數(shù)一樣,unset() 函數(shù)只能刪除對(duì)象的公有成員屬性,當(dāng)要?jiǎng)h除對(duì)象內(nèi)部的私有成員屬性時(shí),需要使用__unset() 方法:

private function __unset($property_name)
{
    unset($this->$property_name);
}

### __call __callStatic  

1.__call()方法。當(dāng)調(diào)用一個(gè)沒有在類中聲明的方法時(shí),可以調(diào)用__call()方法代替聲明一個(gè)方法。接受方法名和數(shù)組作為參數(shù)。

代碼實(shí)例:

<?php  
class test{  
//魔術(shù)方法__call  
/* 
$method 獲得方法名 
$arg 獲得方法的參數(shù)集合 
*/  
public function __call($method,$arg){  
    echo '你想調(diào)用我不存在的方法',$method,'方法<br/>';  
    echo '還傳了一個(gè)參數(shù)<br/>';  
    echo print_r($arg),'<br/>';  
  }   
$list=new test();  
$list->say(1,2,3);  
  
?> 

執(zhí)行結(jié)果:
你想調(diào)用我不存在的方法say方法
還傳了一個(gè)參數(shù)
Array ( [0] => 1 [1] => 2 [2] => 3 )

2.__callStatic()方法。從PHP5.3開始出現(xiàn)此方法,當(dāng)創(chuàng)建一個(gè)靜態(tài)方法以調(diào)用該類中不存在的一個(gè)方法時(shí)使用此函數(shù)。與__call()方法相同,接受方法名和數(shù)組作為參數(shù)。
代碼實(shí)例:

<?php  
class test{  
//魔術(shù)方法__callStatic 
/* 
$method 獲得方法名 
$arg 獲得方法的參數(shù)集合 
*/  
//魔術(shù)方法__callStatic  
public static function __callStatic($method,$arg){  
  
    echo '你想調(diào)用我不存在的',$method,'靜態(tài)方法<br/>';  
    echo '還傳了一個(gè)參數(shù)<br/>';  
    echo print_r($arg),'<br/>';  
  }  
  
}  
test::cry('痛哭','鬼哭','號(hào)哭');  
 
?> 

執(zhí)行結(jié)果:
你想調(diào)用我不存在的cry靜態(tài)方法
還傳了一個(gè)參數(shù)
Array ( [0] => 痛哭 [1] => 鬼哭 [2] => 號(hào)哭 )

參考:
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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容