前言

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

Joomla 3.9.13 二次注入分析(CVE-2019-19846)的更多相关文章

  1. [一道蓝鲸安全打卡Web分析] 文件上传引发的二次注入

    蓝鲸打卡的一个 web 文件上传引发二次注入的题解和思考 蓝鲸文件管理系统 源代码地址:http://www.whaledu.com/course/290/task/2848/show 首先在设置文件 ...

  2. 【PHP代码审计】 那些年我们一起挖掘SQL注入 - 4.全局防护Bypass之二次注入

    0x01 背景 现在的WEB程序基本都有对SQL注入的全局过滤,像PHP开启了GPC或者在全局文件common.php上使用addslashes()函数对接收的参数进行过滤,尤其是单引号.二次注入也是 ...

  3. 【sql注入】简单实现二次注入

    [sql注入]简单实现二次注入 本文转自:i春秋社区 测试代码1:内容详情页面 [PHP] 纯文本查看 复制代码 01 02 03 04 05 06 07 08 09 10 11 12 13 14 1 ...

  4. 二次注入的学习--Buy Flag(http://10.112.68.215:10002)

    这次有做一个二次注入的天枢CTF题目,算是完整地理解了一遍注入的知识.来,启航.   1.判断注入点     经过对题目的实践分析,知道注册时需要输入年龄大于18岁,但在登录后界面,年龄因为太大不能接 ...

  5. 利用反射型XSS二次注入绕过CSP form-action限制

    利用反射型XSS二次注入绕过CSP form-action限制 翻译:SecurityToolkit 0x01 简单介绍 CSP(Content-Security-Policy)是为了缓解XSS而存在 ...

  6. 护网杯 three hit 复现(is_numeric引发的二次注入)

    1.题目源码 https://github.com/ZhangAiQiang/three-hit 题目并不真的是当时源码,是我根据做法自己写的,虽然代码烂,但是还好能达到复现的目的 ,兄弟们star一 ...

  7. 帝国CMS(EmpireCMS) v7.5 代码注入分析(CVE-2018-19462)

    帝国CMS(EmpireCMS) v7.5 代码注入分析(CVE-2018-19462) 一.漏洞描述 EmpireCMS7.5及之前版本中的admindbDoSql.php文件存在代码注入漏洞.该漏 ...

  8. COMMENT SQL二次注入

    这题目太顶了进去随便发个贴,出现登录已经提示用户名和密码了,弱密码登录(得自己去爆破)zhangwei666即可 没啥思路,扫下目录试试,kali的dirb扫到.git泄露githacker得到源码看 ...

  9. sqli-labs less-24(二次注入)

    less-24 原理: 在网站处理用户提交的数据的时候,只是将某些敏感字符进行了转义.因而使得用户第一次提交的时候不会被当做代码执行.但是这些数据存入数据库的时候却没有转义,而网站程序默认数据库中的数 ...

随机推荐

  1. Kubernetes1.4新特性前瞻:设置JOB执行计划

    (一)  核心概念 Kubernetes在新版中会新增了一个设置JOB执行计划的功能,在1.3中已经可以初见端倪了,从进度上来看会在1.4版本中进行发布,下面我们先睹为快. Kubernetes通过这 ...

  2. 关于Lattice Planner规划算法的若干问答

    Apollo问答 | 关于Lattice Planner规划算法的若干问答   上周,我们在Apollo开发者交流群内做了关于Lattice Planner的分享.这里,我们将社群分享里开发者提出的问 ...

  3. 树状数组(Binary Index Tree)

    一维BIT(单点更新,区间求和): Problem - 1166 #include <iostream> #include <algorithm> #include <c ...

  4. hdu 4128 Running relay (线性规划转半平面交)

    Problem - 4128 对偶线性规划转半平面交,这题的正解O(nlogn)解法,目前网上没有找到这样的正解. 原来的不等式组, sigma{-si*xi}>=-W+d*sigma{si} ...

  5. Top 10 Free IT Certification Training Resources

    1. Cybrary Cybrary takes the open source concept and applies it to IT training. Many of the courses ...

  6. AtCoder Regular Contest 059

    C - いっしょ / Be Together 数据比较小,暴力就挺好的.O(n^2)可过的好题 #include <bits/stdc++.h> using namespace std; ...

  7. jvm内存监控

    jstack -- 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程 ...

  8. 强连通分量-----Kosaraju

    芝士: 有向图强连通分量在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connect ...

  9. kwargs.pop是什么意思

    pop()函数一般用来删除list列表的末尾元素,同样,kwargs.pop()用来删除关键字参数中的末尾元素,比如:kwargs = {'Michael': 95, 'Bob': 75, 'Trac ...

  10. H3C 轮询DCC和共享DCC