这一个须要管理员权限的二次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分