Сегoдня мы рассмотрим эксплуатацию критической 1day-уязвимости в популярной CMS Joomla, которая прогремела на просторах интернета в конце октября. Речь пойдет об уязвимостях с номерами CVE-2016-8869, CVE-2016-8870 и CVE-2016-9081. Все три происходят из одного кусочка кода, который пять долгих лет томился в недрах фреймворка в ожидании своего часа, чтобы затем вырваться на свободу и принести с собой хаос, взломанные сайты и слезы ни в чем не повинных пользователей этой Joomla. Лишь самые доблестные и смелые разработчики, чьи глаза красны от света мониторов, а клавиатуры завалены хлебными крошками, смогли бросить вызов разбушевавшейся нечисти и возложить ее голову на алтарь фиксов.

WARNING

Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.
 

С чего все началось

6 октября 2016 года Дэмис Пальма (Demis Palma) создал топик на Stack Exchange, в котором поинтересовался: а почему, собственно, в Joomla версии 3.6 существуют два метода регистрации пользователей с одинаковым названием register()? Первый находится в контроллере UsersControllerRegistration, а второй — в UsersControllerUser. Дэмис хотел узнать, используется ли где-то метод UsersControllerUser::register(), или это лишь эволюционный анахронизм, оставшийся от старой логики. Его беспокоил тот факт, что, даже если этот метод не используется никаким представлением, он может быть вызван при помощи сформированного запроса. На что получил ответ от девелопера под ником itoctopus, подтвердившего: проблема действительно существует. И направил отчет разработчикам Joomla.

Далее события развивались самым стремительным образом. 18 октября разработчики Joomla принимают репорт Дэмиса, который к тому времени набросал PoC, позволяющий регистрировать пользователя. Он опубликовал заметку на своем сайте, где в общих чертах рассказал о найденной проблеме и мыслях по этому поводу. В этот же день выходит новая версия Joomla 3.6.3, которая все еще содержит уязвимый код.

После этого Давиде Тампеллини (Davide Tampellini) раскручивает баг до состояния регистрации не простого пользователя, а администратора. И уже 21 октября команде безопасности Joomla прилетает новый кейс. В нем речь уже идет о повышении привилегий. В этот же день на сайте Joomla появляется анонс о том, что во вторник, 25 октября, будет выпущена очередная версия с порядковым номером 3.6.3, которая исправляет критическую уязвимость в ядре системы.

25 октября Joomla Security Strike Team находит последнюю проблему, которую создает обнаруженный Дэмисом кусок кода. Затем в главную ветку официального репозитория Joomla пушится коммит от 21 октября с неприметным названием Prepare 3.6.4 Stable Release, который фиксит злосчастный баг.

После этого камин-аута к междусобойчику разработчиков подключаются многочисленные заинтересованные личности — начинают раскручивать уязвимость и готовить сплоиты.

27 октября исследователь Гарри Робертс (Harry Roberts) выкладывает в репозиторий Xiphos Research готовый эксплоит, который может загружать PHP-файл на сервер с уязвимой CMS.

 

Детали

Что ж, с предысторией покончено, переходим к самому интересному — разбору уязвимости. В качестве подопытной версии я установил Joomla 3.6.3, поэтому все номера строк будут актуальны именно для этой версии. А все пути до файлов, которые ты увидишь далее, будут указываться относительно корня установленной CMS.

Благодаря находке Дэмиса Пальмы мы знаем, что есть два метода, которые выполняют регистрацию пользователя в системе. Первый используется CMS и находится в файле /components/com_users/controllers/registration.php:108. Второй (тот, что нам и нужно будет вызвать), обитает в /components/com_users/controllers/user.php:293. Посмотрим на него поближе.

