0x01 分析
$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; ... }
/index.php?option=com_content&view=articles&layout=modal&tmpl=component
$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
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')); ... } ...}
foreach (ModuleHelper::getModules($position) as $mod){ $moduleHtml = $renderer->render($mod, $params, $content); ...}
这里回到最开始的漏洞点
$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即可。
0x02 更好的注入
可是目前为止这个漏洞还只是盲注而已。。回显它不香吗?并且之前拼接的SQL语句执行之后会报错
Unknown column 'a.hits' in 'order clause'
$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'));
...
0x03 利用
这里仅放出效果图,具体的poc就不公开了
0x04 总结
这个洞还是比较鸡肋的,1是需要最高的super user权限,2是由于有token校验无法进行csrf,因此把这个漏洞限制成只能有sa账号才能进行利用。
0x05 补丁分析
在打了补丁的3.9.14中,通过diff发现官方做的修复很简单,只是在module中存储时对字段进行了校验
/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);//校验 }}