Проверка паролей пользователей домена Windows на стойкость к атаке по словарю без компрометации пароля

в 11:07, , рубрики: аудит безопасности, информационная безопасность, пароли

Добрый день, коллеги. Хочу рассказать о полученном мной интересном опыте. Может быть кому-то пригодится.

В современном мире пароли используются повсеместно. На корпоративном компьютере, на личном телефоне и планшете, в почте и т.д. И казалось бы всем уже неоднократно объяснялось, что пароль должен быть стойким. Показывались рекомендации, что пароль не должен содержать личные данные, словарные слова, простые комбинации и т.д. Но тем не менее, еще множество людей продолжают использовать простые пароли. Что является не только нарушением требований безопасности, но представляет серьезную опасность как личным так и корпоративным данным.

Соответственно, возникает задача с помощью подручных средств, без установки сложного ПО, проверить пароли пользователей домена на стойкость к атаке по словарю. При этом, нужно сохранить конфиденциальность паролей. Т.е. чтобы проверяющий не видел пароли в открытом виде, но при этом мог однозначно сказать, что пароль является словарным словом.

В связи с тем, что у нас домен построен на базе Microsoft Windows, для решения этой задачи было решено сравнивать хеши паролей пользователей домена с хешами словаря. В целом такой подход можно применить к хешам из любых систем. Изменятся только методы получения хешей пользователей.

Хеши паролей пользователей в домене Windows можно получить из файла ntds.dit. Но в штатном режиме доступ к нему запрещен системой. С помощью Google была найдена возможность получить копию файла ntds.dit (содержащий в том числе и логины/хеши паролей пользователей домена) стандартными средствами контроллера домена.

Для получения копии ntds.dit используются возможности утилиты ntdsutil.exe. С ее помощью необходимо сделать снимок системы (Volume Shadow Service) и получить копию файла SYSTEM (в нем содержится ключ для извлечения хешей из базы ntds.dit).
Для создания копии выполняем следующие команды:

C:>ntdsutil
ntdsutil: activate instance ntds
ntdsutil: ifm
ifm: create full c:audit
ifm: quit
ntdsutil: quit

В результате работы появляется каталог С:Audit. Внутри каталога присутствуют две папки: Active Directory и registry. Соответственно в первой лежит копия ntds.dit, а во второй копии веток реестра SYSTEM и SECURITY. Данные папки можно скопировать на другой компьютер или оставить на контроллере домена.

Далее необходимо извлечь из ntds.dit логины и пароли пользователей домена. Для этого воспользуемся маленькой утилитой ntds_decrypt. Взять ее можно по адресу . Качаем архив, распаковываем. Получаем два файла. Собственно исполняемый файл и readme с описанием опций.

ntds_decode -s FILE -d FILE -m -i

    -s <FILE> : SYSTEM registry hive
    -d <FILE> : Active Directory database
    -m             : Machines (omitted by default)
    -i                : Inactive, Locked or Disabled accounts (omitted by default)

Для использования утилиты необходимо запустить командную строку с правами «Администратор». На файле cmd.exe кликаем правой кнопкой мыши и выбираем пункт «Запуск от имени Администратора». В командной строке запускаем утилиту:

ntds_decode -s C:AuditregistrySYSTEM -d "C:AuditActive Directoryntds.dit"

Здесь мы запускаем утилиту с основными параметрами (пути до файлов ntds.dit и SYSTEM), т.к. нам не нужны заблокированные или отключенные учетные записи (опция -i), и учетные записи компьютеров (опция -m).

В результате, получаем файл hashes.txt. Формат файла аналогичен формату pwdump и принимается большинством программ брутфорсеров (типа L0phtCrack). Формат файла такой:

<username>:<rid>:<lm hash>:<ntlm hash>:<description>:<home directory>

Собственно в файле hashes.txt нам интересны в первую очередь поля «username» и «ntlm hash».

Хорошо, мы получили исходные данные. Теперь нам нужен словарь. Я взял один из словарей в Интернете на 9 миллионов слов размером 92 Мб. Однако хотелось бы немного его расширить типовыми шаблонами. Например, добавить в конце пару цифр, поменять регистр букв и т.д. Для этой операции замечательно подошел старый добрый John the Ripper. В его функционале есть возможность произвести мутацию словаря по определенным правилам. Я решил не ограничивать возможности JtR и запустил его в стандартном варианте со всеми возможными мутациями.

