Содержание статьи
Уязвимость называется CVE-2017-14596, и найдена она была аж 27 июля 2017 года. А нашел ее Йоханес Дасе из RIPS Technologies GmbH. Уязвимы все версии CMS, начиная с 1.5.0 и заканчивая 3.7.5 включительно. То есть ошибка оставалась в коде необнаруженной в течение восьми лет, до того как ее пофиксили 19 сентября 2017-го.
Стенд
Как устанавливать Joomla, я думаю, все в курсе. Чтобы не париться с базовой настройкой сервера, я слепил файл Docker, который ты можешь скачать из моего репозитория. После его запуска тебе останется только пройти по шагам установки CMS.
Теперь дело за LDAP. Мне совсем не хотелось разбираться с настройкой сервера под Linux, поэтому я решил использовать OpenLDAP для Windows и установить все это дело в три клика.
После установки, если ты оставил все параметры по умолчанию, реквизиты для коннекта будут следующими:
User: cn=Manager,dc=maxcrc,dc=com
Password: secret
Для управления сервером я воспользуюсь утилитой LDAP Admin. По умолчанию на сервере уже имеется предустановленный OU (organisation unit) People. Туда я помещу нового пользователя.
Почти все готово. Теперь включаем в настройках Joomla плагин авторизации через LDAP.
Затем переходим в настройки этого плагина и вбиваем наши данные.
Обрати внимание на параметр Search String
, его я взял из официальной документации по настройке этого плагина на сайте CMS.
Детали уязвимости
Переходим к изучению причин уязвимости. Наш тернистый путь начинается с класса LoginController
.
/administrator/components/com_login/controller.php
17: class LoginController extends JControllerLegacy
...
48: /**
49: * Method to log in a user.
50: *
51: * @return void
52: */
53: public function login()
54: {
...
60: $model = $this->getModel('login');
61: $credentials = $model->getState('credentials');
Как видно из названия, он отвечает за процесс авторизации пользователей. Данные пользователей, переданные в форме логина, попадают в переменную $credentials
.
После этого они передаются в метод login
.
/administrator/components/com_login/controller.php
58: $app = JFactory::getApplication();
...
64: $result = $app->login($credentials, array('action' => 'core.login.admin'));
/libraries/cms/application/cms.php
019: class JApplicationCms extends JApplicationWeb
...
859: public function login($credentials, $options = array())
860: {
861: // Get the global JAuthentication object.
862: $authenticate = JAuthentication::getInstance();
863: $response = $authenticate->authenticate($credentials, $options);
В процессе обработки кредсов выполняется метод authenticate
.
Дальше алгоритм работы зависит от активированных плагинов, которые отвечают за авторизацию. Отрабатывает метод onUserAuthenticate
, и данные уходят на обработку соответствующим плагинам.
/libraries/joomla/authentication/authentication.php
017: class JAuthentication extends JObject
...
253: public function authenticate($credentials, $options = array())
254: {
255: // Get plugins
256: $plugins = JPluginHelper::getPlugin('authentication');
...
268: foreach ($plugins as $plugin)
269: {
...
283: // Try to authenticate
284: $plugin->onUserAuthenticate($credentials, $options, $response);
Так как у нас настроена авторизация через LDAP, то выполнение переходит к классу PlgAuthenticationLdap
.
/plugins/authentication/ldap/ldap.php
014: /**
015: * LDAP Authentication Plugin
016: *
017: * @since 1.5
018: */
019: class PlgAuthenticationLdap extends JPlugin
...
032: public function onUserAuthenticate($credentials, $options, &$response)
033: {
В этом плагине имя пользователя попадает в запрос к серверу LDAP, который мы указывали как опцию search_string
. Документация Joomla говорит о том, что в строке запроса темплейт [search]
напрямую изменяется на текст, переданный в поле login. Что ты и можешь наблюдать в сорцах.
/plugins/authentication/ldap/ldap.php
069: switch ($auth_method)
070: {
071: case 'search':
072: {
...
086: // Search for users DN
087: $binddata = $ldap->simple_search(str_replace('[search]', $credentials['username'], $this->params->get('search_string')));
Мы указали uid=[search]
в параметре Search String в настройках плагина, так что после замены полученная строка уходит в метод simple_search
.
/libraries/vendor/joomla/ldap/src/LdapClient.php
016: class LdapClient
017: {
...
275: public function simple_search($search)
276: {
277: $results = explode(';', $search);
278:
279: foreach ($results as $key => $result)
280: {
281: $results[$key] = '(' . $result . ')';
282: }
283:
284: return $this->search($results);
285: }
В качестве заключительного шага сгенерированная строка поиска уходит на LDAP-сервер с помощью search
.
/libraries/vendor/joomla/ldap/src/LdapClient.php
298: public function search(array $filters, $dnoverride = null, array $attributes = array())
299: {
...
313: foreach ($filters as $search_filter)
314: {
315: $search_result = @ldap_search($resource, $dn, $search_filter, $attributes);
На данный момент у нас на руках LDAP-инъекция. Так как никакой фильтрации входных данных не происходит, мы можем влиять на отправляемую на сервер строку.
Сервер возвращает разные ответы в зависимости от результатов обработки запроса LDAP. Если пользователь найден, то система будет отвечать, что пароль неверен, а если пользователя вообще нет, то система известит нас об этом в соответствующем сообщении.
Вызывать различные ошибки можно с помощью шаблонов поиска.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»