這一個須要管理員權限的二次SQL注入,利用起來比較雞肋。這裏僅分享一下挖洞時的思路,不包含具體的poc。數據庫
漏洞觸發點在components/com_content/models/articles.php:L458post
$dateFiltering = $this->getState('filter.date_filtering', 'off'); $dateField = $this->getState('filter.date_field', 'a.created'); switch ($dateFiltering) { case 'range': ... $query->where( '(' . $dateField . ' >= ' . $startDateRange . ' AND ' . $dateField . ' <= ' . $endDateRange . ')' ); break; ... }
能夠看到這裏的dateField從getState('filter.date_field')取值以後未經任何過濾就直接拼接到where語句中。經過在這個model的逆向查找,並無找到date_field這個state初始化的地方。咱們只能先經過構造入口,來看看使用這個model的控制器是否對date_field進行了初始化。測試
這個model屬於前臺的com_content組件,可是這個model的入口與同組件下的其餘幾個model不太同樣。其餘的model基本上均可以經過訪問這個組件來訪問,而articles model在本組件中卻沒有使用。fetch
程序中有兩個名爲articles的model,一個在/components,一個在/administrator/components目錄下。我在黑盒測試的時候構造了一個url以下:ui
/index.php?option=com_content&view=articles&layout=modal&tmpl=component
這裏程序中的控制器會根據view和layout的值,將請求直接跳到了administrator目錄下的articles中了。可是根據存在即合理,天生我材必有用
,/components下面有個前臺articles的model,所以程序中必定會有調用這裏的地方。最終找到了幾處調用前臺article的地方,只是有的跟正常調用的不太同樣,這裏是動態調用。寫法大概有以下幾種this
$model = JModelLegacy::getInstance('Articles', 'ContentModel', array('ignore_request' => true)); 也有動態調用model: /libraries/src/MVC/Controller/BaseController.php:createModel($model, ...){ ... JModelLegacy::getInstance($modelName, $classPrefix, $config); ... }
經過訪問url
index.php/blog?252c5a5ef0e3df8493dbe18e7034957e=1
能夠到達漏洞點,可是state咱們控制不了,由於首先在articles model中沒有對date_field作賦值處理,只能寄但願於調用這個model的地方能對date_field賦值。但是經過查看代碼發現,當前的index.php/blog路由背後的com_content組件並無對date_field進行初始化,所以這個組件只能放棄,看看其餘的。3d
終於,在一個module:mod_articles_popular的helper類中找到了有設置date_field的地方,大概以下/modules/mod_articles_popular/helper.phpcode
function getList(&$params){ $model = JModelLegacy::getInstance('Articles', 'ContentModel', array('ignore_request' => true)); //調用articles model ... $date_filtering = $params->get('date_filtering', 'off'); if ($date_filtering !== 'off'){ $model->setState('filter.date_filtering', $date_filtering); $model->setState('filter.date_field', $params->get('date_field', 'a.created')); ... } ... }
能夠看到這裏經過$params->get('date_field')來進行賦值,這裏的param是從modules表中取出的。經過逆向查找發現,/libraries/src/Helper/ModuleHelper.php:getModuleList()方法會從modules表取出module的屬性(包括param),而後在/libraries/src/Document/Renderer/Html/ModulesRenderer.php:render():L45對module進行遍歷並渲染:
foreach (ModuleHelper::getModules($position) as $mod){ $moduleHtml = $renderer->render($mod, $params, $content); ... }
到這裏咱們理一下思路,首先是那個SQL注入點,date_field,須要從param中獲取值,而param又是從module在數據庫中對應的param獲取的。所以咱們這裏能夠考慮一下二次注入。因爲在獲取date_field的值時使用了$this->getState('filter.date_field', 'a.created');
,且默認值爲a.created,所以猜想這個字段在某個部分是能夠修改的。
經過對漏洞點和此module附近的功能與邏輯進行部分了解以後,能夠發如今首頁的module編輯中,能夠直接編輯date_field字段!所以咱們只要點擊保存後抓包修改一下date_field的內容便可將之寫進modules表中!
這裏回到最開始的漏洞點
$dateFiltering = $this->getState('filter.date_filtering', 'off'); $dateField = $this->getState('filter.date_field', 'a.created'); switch ($dateFiltering) { case 'range': $startDateRange = $db->quote($this->getState('filter.start_date_range', $nullDate)); $endDateRange = $db->quote($this->getState('filter.end_date_range', $nullDate)); $query->where( '(' . $dateField . ' >= ' . $startDateRange . ' AND ' . $dateField . ' <= ' . $endDateRange . ')' );//vuln break; ....
能夠看到這裏還有個dateFiltering的限制。其實咱們只要在剛剛的module設置中把date_filtering設置爲range便可。
但是目前爲止這個漏洞還只是盲注而已。。回顯它不香嗎?而且以前拼接的SQL語句執行以後會報錯
Unknown column 'a.hits' in 'order clause'
因爲最後有個order by一個不可控的column名,而且咱們不知道a.hits列名的表叫什麼(每一個Joomla系統的表前綴都默認是隨機的),所以咱們不能很好的union出數據。這裏最簡單的辦法就是看看是否能控制order by的值,好比將之置爲1。查看代碼發現這個order by的確是能夠控制的,就在以前的漏洞點下面幾行
$query->order($this->getState('list.ordering', 'a.ordering') . ' ' . $this->getState('list.direction', 'ASC'));
這裏依舊是經過getState()來進行取值。經過回看模塊mod_articles_popular的賦值點,發現這裏寫死成a.hits了
所以這個module就不太好用了,咱們要考慮另外一個list.ordering可控的module,結果就發現了模塊mod_articles_category,知足咱們的全部幻想:date_field可控、date_filtering可控、list.ordering可控
$ordering = $params->get('article_ordering', 'a.ordering'); switch ($ordering){ ... default: $articles->setState('list.ordering', $ordering); ... } $date_filtering = $params->get('date_filtering', 'off'); if ($date_filtering !== 'off'){ $articles->setState('filter.date_filtering', $date_filtering); $articles->setState('filter.date_field', $params->get('date_field', 'a.created')); ...
同理,登錄後在首頁編輯模塊,而後將相應的值改掉就行了。通過測試發現這裏的list.ordering沒有進行任何的過濾,所以能夠算是一個單獨的order by注入。不過這裏咱們的目標是隻要將order by的列置爲1便可,以便在date_field的位置進行union 注入。
這裏僅放出效果圖,具體的poc就不公開了
這個洞仍是比較雞肋的,1是須要最高的super user權限,2是因爲有token校驗沒法進行csrf,所以把這個漏洞限制成只能有sa帳號才能進行利用。
在最新版的3.9.14中,經過diff發現官方作的修復很簡單,只是在module中存儲時對字段進行了校驗
也就是隻加了個validate="options"
。下面咱們要跟進一下這個字段有何意義,在這以前咱們要先搞懂這個xml文件是啥。
下圖是利用鏈的第一部分:module的目錄結構
helper.php是咱們利用的文件,而這個xml配置文件主要是包含了當前module的一些基本信息,以及一些參數
的信息,包括參數的描述、type、默認值、值範圍等等,這是咱們須要重點關注的。以咱們的poc中的date_filter做爲例子:
能夠看到它的默認值是a.title
,同時下面還有不少option標籤,也就是說這個字段的值只能是option標籤的值的其中一個。
可是說是這麼說,Joomla在此次補丁以前並無進行校驗,也就是前面說的validate="options"
。
下面跟進源碼走一下,下面的代碼是保存param以前的邏輯
/libraries/src/MVC/Controller/FormController.php public function save(...) { .... $data = $this->input->post->get('jform', array(), 'array');//獲取用戶傳參 .... $form = $model->getForm($data, false); .... $validData = $model->validate($form, $data);//校驗 ... if (!$model->save($validData)) {//保存 ..error... } ... return true; }
跟進這裏的validate,底層代碼以下
/libraries/src/MVC/Model/FormModel.php public function validate(...) { ... $data = $form->filter($data); $return = $form->validate($data, $group); ... return $data; }
繼續跟進validate
/libraries/src/Form/Form.php public function validate($data, $group = null) { ... // Create an input registry object from the data to validate. $input = new Registry($data); // Get the fields for which to validate the data. $fields = $this->findFieldsByGroup($group); ... // Validate the fields. foreach ($fields as $field)// { $value = null; $name = (string) $field['name']; // Get the group names as strings for ancestor fields elements. $attrs = $field->xpath('ancestor::fields[@name]/@name'); $groups = array_map('strval', $attrs ? $attrs : array()); $group = implode('.', $groups); // Get the value from the input data. if ($group) { $value = $input->get($group . '.' . $name); } else { $value = $input->get($name); } // Validate the field. $valid = $this->validateField($field, $group, $value, $input);// // Check for an error. if ($valid instanceof \Exception) { $this->errors[] = $valid; $return = false; } } return $return; }
跟進validateField
protected function validateField(\SimpleXMLElement $element, $group = null, $value = null, Registry $input = null) { ... // Get the field validation rule. if ($type = (string) $element['validate'])//根據xml中的每一個field節點的"validate"屬性作校驗 { // Load the JFormRule object for the field. $rule = $this->loadRuleType($type);//若是$type是options,則$rule爲類"Joomla\\CMS\\Form\\Rule\\OptionsRule"的實例化 ... // Run the field validation rule test. $valid = $rule->test($element, $value, $group, $input, $this);// // Check for an error in the validation test. if ($valid instanceof \Exception) { return $valid; } }
這裏獲取validate
屬性的值以後,調用對應類的test方法。這裏咱們以本次的補丁爲例validate=options
,跟進OptionsRule的test方法
public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) { // Check if the field is required. $required = ((string) $element['required'] == 'true' || (string) $element['required'] == 'required'); if (!$required && empty($value)) { return true; } // Make an array of all available option values. $options = array(); // Create the field $field = null; if ($form) { $field = $form->getField((string) $element->attributes()->name, $group); } // When the field exists, the real options are fetched. // This is needed for fields which do have dynamic options like from a database. if ($field && is_array($field->options)) { foreach ($field->options as $opt)//取出全部option節點 { $options[] = $opt->value;//取出field節點對應的option子節點,用於後面進行in_array()校驗合法性 } } else { foreach ($element->option as $opt)//取出全部option節點 { $options[] = $opt->attributes()->value;//取出field節點對應的option子節點,用於後面進行in_array()校驗合法性 } } // There may be multiple values in the form of an array (if the element is checkboxes, for example). if (is_array($value)) { // If all values are in the $options array, $diff will be empty and the options valid. $diff = array_diff($value, $options);//校驗 return empty($diff); } else { // In this case value must be a string return in_array((string) $value, $options);//校驗 } }
原理比較簡單,就是經過in_array()和array_diff()
將用戶輸入值與option節點的值進行對比。
######################### 最後最後一句話
新年快樂,但願2020年能變強。
2019年12月31日 22點55分