john --wordlist=9mil.txt --rules dict.txt

После некоторого времени у меня получился файл dict.txt содержащий примерно 131 миллион слов. Теперь для слов нужно получить NTLM хеши. Т.к. сравнивать мы хотели именно хеш с хешем. Для расчета NTLM хешей словаря воспользуемся набором утилит HashManager. Взять его можно по адресу . Кстати в его составе также есть утилиты для мутации словарей. Но нам понадобится замечательная утилита GenerateHashList. Она генерирует хэши для всех паролей в исходном файле.

В распакованном архиве переходим в папку Bonus — GenerateHashList. И запускаем в командной строке bat файл с параметрами:

generate.bat NTLM dict.txt

Через некоторое (продолжительное ) время получим файл dictionary.txt, содержащий хеши словаря. Объем файла примерно 4.3 Гб. Это очень большой файл. И в идеале нужно уже переходить к использованию SQL, но не хотелось, т.к. это не подходило под требования по возможности использовать подручные средства не требующие установки сложного ПО.

Тогда, для начала, решено было использовать возможности Windows. А именно, утилиту FINDSTR. Данная утилита позволяет искать строку в файле и является обновленной версией утилиты FIND. Воспользовавшись Google было найдено решение, которое считывало хеш из файла hashes.txt и искало его в файле dictioanry.txt. Собственно, вот команда:

(for /f "usebackq tokens=1,4 delims==:" %%i in ("hashes.txt") do FINDSTR /I /B "%%j" Dictionary_sort.txt && Echo %%i>>"audit.txt" && Echo %%i %%j>>"audit_full.txt")

Результатом работы утилиты будет два файла:

— audit.txt, содержащий только логины пользователей, чьи хеши паролей были найдены в словаре
— audit_full.txt содержащий кроме логина еще и сам хеш. Это на тот случай если у пользователя возникнут сомнения в том, что хеш был найден в словаре.

Имеющийся файл hashes.txt содержал примерно 20 000 строк. Запустив утилиту find я обнаружил, что поиск одного хеша в словаре занимает около 40 секунд на моем ноутбуке Lenovo X220. Прикинув количество имеющихся строк и среднее время поиска получилось, что на поиск всех хешей уйдет в районе 10 дней. Правда результат можно видеть и в процессе. Так как найденные хеши сразу попадают в файл audit.txt. На более мощном компьютере скорость будет повыше, но не на много. Линейный поиск достаточно трудоемкий. И время поиска напрямую зависит от объема словаря. В общем — это рабочий вариант, но неудобный.

Тогда было решено написать свой скрипт поиска. Я давно интересовался Python и совсем недавно, просматривая в Youtube лекции Гарварда CS50, вспомнил про алгоритм бинарного поиска и решил попробовать реализовать его для поиска хешей в словаре. Ну что, поехали!

Для начала нужно отсортировать словарь. Это необходимо для осуществления бинарного поиска и кроме этого позволит еще больше обеспечить сохранность пароля. Так как после сортировки нельзя будет однозначно сопоставить хеш словаря со словом. Исходя из поставленной цели, использовать подручные средства, воспользуемся утилитой SORT из состава Windows. В командной строке запускаем команду:

sort dictionary.txt > dictionary_sort.txt

Получили отсортированный словарь хешей. Теперь сам скрипт. Я не специалист в Python, я только учусь, поэтому с помощью Google и разных нецензурных слов собрал скрипт и заставил его работать. Естественно скрипт не оптимален и страшно выглядит, но работает. В качестве параметров ему передается путь к файлу hashes.txt, путь к файлу словаря, и путь для записи результата (audit.txt, audit_full.txt).

Формат запуска:

PassAudit.exe -i c:audithashes.txt -d c:auditdictionary_sort.txt -o c:audit

Вот сам скрипт:

import argparse
#Создаем поля для парсера
parser = argparse.ArgumentParser(description='This script testing passwords against dictioanry attack. '
                                             'It takes hashes from file and compares to hashes in dictionary.'
                                             'Accounts with weak password outputs into the file audit.txt '
                                             'and audit_full.txt. You can not get clear password from hash.'
                                             'To get hashes from domain controller you should execute next commands'
                                             'at command prompt on domain controller: '
                                             'cd c:\ -> ntdsutil "activate instance ntds" ifm "create full c:\pentest" quit quit'
                                             ' -> cd c:\password\ -> ntds_decode -s c:\pentest\registry\SYSTEM '
                                             '-d "c:\pentest\Active Directory\ntds.dit"  '
                                             'After that you will get file hashes.txt. ntds_decode.exe you can get here: '
                                             'http://www.insecurety.net/downloads/pwdtools/ntds_decode.zip '
                                             'Dictionary hashes file must be sorted and formated one hash per line. '
                                             'You can make it from any dictionary with John the Ripper and Hash manager '
                                             'or any other programs. Copyright Handy761. 2016')
