Сюрприз в логах MaxPatrol VM — удаляем пароли перед отправкой в поддержку

Три стороны систем ИБ и неочевидные риски

При внедрении и сопровождении систем информационной безопасности, часто участвуют три стороны: вендор (разработчик), интегратор (технический подрядчик) и заказчик (организация-клиент). Вендор отвечает за разработку, обновления и вторую линию поддержки. Интегратор выполняет внедрение, настройку, доработки и оказывает первую линию поддержки. Заказчик работает с системой на практике, формирует требования и может передавать логи диагностики через интегратора в вендорскую поддержку. При этом ответственность за обезличивание или фильтрацию чувствительных данных в логах часто лежит на стороне заказчика. На приктике заказчик не всегда проверяет, а какие на самом деле данные в логах выгружаются.

Что может скрываться внутри диагностических архивов

Недавно, работая с системой MaxPatrol VM и формируя диагностический архив для обращения в техническую поддержку, я заметил неприятную особенность: информация, которая выгружается для анализа, содержит пароли в открытом виде. Пароли от учетных записей компонентов, которые являются частью системы. Эти компоненты изолированы в Docker-контейнерах, но доступ к ним может осуществляться из сети через открытые порты. Среди компонентов — PgAdmin для доступа к СУБД с высокой привелегией, Grafana, RabbitMQ.

Сами логи представляют собой набор текстовых файлов с конфигурациями и событиями от различных компонентов системы. Кроме того, при сборе диагностической информации происходит выгрузка переменных окружения из операционной системы Linux, где в открытом виде указаны пароли, используемые для взаимодействия между компонентами. Среди прочего, в данных могут попадаться пароли сервисных учетных записей, например, для подключения к SMTP или Proxy-серверу, которые относятся к инфраструктуре.

Попарсил изрядно 16 гигов логов =)
Попарсил изрядно 16 гигов логов =)

Для того что бы проверить наличие паролей в логах диагноситческого отчета можно зайти в каталог с логами и выполнить команду:

grep -irnE 'var' .

где var — одно из следующих значений переменной:
Password=
Password:

Какие могут быть риски при передаче паролей

Незначительная утечка такой информации может стать началом серьезных проблем. Например, администратор может использовать один и тот же пароль для разных систем, что приведёт к компрометации других систем. Наличие паролей в открытом виде существенно упрощает сценарии повышения привилегий через MaxPatrol VM.

Сама по себе система MaxPatrol VM — это актив повышенного риска. С серверов системы осуществляются подключения в различные сегменты сети, а для выполнения сканирований используются учётные записи с повышенными правами. Компрометация такой платформы потенциально означает доступ ко всей инфраструктуре.

Про здравый подход к безопасности

Иногда достаточно поработать с разными системами вживую, чтобы понять простую вещь: без доступа к логам действительно сложно выявить суть проблемы. Эксперты в области ИБ, работавшие с иностранными решениями, как правило, знают это на собственном опыте и именно поэтому рассматривают логи как ключевой источник информации для диагностики.

Но с опытом приходит и другое понимание: в логах не должно быть того, что само по себе может стать уязвимостью. Открытые пароли, переменные окружения с ключами, технические учётки — всё это, оказавшись в архиве, а затем в чужих руках, превращается в реальный риск.
И это уже не вопрос «хорошо или плохо». Это вопрос зрелого подхода к безопасности. Того самого, который формируется не в теории, а в практике там, где последствия всегда реальны.

В целом хранение и передача паролей в открытом виде противоречит базовым принципам безопасности и международным security best practices, на которых, в том числе, основаны и Российские нормативные требования в сфере ИБ. Ниже приведу некоторые ключевые источники, прямо указывающие на недопустимость хранения паролей в логах или конфигурационных файлах в открытом виде:

  • CWE-256: Plaintext Storage of a Password Зарегистрированная в списке MITRE Common Weakness Enumeration. Прямо указывает, что хранение паролей в незашифрованном виде — это нарушение архитектурной безопасности приложения.

  • OWASP: Password Plaintext Storage OWASP, где подчёркивается, что даже временное хранение пароля в логах может привести к его утечке.

  • OWASP Password Storage Cheat Sheet Практическое руководство по корректному обращению с паролями, включая рекомендации по хэшированию, защите в памяти и минимизации риска экспонирования.

Что делать в таком случае

В итоге я решил сам написать скрипт на Python, который рекурсивно обходит, выгруженный каталог с логами, находит строки с паролями и автоматически затирает их. На мой взгляд, это самое быстрое решение для очистки логов.
Исходный код скрипта выложил на GitHub: https://github.com/ErSilh0x/maxpatrolvm