286:    /**
287:     * Method to register a user.
288:     *
289:     * @return  boolean
290:     *
291:     * @since   1.6
292:     */
293:    public function register()
294:    {
295:    JSession::checkToken('post') or jexit(JText::_('JINVALID_TOKEN'));
...
300:    // Get the form data.
301:    $data = $this->input->post->get('user', array(), 'array');
...
315:    $return = $model->validate($form, $data);
316:
317:    // Check for errors.
318:    if ($return === false)
319:    {
...
345:    // Finish the registration.
346:    $return = $model->register($data);

Здесь я оставил только интересные строки. Полную версию уязвимого метода можно посмотреть в репозитории Joomla.

Разберемся, что происходит при обычной регистрации пользователя: какие данные отправляются и как они обрабатываются. Если регистрация пользователей включена в настройках, то форму можно найти по адресу http://joomla.local/index.php/component/users/?view=registration.

Настройка, отвечающая за разрешение регистрации пользователей
Настройка, отвечающая за разрешение регистрации пользователей

Легитимный запрос на регистрацию пользователя выглядит как на следующем скриншоте.

За работу с пользователями отвечает компонент com_users. Обрати внимание на параметр task в запросе. Он имеет формат $controller.$method. Посмотрим на структуру файлов.

Структура контроллеров компонента com_users
Структура контроллеров компонента com_users

Имена скриптов в папке controllers соответствуют названиям вызываемых контроллеров. Так как в нашем запросе сейчас $controller = "registration", то вызовется файл registration.php и его метод register().

Внимание, вопрос: как передать обработку регистрации в уязвимое место в коде? Ты наверняка уже догадался. Имена уязвимого и настоящего методов совпадают (register), поэтому нам достаточно поменять название вызываемого контроллера. А где у нас находится уязвимый контроллер? Правильно, в файле user.php. Получается $controller = "user". Собираем все вместе и получаем task = user.register. Теперь запрос на регистрацию обрабатывается нужным нам методом.

Попали в уязвимый метод класса UsersControllerUser
Попали в уязвимый метод класса UsersControllerUser

Второе, что нам нужно сделать, — это отправить данные в правильном формате. Тут все просто. Легитимный register() ждет от нас массив под названием jform, в котором мы передаем данные для регистрации — имя, логин, пароль, почту (см. скриншот с запросом).

  • /components/com_users/controllers/registration.php:
      124:    // Get the user data.
      125:    $requestData = $this->input->post->get('jform', array(), 'array');
    

Наш подопечный получает эти данные из массива с именем user.

  • /components/com_users/controllers/user.php:
      301:    // Get the form data.
      302:    $data = $this->input->post->get('user', array(), 'array');
    

Поэтому меняем в запросе имена всех параметров с jfrom на user.

Третий наш шаг — это нахождение валидного токена CSRF, так как без него никакой регистрации не будет.

  • /components/com_users/controllers/user.php:
      296:    JSession::checkToken('post') or jexit(JText::_('JINVALID_TOKEN'));
    

Он выглядит как хеш MD5, а взять его можно, например, из формы авторизации на сайте /index.php/component/users/?view=login.

CSRF-токен из формы авторизации
CSRF-токен из формы авторизации

Теперь можно создавать пользователей через нужный метод. Если все получилось, то поздравляю — ты только что проэксплуатировал уязвимость CVE-2016-8870 «отсутствующая проверка разрешений на регистрацию новых пользователей».

Вот как она выглядит в «рабочем» методе register() из контроллера UsersControllerRegistration:

  • /components/com_users/controllers/registration.php:
      113:    // If registration is disabled - Redirect to login page.
      114:    if (JComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0)
      115:    {
      116:        $this->setRedirect(JRoute::_('index.php?option=com_users&view=login', false));
      117:
      118:        return false;
      119:    }
    

А так в уязвимом:

  • /components/com_users/controllers/user.php:

Ага, никак.

Чтобы понять вторую, гораздо более серьезную проблему, отправим сформированный нами запрос и проследим, как он выполняется на различных участках кода. Вот кусок, который отвечает за проверку отправленных пользователем данных в рабочем методе:

  • /components/com_users/controllers/registration.php:
      137:    $data = $model->validate($form, $requestData);
      ...
      167:    // Attempt to save the data.
      168:    $return = $model->register($data);
    

А вот как он выглядит в уязвимой версии метода:

  • /components/com_users/controllers/user.php:
      315:    $return = $model->validate($form, $data);
      ...
      345:    // Finish the registration.
      346:    $return = $model->register($data);
    

Чувствуешь разницу? В первом случае в базу записываются валидированные пользовательские данные, а во втором они только проверяются на валидность. В базу же записываются сырые — те, что мы отправили в запросе. В данном случае это очень важный момент, позже будет понятно почему.

Метод validate модели Registration не просто выполняет базовые проверки (правильность указания email, наличие пользователя с таким же ником, почтой и так далее), он еще отбрасывает те параметры, что не предусмотрены моделью регистрации.

  • /libraries/legacy/model/form.php:
      339:    // Filter and validate the form data.
      340:    $data = $form->filter($data);
    

Посмотреть все правила можно в файле /components/com_users/models/forms/registration.xml.

Получается, что в случае «правильной» регистрации лишние данные отфильтруются функцией валидации и перезапишут переменную $data, а затем попадут то в место, где создаются пользователи.

В уязвимом методе эта логика нарушена. Результат фильтрации записывается в переменную $return, а в функцию register все так же попадает $data, только на этот раз в ней находятся данные прямиком из запроса. Чтобы понять, зачем нам, собственно, нужно было разбирать это поведение, перенесемся в блок регистрации.

  • /components/com_users/models/registration.php:
      380:    public function register($temp)
      ...
      386:        $data = (array) $this->getData();
    

В $temp обитают наши данные прямиком из запроса. Код на строке 386 готовит данные для создания будущего пользователя. Нас интересует переменная new_usertype.

  • /components/com_users/models/registration.php:
      234:    public function getData()
      ...
      250:        // Get the groups the user should be added to after registration.
      251:        $this->data->groups = array();
      252:        // Get the default new user group, Registered if not specified.
      253:        $system = $params->get('new_usertype', 2);
      254:
      255:        $this->data->groups[] = $system;
    

В new_usertype хранится ID группы, к которой будет относиться новоиспеченный юзер. Этот код берется из настроек, и по умолчанию это Registered (id=2). Только ведь существуют гораздо более интересные группы, зачем нам томиться в этой? Результат выполнения getData — массив, в котором элемент groups указывает на будущую принадлежность пользователя к определенной группе.

[groups] => Array
(
    [0] => 2
)
Перезапись элемента groups
Перезапись элемента groups

Дальше этот массив сливается с отправленными нами данными.

  • /components/com_users/models/registration.php:
      387:    $data = (array) $this->getData();
      388:
      389:    // Merge in the registration data.
      390:    foreach ($temp as $k => $v)
      391:    {
      392:        $data[$k] = $v;
      393:    }
    

Вот тут-то и притаилось главное зло, оно же CVE-2016-8869. Если в запросе, помимо нужных для регистрации данных, мы отправим еще и groups, то дефолтное значение будет перезаписано и пользователь окажется привязан к указанной нами группе.

Перезапись элемента groups
Перезапись элемента groups

Теперь мы можем создавать админов (id=7). При добавлении этого поля обрати внимание на то, что элемент groups — это тоже массив, поэтому в запросе указываем именно user[group][].

Созданный через уязвимость пользователь с правами администратора
Созданный через уязвимость пользователь с правами администратора

К сожалению, нельзя так просто взять и создать суперадмина. При регистрации выполняется проверка.

  • /libraries/joomla/user/user.php:
      757:    // We are only worried about edits to this account if I am not a Super Admin.
      758:    if ($iAmSuperAdmin != true && $iAmRehashingSuperadmin != true)
      ...
      766:    if ($this->groups != null)
      767:    {
      768:        // I am not a Super Admin and I’m trying to make one.
      769:        foreach ($this->groups as $groupId)
      770:        {
      771:            if (JAccess::checkGroup($groupId, 'core.admin'))
      772:            {
      773:                throw new RuntimeException('User not Super Administrator');
      774:            }
      775:        }
      776:    }
    

Следовательно, только суперадмины могут создавать пользователей, подобных себе. Но нам это и не нужно, ведь в рукаве припрятан еще один козырь — CVE-2016-9081.

Благодаря слаженной работе найденных багов и функций CMS мы можем не только создавать новых пользователей, но и перезаписывать данные уже существующих. Нам нужно узнать ID зарегистрированного суперадминистратора и передать его в запросе как user[id]. Помимо этого, в user[groups][] должна быть отправлена пустая строка. Это нужно для того, чтобы дефолтное значение группы пользователя затерлось и не изменилось в базе. Если этого не сделать, пользователь из группы суперадминов (id=8) уедет в группу зарегистрированных (id=2).

Перезаписанные параметры класса User
Перезаписанные параметры класса User

После отправки данные попадут в метод bind, который превратит их в параметры класса создаваемого пользователя.

  • /libraries/joomla/user/user.php:
      681:    // Bind the array
      682:    if (!$this->setProperties($array))
      683:    {
      684:        $this->setError(JText::_('JLIB_USER_ERROR_BIND_ARRAY'));
      685:
      686:        return false;
      687:    }
    
  • /libraries/joomla/object/object.php:
      212:    public function setProperties($properties)
      ...
      216:        foreach ((array) $properties as $k => $v)
      217:        {
      218:        // Use the set function which might be overridden.
      219:        $this->set($k, $v);
      220:        }
    

Затем save запишет их в таблицу users.

  • /libraries/joomla/user/user.php:
      706:    public function save($updateOnly = false)
      ...
      711:    $table->bind($this->getProperties());
      ...
      791:        // Store the user data in the database
      792:        $result = $table->store();
    

Вуаля! Все данные, в том числе и пароль, теперь изменены на указанные нами в запросе, а группа пользователя осталась та же.

Оригинальные данные суперадминистратора
Оригинальные данные суперадминистратора

Измененные данные суперадминистратора
Измененные данные суперадминистратора

Здесь я не буду расписывать, каким образом можно выполнить произвольный код из аккаунта суперадминистратора. Задача эта тривиальная, да и тема статьи другая.
На этом с уязвимостями предлагаю закончить. Но осталась еще одна смежная тема, которую я хотел бы осветить.

 

Обход ограничения на загрузку неугодных файлов

Не могу не упомянуть о способе загрузки PHP-файлов, который был найден ребятами из Xiphos Research.

Исследуя описанные выше уязвимости, они столкнулись с такой проблемой: Joomla отклоняет загруженные файлы, содержащие <?php и файлы c опасными расширениями. Полный кусок кода, который проверяет файлы на вшивость, можно посмотреть в /libraries/joomla/filter/input.php:584 или перейдя по этой ссылке на исходник. Выход нашелся благодаря знаниям тонкостей настройки веб-серверов. Оказывается, помимо стандартных php4, php5 и прочих .phtml, большая часть веб-серверов из коробки выполняет файлы .pht.

<FilesMatch ".+\.ph(p[345]?|t|tml)$">
    SetHandler application/x-httpd-php
</FilesMatch>

Естественно, Joomla не считает это расширение опасным и разрешает его загрузку и наличие шорт-тега <?= внутри файла. В своем эксплоите Xiphos используют именно такой способ доставки PHP-кода.

 

Заключение

Найденная уязвимость еще раз подтверждает, что иногда баги могут годами лежать на самом видном месте и не быть обнаруженными. Добавлю, что оперативно обратить внимание на эту уязвимость мне помог проект Vulners. Если вдруг кто не знает — это поисковик по всевозможному контенту, связанному с безопасностью. Вот, например, вся информация по обнаруженным уязвимостям Joomla.

4 комментария

  1. Аватар

    webdizy

    07.11.2016 в 09:26

    Спасибо за статью! Интересно!

  2. Аватар

    ruslan

    07.11.2016 в 15:17

    Поддерживаю! За все скрины и куски сорса отдельное спасибо! 🙂

  3. Аватар

    Guzoo

    30.11.2016 в 16:11

    Окей, но если перезаписать данные существующего супер-пользователя, то значения block и activation как перезаписать? Без них супер-пользователь не будет работать, а при активации по email будет «User not Super Administrator»

    • Аватар

      aLLy

      28.12.2016 в 23:49

      Нужно же оставить немного для самостоятельного изучения, так же куда интереснее и полезнее.
      Не будет работать, совершенно верно. Класс JUser не даст перезаписать суперадмина кому ни попадя. Именно поэтому нельзя просто в user[groups][] указать ID=8.
      Нужно посмотреть в код и в корень проблемы. Почему пользователь становится заблоченым? Потому что при регистрации проверяется переменная $useractivation и если она =1 или =2, то block перезаписывается на 1. Значение этой $useractivation берется из настроек системы и по умолчанию оно =1, т.е. токен активации улетает на мыло => нужно её просто отключить, а затем уже отправлять пакет на изменение данных суперюзеру.
      Выключить очень просто. Регаешься сначала админом (ID=7), затем активируешь его. Заходишь в админку и отключаешь активацию зарегестрированных пользователей. Заодно, кстати, можно глянуть ID самого суперадмина, в разделе Users, брутфорс ведь тоже не лучшее решение.

Оставить мнение