前言
最近的CTF比賽中,感覺PHP序列化與反序列化漏洞中的POP鏈的身影越越多了,特此,寫一篇關於POP鏈構造的小文,內容不深,更多的是想提供給想要入坑的小伙伴們一點借鑒(厲害的大牛實在太多),所以,本文的自我定位並不高,跟多的談談自己的一些理解,如有不恰當的地方,懇請指點,但求共同進步。
引入
我們知道,在PHP中可以定義“類”,“類”里面可以定義很多變量和類方法,當我們實例化一個類時,一些類方法可以手工調用,一些類方法可以自動調用(此時,需要滿足某種觸發條件),這種擁有自動調用能力的方法被稱為魔術方法。序列化一個“類”的過程形象點講就是將“類”變為字符串的過程,只不過這個字符串比較特殊,保存了類的變量的某些屬性特征(類名,變量名,變量值,變量權限,類型,變量大小),但省略了類方法的保存。
序列化與反序列化
簡單提一下,序列化一個類的過程:
可以觀察到:"類名,變量名,變量值,變量權限,類型,變量大小" 這些都被保存在了字符串中,但唯獨沒有將方法保存在這個字符串中。
而反序列化就是一個將字符串還原的過程,在本例中,這個特殊的字符串將被最終還原為原本的person 類
:
可以發現,反序列化形成的person類
的基本狀態信息與原person類
基本一致(變個樣子是,無所謂,描述信息不變就行)
序列化與反序列化漏洞
承接上面的例子,可以有一個這樣的猜測,如果我們將序列化的字符串按格式要求隨意更改,是不是可以控制intro()方法的打印信息?答案是肯定的。事實上,序列化與反序列化漏洞也正是由此而來。(簡單提一下)
假設有這樣一個環境:
存在url:
127.0.0.1/ser.php
其對應的后台代碼如下:
<?php class person { public $name="echo 'I am isee'"; public function __wakeup(){ eval ("$this->name;"); } } if (isset($_GET['mid'])){ $mid=$_GET['mid']; unserialize("$mid"); } else{ echo "<h1>hello!!!<h1/>"; }
這段代碼定義了一個person類
,person類
里存在一個敏感函數eval()
可以執行任意代碼,當反序列化這個**person類
形成的特殊字符串成功時會自動觸發__wakeup()
魔術方法,從而執行eval()
函數(本例中,正常情況下,eval()
觸發會將$name的值當PHP代碼解析,即輸出I am isee
** ),訪問該頁面,不傳遞參數時返回一個**hello!!!
信息
如果帶上?mid
參數服務器后端對應的PHP代碼將會反序列化這個用戶傳參,此時如果我們在本地正常序列化這個**pesong類
並拼接到?mid
的結果是這樣的
本地正常序列化**person類
:
<?php class person { public $name = "echo 'I am isee'"; public function __wakeup() { eval ("$this->name;"); } } $person1=new person(); $mid=serialize($person1); var_dump($mid); //序列化后的字符串 //O:6:"person":1:{s:4:"name";s:16:"echo 'I am isee'";}
url拼接mid
參數后訪問服務器:
可以清楚的看到,傳遞序列化后的字符串成功的被后台的unserialize()
還原成了原本的person類
,從而觸發了__wakeup
魔術方法,最終eval()
成功的將$name
的值 echo 'I am isee'
將PHP代碼解析,輸出了I am isee
字樣
如果我們自定義這個序列化后的特殊字符串,會怎樣?
可以看到,ping命令被成功的執行,這也說明,其他的任意命令也可以執行了。
因此,簡單小結一下PHP序列化與反序列化漏洞的成因:
序列化的字符串可以保存類的基本信息,反序列化的過程可以將這個特殊的字符串重新還原成類,雖然在整個序列化與反序列化的過程中我們無法控制類方法的改變(這個主要指后台的自定義函數),但是我們卻可以通過復寫變量並借用類中自定義好的方法(服務器上的)或魔術方法(服務器存在的或本地自定義的),並借用敏感函數來達到惡意效果。
關鍵點就在於PHP序列化與反序列化的過程用戶可控。
POP鏈
個人認為,序列化與反序列化漏洞的精髓在與敏感函數利用與類重構。POP鏈也是序列化與反序列化漏洞利用方式的一種,兩者都要利用到PHP中的魔發函數,自我感覺,區別就在於一個”短“點兒,一個”長“兒。
比如說,下面這個例子
<meta charset="utf-8"> <?php //hint is in hint.php error_reporting(1); class Start { public $name='guest'; public $flag='syst3m("cat 127.0.0.1/etc/hint");'; public function __construct(){ echo "I think you need /etc/hint . Before this you need to see the source code"; } public function _sayhello(){ echo $this->name; return 'ok'; } public function __wakeup(){ echo "hi"; $this->_sayhello(); } public function __get($cc){ echo "give you flag : ".$this->flag; return ; } } class Info { private $phonenumber=123123; public $promise='I do'; public function __construct(){ $this->promise='I will not !!!!'; return $this->promise; } public function __toString(){ return $this->file['filename']->ffiillee['ffiilleennaammee']; } } class Room { public $filename='/flag'; public $sth_to_set; public $a=''; public function __get($name){ $function = $this->a; return $function(); } public function Get_hint($file){ $hint=base64_encode(file_get_contents($file)); echo $hint; return ; } public function __invoke(){ $content = $this->Get_hint($this->filename); echo $content; } } if(isset($_GET['hello'])){ unserialize($_GET['hello']); }else{ $hi = new Start(); } ?>
我們直接看這里
敏感函數file_get_contents
已經有了,可以讀取服務器上的任意文件(權限允許的話),echo()
將讀取結果輸出到用戶界面,但是,雖然序列化的過程我們是可以控制的,但確實沒有發現一下就能將Room類
利用起來的地方(Room類不像前面的Person類存在直接借用魔術方法調用eval()這樣的利用點。
兩個類的對比:
用戶可控序列化的過程:
此時就需要借用其他類,一步一步調,直到最后調到Room類
。
這個過程需要大量用到PHP中的魔術方法,因此,在開始我們的pop鏈的過程之前,需要先討論一下本例中要用到的魔術方法,其他的,如果讀者有興趣,可以自行了解,此處不再作過多贅述。
魔術方法
我們知道,再PHP中,要使用一個類的方法,首先要實例化這個類,之后再通過這個實例化的類對象去調用類方法(這也是面向對象語言的最大特別之處)
<?php //一個類,類里面定義了兩個變量,和一個intro()方法用作輸出兩變量信息 class person { public $name="isee"; public $age="女"; public function intro(){ echo "打印結果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; } } //實例化person類,並調用intro()方法 $person1=new person(); $person1->intro();
但有些類方法不需要我們手動調用,而是當一些事件或條件觸發時自動調用。這類方法,我們稱之為魔術方法。
再者,提一點,魔術方法里面的內容是可以自定義的。
本次討論如下幾個魔術方法:
__construct()
__destruct()
__sleep()
__wakeup()
__invoke()
__toString()
__get()
__construct()
當實例化一個類時,自動觸發。
<?php class person { public $name="isee"; public $age="女"; public function __construct() { $this->name="eesi"; $this->age="女"; } public function intro(){ echo "打印結果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; } } $person1=new person(); $person1->intro();
__destruct()
銷毀一個類時自動觸發。
<?php class person { public $name="isee"; public $age="女"; public function __construct() { echo '1、__construct()方法已執行!'."\n"; echo '$name,$sex已重置為新值.'."\n"; $this->name="eesi"; $this->age="女"; echo "\n"; } public function intro(){ echo '2、__intro方法已執行'."\n"; echo "打印結果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __destruct() { $this->name="isee"; $this->age="女"; echo '3、__destruct()方法已執行!'."\n"; echo '$name,$sex已重置為默認值.'."\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } } $person1=new person(); $person1->intro();
__sleep()
執行serialize()
前如果存在__sleep()方法,觸發
不序列化時:
<?php class person { public $name="isee"; public $age="女"; public function intro(){ echo '1、__intro方法已執行'."\n"; echo "打印結果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __sleep() { echo '2、__sleep()方法已執行!'."\n"; echo '$name,$sex已重置為新值.'."\n"; $this->name="eesi"; $this->age="女"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; return array('name','age'); } } $person1=new person(); $person1->intro();
序列化時:
<?php class person { public $name="isee"; public $age="女"; public function intro(){ echo '1、__intro方法已執行'."\n"; echo "打印結果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __sleep() { echo '2、__sleep()方法已執行!'."\n"; echo '$name,$sex已重置為新值.'."\n"; $this->name="eesi"; $this->age="女"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; return array('name','age'); } } $person1=new person(); $person1->intro(); serialize($person1);
__wakeup()
執行unserialize()
前,如果存在__wakeup()方法,觸發,與__sleep()
相對
<?php class person { public $name="isee"; public $age="女"; public function intro(){ echo '1、__intro方法已執行'."\n"; echo "打印結果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __sleep() { echo '2、__sleep()方法已執行!'."\n"; echo '$name,$sex已重置為新值.'."\n"; $this->name="eesi"; $this->age="女"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __wakeup() { $this->name="isee"; $this->age="女"; echo '3、__wakeup()方法已執行!'."\n"; echo '$name,$sex已重置為默認值.'."\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } } $person1=new person(); $person1->intro(); $mid=serialize($person1); unserialize($mid);
__invoke()
類被用作以函數的方式調用時,觸發
__toString()
類被用作字符串輸出時,觸發
<?php class person { public $name="isee"; public $age="女"; public function intro(){ echo '__intro方法已執行'."\n"; echo "打印結果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __invoke() { echo '__invoke()方法已執行!'."\n"; echo '$name,$sex已重置為新值.'."\n"; $this->name="eesi"; $this->age="女"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __toString() { echo '__toString()方法已執行!'."\n"; echo '$name,$sex已重置為新值.'."\n"; $this->name="EESI"; $this->age="男"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; return 'success!!!'."\n\n"; } } //常規類方法調用 $person1=new person(); $person1->intro(); //__invoke()觸發 $person1(); //__toString觸發 echo $person1;
__get()
訪問私有類中的變量時,觸發
<?php class alien{ private $Aname="TOTO"; private $Aage="1000"; public function __get($name) { echo '__get()方法已執行!'."\n"; echo '$name:'."$this->Aname\n"; echo '$age:'."$this->Aage\n"; echo "\n"; } } //__get()方法觸發 $alien=new alien(); echo $alien->Aname;
POP鏈構造
以剛剛上面提到的長代碼為例,接着,開始我們的POP鏈構造過程
首先,簡單的將各個部分拆分,了解一下其大致的功能(這里,添加了注釋)
class Start
class Start { //定義了兩個已經初始化了值的變量,且其權限修飾符為"public" public $name='guest'; public $flag='syst3m("cat 127.0.0.1/etc/hint");'; //__construct()用echo()函數輸出一句話 public function __construct(){ echo "I think you need /etc/hint . Before this you need to see the source code"; } //_sayhello用echo()函數輸出$name變量的內容 public function _sayhello(){ echo $this->name; return 'ok'; } //__wakeup()用來調用_sayhello() public function __wakeup(){ echo "hi"; $this->_sayhello(); } //__get用echo()函數輸出$flag的內容 public function __get($cc){ echo "give you flag : ".$this->flag; return ; } }
class Info
class Info { //定義了兩個已經初始化值的變量,其權限修飾符分別為“private”和“public” private $phonenumber=123123; public $promise='I do'; //調用__construct()函數會返回一個字符串'I do',即$promise的值 public function __construct(){ $this->promise='I will not !!!!'; return $this->promise; } //__tostring()返回一個對象內部的變量,且該對象被存儲在數組file['filename']里 public function __toString(){ return $this->file['filename']->ffiillee['ffiilleennaammee']; } }
class Room
class Room { //定義了三個變量,一個已經初始化值,兩個未初始化值;且均為“public”修飾 public $filename='/flag'; public $sth_to_set; public $a=''; //__get()返回一個函數 public function __get($name){ $function = $this->a; return $function(); } //Get_hit()用echo()輸出$hit的內容,且$hit的值為用file_get_contents()讀取的文本內容 public function Get_hint($file){ $hint=base64_encode(file_get_contents($file)); echo $hint; return ; } //__invoke()會調用Get_hit()函數,且傳遞的參數為$filename public function __invoke(){ $content = $this->Get_hint($this->filename); echo $content; } }
入口
//如果傳遞$hello,則反序列化這個變量,如果未傳遞,則實例化一個類,即,class Start if(isset($_GET['hello'])){ unserialize($_GET['hello']); }else{ $hi = new Start(); } //和class Start
更多時候,會存在非常多的無法利用的類,因此,並不建議通讀每條代碼,更多情況下是POP鏈的起始點與末端敏感函數反推。
分析如下:
直接訪問,后台對應的url返回如下結果:
客戶端由於沒有傳遞參數,服務器端實例化了一個Start 類
,而實例化Start類
觸發__construct
魔術方法,該方法輸出了一句話,至此,結束。
接着分析,很明顯,
class Room
是調用鏈的末端- 敏感函數
file_get_contents()
可以讀取服務器上的文件並通echo()
回顯到服務器; __invoke()
魔術方法可以實現調用Get_hint()
,觸發條件是實例化的class Room
被當作函數調用,且最終會給file_get_contents($file)
,傳遞一個$filename='/flag'
參數;__get()
魔術方法可以實現"實例化的類以函數方式調用"這個條件,只要我們給$a重新賦值一個實例化的
Room對象**,其觸發條件是訪問實例化
Room`的私有變量
目前來看,只需要解決如何調用__get()
即可
接着分析,要用到class info
實現對class Room
里__get()
的調用
__toString()
魔術方法可以實現實例化的類對象調用自身的變量,觸發條件是實例化的class info被當作字符串輸出,file['filename']
未定義,但存在__construct()
魔術方法,我們利用construct()
將file['filename']
賦值一個實例化后的Room對象
,這樣以來,未定義的$ffiillee['ffiilleennaammee']
會被當作class Room
里的私有變量來訪問,從而觸發class Room
里的__get()
方法__construct()
魔術方法可以實現對$promise
的賦值和構造$ffiillee['ffiilleennaammee']
,其觸發條件是class Info
被實例化,我們可以利用__construct()
創造一個$ffiillee['ffiilleennaammee']
,並初始一個class Room`實例化對象
目前來看,只要實現實例化class Info
就行
接着分析
- __construct()魔術方法可以實現對變量的重構,我們可以利用其將$name初始化一個實例化的 class Info對象,從而構造出完整的利用鏈
最終的payload如下:
這里,放一個比賽時,執行成功的結果:
最后base64解一下密就行
小結
感覺,對於POP的鏈構造,魔術函數要玩熟練,且這種漏洞必須通過白盒審計發現,可能在實際中更多的是通過審計流行的框架去挖掘POP鏈(java里也有),然后,如果一些網站使用了這些開源的框架或軟件且二次開發是沒有修補這些漏洞,就有可能被攻擊,並且,現在也存在很多自動化的工具可以直接利用序列化與反序列化漏洞。