前言

这一个需要管理员权限的二次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. Python基础:17类和实例之一(类属性和实例属性)

    1:类通常在一个模块的顶层进行定义.对于Python来说,声明与定义类是同时进行的. 2:类属性仅与其类相绑定,类数据属性仅当需要有更加“静态”数据类型时才变得有用,这种属性是静态变量.它们表示这些数 ...

  2. Python基础:00概述

    1:续行符 在Python中,一般是一行一个语句.一个过长的语句可以使用反斜杠( \ )分解成几行. 有两种例外情况,一个语句不使用反斜线也可以跨行.在使用闭合操作符时,单一语句可以跨多行,例如:在含 ...

  3. laravel 踩坑 env,config

    正常情况: env 方法 可以获取 .env 文件的值 config 可以获取 config 文件夹下 指定配置的值 非正常情况: 当我们执行了 php artisan config:cache 之后 ...

  4. oracle函数 TRANSLATE(c1,c2,c3)

    [功能]将字符表达式值中,指定字符替换为新字符 [说明]多字节符(汉字.全角符等),按1个字符计算 [参数] c1   希望被替换的字符或变量 c2   查询原始的字符集 c3   替换新的字符集,将 ...

  5. Libev源码分析06:异步信号同步化--sigwait、sigwaitinfo、sigtimedwait和signalfd

    一:信号简述 信号是典型的异步事件.内核在某个信号出现时有三种处理方式: a:忽略信号,除了SIGKILL和SIGSTOP信号不能忽略外,其他大部分信号都可以被忽略: b:捕捉信号,也就是在信号发生时 ...

  6. 【DCN】Wireless Intranet Captive Portal

    Wireless Intranet Captive Portal 配置AAA服务Radius认证 radius-server key 0 radius radius-server authentica ...

  7. Educational Codeforces Round 5(A,B题)

    虽然是水题但还是贴下代码把 A #include<cstring> #include<cstdio> using namespace std; ; char x[qq],y[q ...

  8. java Iterator接口

    Iterator主要遍历Collection集合中的元素,也有称为迭代器或迭代精灵. boolean hasNext():若被迭代的集合元素还没有被遍历,返回true. Object  next(): ...

  9. java 多线程安全问题的解决方法

    三种方法: 同步代码块: synchronized(obj) { //obj表示同步监视器,是同一个同步对象 /**..... TODO SOMETHING */ }   同步方法 格式: 在方法上加 ...

  10. pycharm下的多个python版本共存(二)

    上一篇博文介绍了在windows下同时安装python2和python3.而在工作的过程中,我习惯于用pycharm作为IDE.本文将记录如何在pycharm中选择python版本,并给相应的版本安装 ...