Хакер - Брут для «Джумлы». Обходим авторизацию, чтобы получить пароли пользователей в Joomla CMS
nopaywall

Содержание статьи
Популярнейшая многофункциональная CMS Joomla снова с нами. Несколько месяцев назад мы уже разбирали уязвимость в ней, но тогда это была SQL-инъекция, а теперь на повестке более экзотическая штука — LDAP Injection. Уязвимости такого типа встречаются нечасто, тем более в системах управления сайтами, но тем интереснее будет изучить ее.
Уязвимость называется 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. Туда я помещу нового пользователя.
Созданный юзер в программе LDAP AdminПочти все готово. Теперь включаем в настройках Joomla плагин авторизации через LDAP.
Активация плагина LDAP-авторизацииЗатем переходим в настройки этого плагина и вбиваем наши данные.
Настройка 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. Если пользователь найден, то система будет отвечать, что пароль неверен, а если пользователя вообще нет, то система известит нас об этом в соответствующем сообщении.
Разные сообщения об ошибках в зависимости от переданных кредсовВызывать различные ошибки можно с помощью шаблонов поиска.
PoC
Используя астериск (*) и логические операторы, ты сможешь вытащить логины пользователей c их паролями из LDAP. Синтаксис логических операторов в LDAP можешь посмотреть в MSDN.
Указываем в поле «Логин» формы авторизации строку ;uid=a*, а в качестве passwd — что угодно и отправляем запрос. Если пользователь, логин которого начинается с символа а, присутствует в базе LDAP, то получаем ошибку неверного пароля.
Таким образом можно посимвольно перебрать сначала имя пользователя, а затем по аналогии и его пароль.
Я накидал небольшой скриптик на JS, который выполняет всю работу. Все, что нужно сделать, — это перейти на страницу авторизации, выполнить его и смотреть, как бегут буквы при переборе.
Принцип работы скрипта прост: берутся по очереди символы, указанные в переменной charset, формируется и отправляется на сервер запрос при помощи метода post из библиотеки jQuery. Если сервер возвращает ошибку о невозможности найти пользователя, то очередь переходит к следующему символу из списка.
После удачного подбора логина настает очередь пароля. Символы амперсанда экранируются системой, а значит, использовать условие «И» у нас не выйдет. Поэтому воспользуемся условием «ИЛИ».
;|(uid=none)(password=q*)
Тут мы нарочно делаем первую часть выражения логическим false (uid=none) и пытаемся получить true от второй.
Этот процесс напомнил мне взлом паролей из фильмов двухтысячных. Да и в старой доброй игре Uplink был похожий подход.
Сбрученный через LDAP-инъекцию пароль от юзераВывод
В целом это все, что я хотел тебе рассказать. Уязвимости такого плана редко встретишь в CMS, подобных Joomla. Однако это не повод списывать их со счетов. Крупные и не очень компании любят все автоматизировать, и единая авторизация через LDAP попадается довольно-таки часто.
Также хотелось бы поделиться ссылкой на небольшое исследование, которое было представлено на Black Hat, — в нем речь идет о разных LDAP-инъекциях. Хотя сам документ древний, идеи, озвученные в нем, до сих пор актуальны. Если тебя интересует эта тема, то настоятельно рекомендую к прочтению. И не стесняйся гуглить «ldap injection»!
Читайте ещё больше платных статей бесплатно: https://t.me/nopaywall