Joomla!是一套全球知名的内容管理系统。Joomla!是使用PHP语言加上MySQL数据库所开发的软件系统,可以在Linux、 Windows、MacOSX等各种不同的平台上执行。目前是由Open Source Matters(见扩展阅读)这个开放源码组织进行开发与支持,这个组织的成员来自全世界各地,小组成员约有150人,包含了开发者、设计者、系统管理者、文件撰写者,以及超过2万名的参与会员。

漏洞简述

这个漏洞出现在Joomla3.7.0新增的组件com_field里,这个组件的访问没有做任何身份验证,并且在处理fullordering参数时没有合格的过滤,导致最终将用户的输入拼接在了sql查询语句的order by参数里,形成注入。

源码结构

Joomla!源码结构如下图

调用流程

入口函数如下,前面的都是用来宏定义一些参数,最后一行execute转入site.php接着转入helper.php,通过require_once调用传入的组件参数。

如下为Joomla的调用栈,可以很清晰的看到Joomla的调用路径。

这个fields.php关键代码如下。分别完成了组件注册,控制器的实例生成,执行命令等功能。

1
2
3
4
JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php');
$controller = JControllerLegacy::getInstance('Fields');
$controller->execute(JFactory::getApplication()->input->get('task'));
$controller->redirect();

首先来看看fields组件生成实例部分的代码,在它的构造函数里,注意到当我们访问这个组件时,它会把路径设置为JPATH_COMPONENT_ADMINISTRATOR,而这个宏定义默认为administrator\components\,使得后面加载model时是直接用administrator目录下的函数进行加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __construct($config = array())
{
$this->input = JFactory::getApplication()->input;
// Frontpage Editor Fields Button proxying:
if ($this->input->get('view') === 'fields' && $this->input->get('layout') === 'modal')
{
// Load the backend language file.
$lang = JFactory::getLanguage();
$lang->load('com_fields', JPATH_ADMINISTRATOR);
$config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR;
}
parent::__construct($config);
}

在获取实例后就进入了$controller->execute方法,该方法首先调用如下函数,它最后返回的doTask值为display,接着调用库函数中的display函数,它又会调用组件目录下display函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function execute($task)
{
$this->task = $task;
$task = strtolower($task);
if (isset($this->taskMap[$task]))
{
$doTask = $this->taskMap[$task];
}
elseif (isset($this->taskMap['__default']))
{
$doTask = $this->taskMap['__default'];
}
else
{
throw new Exception(JText::sprintf('JLIB_APPLICATION_ERROR_TASK_NOT_FOUND', $task), 404);
}
// Record the actual task being fired
$this->doTask = $doTask;
return $this->$doTask();
}

|

display函数调用组件的model文件,接着它调用了libraries\legacy\model\list.php中的populateState方法,在处理参数fulloredering时,没有太多严格的过滤,接着就直接使用了setstate方法把用户输入保存了下来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case 'fullordering':
$orderingParts = explode(' ', $value);
if (count($orderingParts) >= 2)
{
...
}
else
{
$this->setState('list.ordering', $ordering);
$this->setState('list.direction', $direction);
}
break;
...
$value = $app->getUserStateFromRequest($this->context . '.limitstart', 'limitstart', 0, 'int');
$limitstart = ($limit != 0 ? (floor($value / $limit) * $limit) : 0);
$this->setState('list.start', $limitstart);

|

保存下来的用户输入如下。

整个调用栈如下

其中调用getUserStateFromRequest方法处理用户的输入,接着它调用了getUserState方法进行处理,注册session,生成list.fullordering的值。

1
2
3
4
5
6
7
8
9
10
public function getUserState($key, $default = null)
{
$session = JFactory::getSession();
$registry = $session->get('registry');
if (!is_null($registry))
{
return $registry->get($key, $default);
}
return $default;
}

|

接着在display函数里的$this->get(Items’)方法中,通过getstate方法将list.fullordering的值,在逃脱了escape方法过滤的情况下,拼接进了sql语句中,并在之后得到执行并回显

1
2
3
4
5
6
7
8
9
10
11
// Add the list ordering clause
$listOrdering = $this->getState('list.fullordering', 'a.ordering');
$orderDirn = '';
if (empty($listOrdering))
{
$listOrdering = $this->state->get('list.ordering', 'a.ordering');
$orderDirn = $this->state->get('list.direction', 'DESC');
}

$query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn));
return $query;

执行结果如下

流程图如下

修复方法

官方给出的修复如下

在第三步拼接sql时,在administrator/components/com_fields/models/fields.php里不再使用用户可控的fullordering参数,而是直接拼接ordering参数,而这个参数在输入时会进行白名单检测,无法形成注入,因此可以成功防御此漏洞。