前言
這一個需要管理員權限的二次SQL注入,利用起來比較雞肋。這里僅分享一下挖洞時的思路,不包含具體的poc。
分析
漏洞觸發點在components/com_content/models/articles.php:L458
$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在本組件中卻沒有使用。
程序中有兩個名為articles的model,一個在/components,一個在/administrator/components目錄下。我在黑盒測試的時候構造了一個url如下:
/index.php?option=com_content&view=articles&layout=modal&tmpl=component
這里程序中的控制器會根據view和layout的值,將請求直接跳到了administrator目錄下的articles中了。但是根據存在即合理,天生我材必有用
,/components下面有個前台articles的model,因此程序中一定會有調用這里的地方。最終找到了幾處調用前台article的地方,只是有的跟正常調用的不太一樣,這里是動態調用。寫法大概有如下幾種
$model = JModelLegacy::getInstance('Articles', 'ContentModel', array('ignore_request' => true));
也有動態調用model:
/libraries/src/MVC/Controller/BaseController.php:createModel($model, ...){
...
JModelLegacy::getInstance($modelName, $classPrefix, $config);
...
}
通過訪問
index.php/blog?252c5a5ef0e3df8493dbe18e7034957e=1
可以到達漏洞點,但是state我們控制不了,因為首先在articles model中沒有對date_field做賦值處理,只能寄希望於調用這個model的地方能對date_field賦值。可是通過查看代碼發現,當前的index.php/blog路由背后的com_content組件並沒有對date_field進行初始化,因此這個組件只能放棄,看看其他的。
終於,在一個module:mod_articles_popular的helper類中找到了有設置date_field的地方,大概如下/modules/mod_articles_popular/helper.php
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分