Немного статистики по количеству найденых строк
Немного статистики по количеству найденых строк
#!/usr/bin/python3

import os
import re
import argparse

#python3 sanitmpvmlogs.py --logs ./troubleshoot_dir

#Arguments
parser = argparse.ArgumentParser(description='Удаление паролей в логах диагностики Maxpatrol VM. Распакуйте архив'
                                             ' с логами, полученными от утилиты сбора логов в каталог'
                                             ' и задайте каталог скрипту.\n'
                                             'Delete passwords in Maxpatrol VM diagnostic logs.')
parser.add_argument('-l', '--logs', required=True, help='Enter directory with logs')
args = parser.parse_args()
catalog = args.logs

pass_patterns = [r'(PgPassword=)\S*',
                r'(PostgrePassword=)\S*'
                r'(ConsulSecret=)\S*',
                r'(EventStorageAuthPassword=)\S*',
                r'(MetricsPassword=)\S*',
                r'(RMQAgentPassword=)\S*',
                r'(RMQPassword=)\S*',
                r'(RMQSiemPassword=)\S*',
                r'(RMQAdminPassword=)\S*',
                r'(RMQSiemPassword=)\S*',
                r'(SmtpPassword=)\S*',
                r'(ClickHouseUserPassword=)\S*',
                r'(GrafanaAdminPassword=)\S*',
                r'(TelemetryInstanceAccessToken=)\S*',
                r'(ProxyPassword=)\S*',
                r'(--httpAuth.password=)\S*',
                r'(METRICS_PASSWORD=)\S*',
                r'(GF_SECURITY_ADMIN_PASSWORD=)\S*',
                r'(LogManager_ClickhousePassword=)\S*',
                r'(VictoriaMetrics_Password=)\S*',
                r'(VictoriaMetricsSettings_BasicAuthPassword=)\S*',
                r'(Database_VictoriaMetricsPassword=)\S*',
                r'(FRONTEND__LOGSPACE__SECURITY__PASSWORD=)\S*',
                r'(FRONTEND__CLICKHOUSE__SECURITY__PASSWORD=)\S*',
                r'(FRONTEND__ELASTICSEARCH__SECURITY__PASSWORD=)\S*',
                r'(RABBITMQ_ADMIN_PASS=)\S*',
                r'(RABBITMQ_SIEM_PASS=)\S*',
                r'(COLLECTOR_METRICS_PASSWORD=)\S*',
                r'(RABBITMQ_AGENT_PASS=)\S*',
                r'(PGADMIN_DEFAULT_PASSWORD=)\S*',
                r'(COLLECTOR_POSTGRESQL_PASSWORD=)\S*',
                r'(POSTGRES_PASSWORD=)\S*',
                r'(CONTENT_POSTGRES_PASSWORD=)\S*',
                r'(CONSUL_SECRET=)\S*',
                r'(HMRabbitSettings_Password=)\S*',
                r'(CONTENT_HM_RMQ_AUTH_PLAIN_PASSWORD=)\S*',
                r'(RabbitMq_Password=)\S*',
                r'(CSPF_HM_RMQ_AUTH_PLAIN_PASSWORD=)\S*',
                r'(RABBITMQ_CORE_PASS=)\S*'
                r'(FlusProxy_ProxyPassword=)\S*',
                r'(Password=)\S*',
                r'(Password:) \S*',
                r'(Password\":) \S*'
                ]

replace = ''
stats = {}

#Count statistics
def count_stat(p):
    if p in stats:
        stats[p] += 1
    else:
        stats[p] = 1

def clean_log(f):
    with open(f, 'r', encoding='utf-8', errors='ignore') as fr:
        lines = fr.readlines()

    with open(f, 'w', encoding='utf-8', newline='\n') as fw:
        for line in lines:
            for pattern in pass_patterns:

                #Search
                if re.search(pattern, line):
                    replace = re.search(pattern, line)
                    line = re.sub(pattern, replace.group(1), line, flags=re.IGNORECASE)
                    count_stat(replace.group(1).rstrip('='))
                    print(line)
                    #print(replace)

            fw.write(line)


for current_dir, subdirs, files in os.walk(catalog):
    #Current Iteration Directory
    #print('Current Directory: ', current_dir)

    #Directories
    '''for dirname in subdirs:
        print('\tSub Directory: ' + dirname)'''

    #Files
    for filename in files:
        fullpath = os.path.join(current_dir, filename)
        print('Parsing: ', fullpath)
        clean_log(fullpath)

#Statistics
for i in stats:
    print('Strings found: ', i, stats[i])

Habrahabr.ru прочитано 7251 раз