parser.add_argument('-i', '--input', help='Full path to hashes file', required=True)
parser.add_argument('-d', '--dictionary', help='Full path to dictionary file', required=True)
parser.add_argument('-o', '--output', help='Path to output files', required=True)
args = parser.parse_args()
#Открываем файлы hashes.txt и словарь
f0 = open(args.input)
f = open(args.dictionary)
#Формируем пути для открытия файлов результатов
result_file_path0 = args.output + '\' + 'Audit.txt'
#Открываем файл результата Audit
r0 = open(result_file_path0, "w")
result_file_path1 = args.output + '\' + 'Audit_full.txt'
#Открываем файл результата Audit_full
r = open(result_file_path1, "w")
#Создаем переменную для хеша пользователя
pass_hash = ''
#Запускаем цикл поиска
while True:
#Устанавливаем указатель на конец файла
    f.seek(0, 2)
#Задаем начальный указатель в словаре
    begin = 0
#Задаем конечный указатель в словаре
    end = f.tell()
#Т.к. указатели на байты, а у нас строки то будем конвертировать байты в строки. Задаем номер начальной строки
    lines_begin = 0
#Задаем номер конечной строки. Т.к. длина строки 32 байта плюс перевод строки делим на 34
    lines_end = end / 34
#Считываем из фала Hashes.txt строку и проверяем на конец файла
    pass_line = f0.readline()
    if ("" == pass_line):
        print("file finished")
        break
#Парсим строку на элементы. Разделитель :
    pass_line_parse = pass_line.split(":")
#Выбираем поле ntlm_hash и прибавляем к нему символ перевода строки. Т.к. в словаре строки оканчиваются переводом строки.
    pass_hash = pass_line_parse[3] + 'n'
#Создаем переменную для указателя на середину диапазона поиска
    point1 = 100
#Пока указатель на середину диапазона больше 1, будем искать.
    while (point1 > 1):
#Вычисляем середину диапазона
        point1 = (lines_end - lines_begin) // 2
#Если у нас указатель на начало диапазона строк больше нуля, то полученное значение середины диапазона прибавляем к этому указателю и умножаем на 34 для получения смешения в байтах
        if lines_begin > 0:
            point = (lines_begin + point1) * 34
        else:
            point = point1 * 34
#Устанавливаем указатель в файле словаря на середину диапазона
        f.seek(point, 0)
#Считываем строку из словаря
        line_key = f.readline()
#Сравниваем значение из словаря со значением из файла hashes.txt и если совпало, то выводим в файлы и на экран. Иначе определяем, где искомое значение (выше или ниже), чтобы понять какую половину отбросить и соответственно сдвинуть указатели на начало и конец диапазона. После этого начинаем сначала.
        if pass_hash == line_key:
            print(pass_line_parse[0], line_key)
            r.write(pass_line_parse[0])
            r.write(' ')
            r.write(line_key)
            r0.write(pass_line_parse[0])
            r0.write('n')
            break
        elif pass_hash > line_key:
            begin = f.tell()
            lines_end = end / 34
            lines_begin = begin / 34
        else:
            end = f.tell()
            lines_end = end / 34
            lines_begin = begin / 34
#По окончанию закрываем все файлы
f.close()
f0.close()
r.close()
r0.close()

Для удобства я скомпилировал скрипт в exe-файл. После запуска, на том же самом наборе данных, время перебора составило порядка 3 минут.

Все перечисленные здесь манипуляции по получению хешей паролей пользователей домена и проверке их по словарю достаточно неплохо автоматизируются в скрипт. Генерация хешей словаря операция разовая и производится по необходимости.

Таким образом, можно настроить скрипт, который на регулярной основе будет выгружать хеши паролей пользователей из контроллера домена, проверять их по словарю и отправлять результат в службу технической поддержки для оповещения пользователей о слабом пароле.

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

Автор: Handy761

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js