Дмитрий Неверов

Идём по киберследу: Анализ защищенности Active Directory c помощью утилиты BloodHound

Знак информационной продукции (Федеральный закон № 436-ФЗ от 29.12.2010 г.)

Редактор: Евгения Якимова

Руководитель проекта: Анна Туровская

Арт-директор: Татевик Саркисян

Корректоры: Наташа Казакова, Елена Сербина

Верстка: Белла Руссо

Все права защищены. Данная электронная книга предназначена исключительно для частного использования в личных (некоммерческих) целях. Электронная книга, ее части, фрагменты и элементы, включая текст, изображения и иное, не подлежат копированию и любому другому использованию без разрешения правообладателя. В частности, запрещено такое использование, в результате которого электронная книга, ее часть, фрагмент или элемент станут доступными ограниченному или неопределенному кругу лиц, в том числе посредством сети интернет, независимо от того, будет предоставляться доступ за плату или безвозмездно.

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

© Неверов Д., 2024

© Оформление. ООО «Альпина ПРО», 2025

* * *

Вступление

Утилита BloodHound – популярный инструмент для проведения оценки защищенности Active Directory. BloodHound использует графовую базу данных neo4j и язык запросов Cypher, что позволяет увидеть небезопасные связи между объектами, которые не очевидны при обычном линейном рассмотрении. В книге приводятся интерфейсы BloodHound и базы данных neo4j. Также мы знакомимся с языком запросов Cypher на реальных примерах, а в завершение рассматриваем, как можно расширить функционал BloodHound, чтобы повысить эффективность утилиты.

01. Общая информация и настройка лаборатории

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

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

Графы можно рисовать на бумаге или в приложениях (например, visio), но при использовании этого метода могут быть упущены некоторые связи. Существуют инструменты, которые на основе полученных данных могут показать связи между объектами, даже если эти связи на первый взгляд неочевидны. Среди них Adelante[1], Ping Castle[2] и BloodHound[3]. Именно о BloodHound эта книга.

Что такое BloodHound

BloodHound состоит из трех элементов:

1. BloodHound – это одностраничное веб-приложение, написанное на Java Script; при создании приложения используется Linkurious. Для компиляции используется Electron.

2. Neo4j – база данных для хранения информации, в которой используется язык запросов Cypher.

3. SharpHound – сборщик информации из Active Directory.

BloodHound использует теорию графов, чтобы показать скрытые и часто непреднамеренные связи в среде Active Directory или Azure.

Область применения

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

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

● Защитники могут использовать BloodHound для выявления и устранения тех же последовательностей атак.

● Специалисты по реагированию на инциденты могут использовать BloodHound для проведения расследований и выявления причин инцидента.

● Аудиторы могут проводить проверки на соответствие стандартам безопасности.

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

Настройка лаборатории

Материал в книге подготовлен с использованием среды Windows. Для сбора дополнительной информации используются скрипты, написанные на Powershell, который установлен по умолчанию на Windows и имеет достаточный набор функций для работы с доменом, файловой системой и т. д.

Для изучения материала потребуется тестовый стенд с Active Directory, а также машина, на которой будут анализироваться данные и добавляться функционал к самому BloodHound. В книге домен называется DOMAIN.LOCAL, но это не имеет большого значения, самое главное – менять имя домена на свое при выполнении запросов.

Минимальные требования к стенду – это контроллер домена и компьютер для аналитики и разработки. Для удобства сбора информации и анализа данных машину для BloodHound можно ввести в домен. Наименование машин будет следующим:

● DC – контроллер домена, Windows Server 2019;

● COMP – рабочая станция для BloodHound, Windows 10/11.

Если мощности позволят, необходимо создать еще одну виртуальную машину и добавить ее в домен. Если мощностей нет – тогда просто создать еще один объект, компьютер:

● SERVER – просто сервер, Windows Server 2019.

Установка Active Directory

В первую очередь для установки Active Directory на сервере, который будет являться контроллером домена, необходимо поменять имя на DC и установить статический адрес.

Рис. 1.1. Установка статического адреса для контроллера домена

Запускаем Server Manager и выбираем Add roles and features. Следуем за мастером добавления новой роли. Нажимаем кнопку Next. Предложенные по умолчанию настройки нас будут устраивать, поэтому нажимаем кнопку Next до тех пор, пока не появится окно Select server roles.

Выбираем следующие роли:

● Active Directory Domain Service;

● DNS Server.

Рис. 1.2. Выбор ролей

Далее нажимаем кнопку Next до самого конца, пока кнопка Install не станет активной, и нажимаем на нее (рис. 1.3)

После установки нажимаем на кнопку Close. В верхнем правом углу появился желтый восклицательный знак, который указывает нам, что роли требуют завершения настройки (рис. 1.4).

Нажимаем на ссылку Promote this server to a domain controller. Появляется окно с выбором, куда добавить контроллер домена. Так как у нас ничего нет, выбираем Add a new forest и вводим имя домена domain.local (рис. 1.5).

В следующем окне оставляем все по умолчанию и вводим пароль для восстановления (рис. 1.6).

Рис. 1.3. Подтверждение установки Active Directory

Рис. 1.4. Завершение установки Active Directory

Рис. 1.5. Установка имени домена

Рис. 1.6. Выбор уровня домена

Все остальные настройки оставляем без изменений и нажимаем кнопку Next до того момента, как кнопка Install станет активной. Нажимаем на нее, дожидаемся установки и перезагрузки сервера.

Теперь необходимо ввести машины COMP и SERVER в домен. Но перед этим их нужно переименовать и установить статические IP-адреса.

Совет

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

Создание объектов домена

После создания домена необходимо создать несколько учетных записей:

● admin – пароль Qwerty123, установить бесконечный срок действия пароля, после создания добавить пользователя в группу доменных администраторов;

● user – пароль Qwerty123, установить бесконечный срок действия пароля;

● victim – пароль Qwerty123, установить бесконечный срок действия пароля.

Создать пользователей можно с помощью ADUC или Active Directory Module. В этом случае команды будут следующими:

# Создать пользователя admin

New-ADUser -Name"admin" -SamAccountName "admin" -UserPrincipalName "admin@domain.local" -DisplayName "admin" -GivenName "admin" -AccountPassword (ConvertTo-SecureString "Qwerty123" -AsPlainText -force) -Enabled $true -PasswordNeverExpires $true

# Создать пользователя user

New-ADUser -Name "user" -SamAccountName "user" -UserPrincipalName "user@domain.local" -DisplayName "user" -GivenName "user" -AccountPassword (ConvertTo-SecureString "Qwerty123" -AsPlainText -force) -Enabled $true -PasswordNeverExpires $true

# Создать пользователя victim

New-ADUser -Name "victim" -SamAccountName "victim" -UserPrincipalName "victim@domain.local" -DisplayName "victim" -GivenName "victim" -AccountPassword (ConvertTo-SecureString "Qwerty123" -AsPlainText -force) -Enabled $true -PasswordNeverExpires $true

# Добавить пользователя admin в группу доменных администраторов

Add-ADGroupMember -Identity "Domain Admins" -Members admin

Совет

Для наполнения домена можно воспользоваться скриптом BadBlood[4], но не стоит для первых экспериментов создавать большое количество объектов. Работать с хостом COMP будем от имени учетной записи admin, она входит в группу локальных администраторов как член группы доменных администраторов. В некоторых ситуациях можно использовать другие учетные записи для тестирования различных запросов.

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

● добавим пользователя user в группу локальных администраторов на хосте COMP;

● авторизуемся на хосте SERVER от имени пользователя victim.

Совет

Добавьте пользователя user в группу локальных администраторов на хосте COMP для будущих запросов.

Установка neo4j

Перед использованием BloodHound необходимо подготовить рабочую станцию (в нашем случае это будет компьютер с именем COMP): установить OpenJDK и neo4j.

Установка OpenJDK

База данных neo4j написана на языке Java, и для ее работы требуется OpenJDK. Для версии neo4j 4.4.11, которая будет использоваться на протяжении всей книги, необходимо установить OpenJDK 11. Существует два варианта установки: вручную, где потребуется прописывать все пути самостоятельно, и с помощью winget.

Внимание

Если использовать другие версии OpenJDK, при запуске neo4j возникнет ошибка с сообщением, что данная версия не поддерживается, и рекомендациями по поддерживаемым версиям.

Установка OpenJDK вручную

Для начала нужно скачать скомпилированный дистрибутив OpenJDK 11 с официального сайта[5]. Распакуем архив в директорию C: \Program Files\openjdk\. Теперь необходимо прописать пути в переменных окружения. В командной строке с правами локального администратора нужно выполнить sysdm.cpl, перейти во вкладку «Дополнительно» и нажать на переменные среды.

Рис. 1.7. Свойства системы

Теперь нужно создать новую системную переменную с именем JAVA_HOME и указать путь до распакованного архива (в нашем случае это C:\Program Files\openjdk\jdk-11.0.0.1).

Рис. 1.8. Переменная окружения JAVA_HOME

Также необходимо добавить созданную переменную окружения в PATH.

Рис. 1.9. Добавление в PATH

Установка OpenJDK с помощью winget

Для Windows 10 необходимо установить App Installer из магазина, а в Windows 11 winget установлен по умолчанию. Запустите консоль с правами администратора и выполните команду:

winget install ojdkbuild.openjdk.11.jdk

После установки все пути будут добавлены автоматически.

Установка neo4j

Скачать neo4j можно с официального сайта разработчика[6], для изучения будем использовать бесплатную версию Community Edition. На момент подготовки книги к печати использовалась версия 4.4.11.

Внимание

Бесплатная версия neo4j позволяет создавать только одну базу. Для работы над проектом этого достаточно.

Распаковываем архив с neo4j в удобную директорию, я обычно использую C: \Tools\Neo4j\ (для удобства будем называть эту директорию $NEO4J_HOME), затем запускаем командную строку или powershell с правами администратора (потребуется для установки службы) и выполняем следующую команду:

c: \Tools\Neo4j\bin\neo4j.bat console

Если никаких ошибок не возникнет, то можно увидеть информацию об успешном запуске, как показано ниже.

Рис. 1.10. Первый запуск neo4j

Остановить neo4j можно сочетанием клавиш CTRL + z.

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

C: \Tools\Neo4j\bin\neo4j.bat install-service

C: \Tools\Neo4j\bin\neo4j.bat start

Первая команда установит службу, а вторая ее запустит.

Рис. 1.11. Создание и запуск службы

Проверить статус службы можно с помощью команды:

c: \Tools\Neo4j\bin\neo4j.bat status

Рис. 1.12. Проверка статуса

Смена пароля

После успешного запуска neo4j требует сменить пароль, установленный по умолчанию. Для этого запускаем браузер, переходим по адресу http://localhost:7474 и вводим логин neo4j и пароль neo4j.

Рис. 1.13. Первый запуск браузера neo4j

Выполнив первую аутентификацию, neo4j попросит сменить пароль для пользователя neo4j.

Рис. 1.14. Форма смены пароля

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

Установка BloodHound

Для установки BloodHound не требуется сложных действий. Скачаем необходимую версию (на момент подготовки книги к печати 4.3.1) с официального GitHub[7].

Внимание

Более новая версия BloodHound может потребовать новую версию neo4j.

Разархивируем загруженный архив, перейдем в директорию с полученными из архива файлами и запустим bloodhound.exe. После запуска приложения появится приглашение для ввода пароля.

Рис. 1.15. Форма аутентификации BloodHound

Можно установить флаг Save Password, чтобы не вводить пароль каждый раз.

02. Знакомство с SharpHound, BloodHound и neo4j

Как говорилось ранее, утилита BloodHound состоит из трех частей: непосредственно сама BloodHound, сборщик данных SharpHound и база данных neo4j. В этой части книги мы рассмотрим интерфейсы этих приложений.

SharpHound

SharpHound – это консольное приложение, написанное на C#. SharpHound собирает информацию об объектах домена через запросы LDAP, а также информацию с хостов, такую как членство в локальных группах и сессиях.

Скачать SharpHound можно на официальном GitHub[8]. Разные версии SharpHound генерируют разные форматы данных в JSON, которые BloodHound не всегда принимает. Для BloodHound версии 4.3.1 подойдет SharpHound 1.1.0 или 1.1.1.

Информация

В исходных кодах BloodHound тоже есть SharpHound, он находится в директории Collectors.

Внимание

Стоит упомянуть, что антивирусные решения считают SharpHound вредоносной утилитой.

SharpHound имеет большое количество настроек, которые помогают более гибко собирать информацию с домена и рабочих станций. Не будем останавливаться на всех параметрах запуска утилиты – у нее есть понятная справка, которую можно получить, выполнив команду:

SharpHound.exe -h

В интернете можно найти различные подсказки по методам запуска и некоторую дополнительную информацию по запросам Cypher. Одна из таких подсказок представлена на рисунке 2.1[9].

Для последующего изучения нам потребуются данные из домена. Поэтому в нашей лаборатории запустим SharpHound на хосте COMP от имени доменной учетной записи admin, которая входит в группу доменных администраторов. Это позволит собрать всю полезную информацию из домена и хостов (рис. 2.2).

SharpHound -c All

Рис. 2.1. Подсказка по SharpHound

Рис. 2.2. Результат сбора информации

Внимание

Обновленные операционные системы не позволяют получить информацию о сессиях пользователей (HasSession) и членстве в локальных группах (CanRDP или AdminTo) без прав локального администратора.

К собранной информации вернемся позже, а сейчас рассмотрим интерфейс BloodHound.

Интерфейс BloodHound

После прохождения аутентификации открывается основное окно. Пока данных нет, оно пустое; после загрузки данных BloodHound выполняет запрос, который показывает, какие пользователи входят в группу доменных администраторов.

Интерфейс BloodHound интуитивно понятен. Весь его функционал представлен в одном окне.

Рис. 2.3. Основное окно BloodHound

В левом верхнем углу располагаются форма поиска и информационные вкладки, в правом верхнем углу – меню для настройки интерфейса, загрузки и выгрузки данных. В правом нижнем углу – работа с масштабом, внизу по центру окна располагается форма Raw Query для Cypher-запросов.

Основное поле

В основном поле отображается или узел, или граф на основании запроса Cypher. Узлы можно перемещать по полю. Само поле, узлы и связи имеют контекстное меню, которое можно вызвать правой клавишей мыши.

Форма поиска

Рис. 2.4. Форма поиска

Форма поиска состоит из следующих элементов:

● Дополнительная информация (More Info)

● Поле поиска узлов

● Поиск путей (Pathfinding)

● Возврат (Back)

● Фильтрация типов связей (Filter Edge Types)

Дополнительная информация (More Info)

Этот объект является основным полем для получения информации о свойствах и некоторых связях узлов. При нажатии на кнопку More Info выпадает поле, состоящее из трех элементов.

Рис. 2.5. Вкладка «Дополнительная информация»

Database Info

Вкладка содержит статистические данные по количеству объектов в базе данных, а также некоторые инструменты для работы с базой данных (рис. 2.6).

Рис. 2.6. Статистика по узлам и связям

Ниже статистики находятся элементы для управления данными.

Рис. 2.7. Управление данными

Кратко рассмотрим эти элементы и их функционал:

● Refresh Database Stats – после загрузки данных или добавления информации через запросы Cypher статистика может быть неверной, эта кнопка обновляет статистические данные.

● Warm Up Database – по описанию от разработчиков, при нажатии этой кнопки данные из базы переносятся в оперативную память, что позволяет увеличить скорость работы с ними.

● Clear Sessions – при нажатии этой кнопки удаляются все связи HasSession. Эта функция бывает полезной перед загрузкой новых данных о сессиях пользователей.

● Clear Database – при нажатии этой кнопки удаляются все узлы и связи между ними.

Замечание

Интересно, что при очистке базы данных в браузере neo4j остаются ссылки на свойства объектов и названия связей.

Информация об узле (Node Info)

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

Рис. 2.8. Информация об узле

Разные типы узлов содержат разную информацию. Так, например, групповые политики содержат информацию, к каким пользователям или компьютерам они применяются, а у пользователей и компьютеров отображается информация о правах на другие объекты или правах, связанных с боковым перемещением (рис. 2.9).

Рис. 2.9. Информация о группах и правах

Очень полезна информация о входящих и исходящих правах (ACL) на другие объекты, которые могут быть использованы во время работ.

Рис. 2.10. Входящие и исходящие ACL

Анализ (Analysis)

Вкладка Анализ (Analysis) содержит встроенные в BloodHound полезные запросы, с которых можно начать исследовать инфраструктуру Active Directory (рис. 2.11).

Рис. 2.11. Список встроенных запросов

Информация

Встроенные запросы находятся в файле PrebuildQueries.json в директории src\components\SearchContainer\Tabs.

Ниже находится раздел для добавления собственных Cypher-запросов, который содержит форму для их создания, а также список созданных запросов, разделенный по категориям.

Рис. 2.12. Раздел создания собственных запросов

При нажатии на кнопку в виде карандаша появляется форма для добавления запросов (рис. 2.13).

При переходе к полю выбора категории запроса можно создать собственную категорию или выбрать из существующих.

Информация

Файл customqueries.json с собственными запросами находится в домашней директории пользователя, запустившего BloodHound, с путем \AppData\Roaming\bloodhound\.

Рис. 2.13. Форма добавления собственных запросов

Информация

При вызове формы добавления собственных графов файл создается автоматически, если он еще не создан.

Мы еще вернемся к созданию собственных запросов.

Поле поиска

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

Внимание

По умолчанию поиск по ключевому слову ограничивается 10 узлами. Изменить количество узлов можно в Raw Query, если в настройках включен Query Debug Mode.

Кроме имени для поиска можно использовать свойство objectid.

Форма Поиск путей (Pathfinding)

Функция позволяет строить короткие пути. При нажатии на иконку появляется еще одна форма поиска, в первой строке указывается начальный узел, во второй – конечный (рис. 2.14).

Работает аналогично полю поиска, при нажатии стрелки формируется Cypher-запрос с построением коротких путей от первого узла до конечного.

Рис. 2.14. Форма поиска путей

Информация

BloodHound создает Cypher-запрос со всеми связями, указанными в файле AppContainer.jsx, и с учетом фильтра связей.

Возврат (Back)

Кнопка в виде стрелки влево возвращает предыдущий граф, но без возврата самого запроса в Raw Query.

Фильтр связей (Filter Edge Types)

При нажатии на кнопку в виде воронки появляется новое окно с перечнем доступных связей, при снятии галки связь убирается из запроса при использовании формы Поиск путей (Pathfinding). Каждая группа имеет кнопки включения всех связей (две галки) и отключения всех связей (ластик).

Рис. 2.15. Фильтр связей

Внимание

Фильтр работает только с функцией Поиск путей (Pathfinding).

Меню

Меню состоит из следующих элементов:

● Обновление графа (Refresh)

● Экспорт графа (Export Graph)

● Импорт графа (Import Graph)

● Загрузка данных

● Статус загрузки (View Upload Status)

● Изменение отображения графа (Change Layout Type)

● Настройки

● Информация о программе (About)

Рис. 2.16. Меню

Обновление графа (Refresh)

Нажатие на эту кнопку позволяет вернуть граф в исходное состояние, которое было получено при выполнении запроса.

Экспорт графа (Export Graph)

Кнопка в виде стрелки вверх позволяет экспортировать граф. BloodHound поддерживает два формата – в виде картинки PNG или в формате JSON.

Данный функционал полезен для вставки картинки в отчет, а JSON-файл можно передать для анализа графа.

Импорт графа (Import Graph)

Кнопка в виде стрелки вниз позволяет загрузить ранее сохраненный граф в виде JSON-файла, в результате чего будет отрисован граф, созданный ранее.

Информация

Импортировать граф можно даже в пустую базу или в базу с другими данными.

Загрузка данных (Upload Data)

При нажатии на кнопку в виде стрелки вверх в круге появляется окно, в котором указываются файлы, сгенерированные SharpHound. Обычно SharpHound формирует ZIP-архив, в котором находятся файлы, разделенные по классу объектов домена.

BloodHound автоматически распаковывает архив и преобразовывает JSON в Cypher-запросы, тем самым загружая данные.

Статус загрузки (View Upload Status)

Кнопка в виде списка показывает статус загрузки данных. При нажатии на кнопку Clear Finished статус удаляется.

Изменение отображения графа (Change Layout Type)

Иконка в виде графика позволяет изменять отображение графа. BloodHound предоставляет два вида отображения – Направленное (Directed) и Иерархическое (Hierarchical) (рис. 2.17–2.18).

Изменение типа позволяет лучше рассмотреть граф, и в некоторых случаях BloodHound выстраивает красивые цепочки.

Рис. 2.17. Направленное отображение графа

Рис. 2.18. Иерархическое отображение графа

Настройки (Settings)

При нажатии на кнопку Настройки появляется окно с настройками, их немного.

Рис. 2.19. Окно с настройкам

Кратко рассмотрим представленные настройки.

Порог свертывания узлов (Collapse Threshold). В BloodHound есть механизм, который группирует узлы, имеющие одинаковую связь (только одну) с другим узлом. Это позволяет уменьшить нагрузку на отображение графа, но при этом можно упустить какой-то узел. Применяется к Группам (Group), Контейнерам (Container) и Подразделениям (OU). Данный параметр имеет числовое значение, по умолчанию это 5, что означает – если будет пять узлов или больше, то они объединятся в одну группу. В этой группе появится значок количества объектов внутри группы. Значение 0 отключает эту функцию.

Отображение названия связи (Edge Lable Display) – режим отображения названия связи.

Отображение названия узла (Node Lable Display) – режим отображения названия узла.

Режим отображения узлов и связей может иметь следующие варианты:

● Пороговое отображение (Threshold Display) – название появляется или исчезает при изменении масштаба графа.

● Всегда показывать (Always Display) – название всегда отображается.

● Никогда не показывать (Never Display) – название никогда не отображается.

Вне зависимости от выбранного режима отображения название появится при наведении курсора мыши на узел.

Режим отладки запросов (Query Debug Node) – при включении данной опции все запросы отображаются в Raw Query. Это удобный вариант для обучения или отладки запросов Cypher.

Режим низкой детализации (Low Detail Mode) полезен для слабых машин, так как требует меньше ресурсов для отрисовки узлов. Узлы будут представлять собой только цветные круги.

Темный режим (Dark Mode) включает темный режим интерфейса.

Информация о программе (About)

Кнопка Информация о программе показывает окно с информацией о разработчиках и типе лицензии данной программы (рис. 2.20).

Рис. 2.20. Информация о программе

Масштабирование (Zoom)

Рис. 2.21. Управление масштабом

Тут все просто: плюс увеличивает масштаб графа, минус – уменьшает, а кнопка в виде дома возвращает масштаб в исходное состояние. При этом если узлы перемещались, то они не будут возвращены в исходное состояние, в отличие от действия кнопки Обновление графа (Refresh).

Совет

Колесо мыши также можно использовать для изменения масштаба графа.

Запросы Cypher (Raw Query)

Данная форма позволяет вводить собственные запросы Cypher или отлаживать существующие. Не поддерживает многострочные запросы, в результате чего все многострочные запросы будут преобразованы в одну строку.

Управление узлом (Node Options)

При нажатии правой клавишей мыши на узле появится окно управления данным узлом.

Рис. 2.22. Окно управления узлом

Данное окно содержит несколько интересных функций, которые могут пригодиться во время выполнения работ. Поэтому кратко их рассмотрим.

Установить в качестве начального узла (Set as Starting Node) – при выборе этого варианта в Форме поиска появится имя выбранного узла.

Установить в качестве конечного узла (Set as Ending Node) – при выборе этого варианта откроется форма Поиск путей (Pathfinding) и в конечном узле появится имя выбранного узла.

Построить короткие пути до узла (Shortest Paths to Here) – позволяет получить все короткие пути до выбранного узла.

Построить короткие пути от скомпрометированных узлов до указанного узла (Shortest Paths to Here from Owned) – данный вариант позволяет получить короткие пути от всех скомпрометированных узлов (у которых свойство owned имеет значение TRUE) до выбранного узла.

Изменить свойства узла (Edit Node) – нажатие на эту кнопку открывает окно, позволяющее изменять существующие свойства или добавлять новые.

Рис. 2.23. Форма изменения свойств узла

Пометить узел как скомпрометированный (Mark as Owned) – устанавливает свойству узла owned значение TRUE. Кроме изменения свойства owned на узле в правом нижнем углу появится значок в виде черепа. Если в свойстве owned уже установлено значение TRUE, тогда вариантом будет Снять метку со скомпрометированного узла (Unmark as Owned) и при нажатии на эту кнопку свойству owned узла будет назначено значение FALSE.

Пометить узел как имеющий высокую ценность (Mark as High Value) – функционал похож на предыдущий, устанавливает свойство узла highvalue в значение TRUE. После установки значения в правом верхнем углу узла появится значок в виде бриллианта. Если в свойстве highvalue уже установлено значение TRUE, тогда вариантом будет Снять метку высокой ценности с узла (Unmark as Owned) и при нажатии на эту кнопку свойству highvalue узла будет назначено значение FALSE.

Удалить узел (Delete Node) – функция удаления выбранного узла.

Совет

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

Управление связью (Edge Options)

При нажатии правой клавишей мыши на связь между двумя узлами появляется окно управления данной связью.

Рис. 2.24. Окно управления связью

В этом окне всего два варианта.

Подсказка (Help) – позволяет получить информацию как о самой связи, так и об ее эксплуатации.

● Вкладка General – показывает краткое описание связи и ее возможности.

● Вкладка Abuse – показывает примеры эксплуатации.

● Вкладка OpSec – описывает, как можно обнаружить эксплуатацию связи.

● Вкладка References – предоставляет различные ссылки по данной связи.

Рис. 2.25. Окно подсказки

Удалить связь (Delete Edge) – позволяет удалить выбранную связь между двумя узлами.

Совет

Как и с удалением узла, связь можно пометить как blacklisted и исключить из запроса.

Управление графом (Graph Options)

Если кликнуть правой клавишей мыши на пустом месте основного поля, то появится окно управления графом.

Рис. 2.26. Окно управления графом

Некоторые кнопки мы уже рассматривали в разделе Меню, поэтому рассмотрим только новые.

При нажатии Добавить узел (Add Node) появляется форма, в которой можно указать имя узла и его тип. Все остальные свойства узла будут указываться при вызове Изменить свойства узла (Edit Node) в контекстном меню узла.

Рис. 2.27. Форма добавления нового узла

Разные типы узла требуют своего формата. Если он не соответствует, будет выведена ошибка. Так, например, тип узла User требует, чтобы в имени был знак @, который разделяет имя пользователя и домен.

Информация

Тип узла берется из файла AddNodeModal.jsx, расположенного в src\components\Modals.

При нажатии на Добавить связь (Add Edge) появляется форма, позволяющая добавить связь между двумя узлами. Аналогично с поиском при вводе имени или objectid, будут предложены различные варианты.

Рис. 2.28. Форма добавления связи между двумя узлами

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

Информация

Названия связей берутся из файла AddEdgeModal.jsx, расположенного в src\components\Modals.

Обновить запрос (Refresh Query) – заставляет BloodHound заново выполнить запрос.

Список узлов

Завершим раздел полезной функцией. Если граф очень большой, можно вывести список узлов, нажав пробел, в результате появится окно со списком (рис. 2.29).

Рис. 2.29. Список узлов

Список узлов поддерживает поиск и при вводе букв будет фильтровать данные, но, в отличие от формы поиска, не поддерживает поиск по objectid.

Рядом с каждым именем есть иконка, обозначающая метку узла. В колонке Collapse Info показывается, в какую группу собраны узлы.

Чтобы закрыть список узлов, нужно повторно нажать на пробел.

База данных neo4j

Neo4j – это графовая база данных. Ее модель проста и основана на узлах и связях.

Модель описывается следующим образом:

● Каждый узел может иметь различные связи с другими узлами.

● Каждая связь может переходить от одного узла к другому или к тому же узлу.

● Каждая связь может иметь или не иметь направление.

● Узлы и связи могут иметь свойства, а каждое свойство имеет имя и значение.

Версия neo4j Community Edition имеет очень ограниченный функционал, и из него мы будем использовать только создание резервных копий и восстановление данных из резервных копий.

Работать с информацией из базы данных можно с помощью браузера neo4j, консольного приложения cypher-shell, расположенного в директории $NEO4J_HOME/bin, или с помощью API.

Если необходимо подробнее изучить сам neo4j, то лучше обратиться к специальной литературе, мы же используем возможности языка запросов Cypher.

Интерфейс браузера neo4j

BloodHound выводит результаты запросов в виде графа или отдельных узлов. Свойства узлов можно увидеть во вкладке Node Info, а просмотр свойств связей вообще недоступен. Браузер neo4j в этом плане более гибкий, можно выводить информацию в различных видах. В основном мы будем использовать графы и таблицы.

После смены пароля мы оставили браузер neo4j, теперь стоит к нему вернуться и рассмотреть более детально.

Рис. 2.30. Браузер neo4j

Меню

Слева располагается меню, при нажатии на любой элемент раскрывается дополнительное поле.

Меню достаточно простое и не имеет сложных элементов. Мы рассмотрим только Информацию о базе данных (Database), Избранные запросы (Favorites) и Настройки браузера (Browser Settings), которые могут оказаться полезными. В большинстве случаев с остальными элементами вряд ли придется столкнуться во время использования.

Рис. 2.31. Меню браузера neo4j

Информацию о базе данных (Database)

Первый раздел – Информация о базе данных (Database) – содержит информацию о базе данных и о том, какие элементы входят в нее.

● Используемая база данных (Use database). Как говорилось ранее, в версии community edition есть всего одна база neo4j кроме system.

● Метки узлов (Node Labels) – этот пункт показывает, какие в базе есть метки. Каждая метка интерактивная, можно нажать на любую метку и получить результат. Вывод ограничен 25 узлами.

● Типы связей (Relationship Types) – аналогично с метками, показывает, какие связи есть в базе данных, и тоже интерактивно.

● Названия свойств (Property Keys) – показывает, какие свойства есть у узлов и связей.

● Подключение от имени какой учетной записи (Connected as) – в нашем случае у нас одна учетная запись с именем neo4j без определения ролей.

● DMBS – информация о СУБД.

Избранные запросы (Favorites)

Раздел Избранные запросы (Favorites) позволяет сохранять запросы, которые затем можно использовать. К сожалению, в браузере neo4j есть функция экспорта запросов, но нет импорта. Как добавить и использовать этот функционал, мы рассмотрим дальше.

Настройки браузера (Browser Settings)

Раздел Настройки браузера (Browser Settings) содержит несколько подразделов, которые позволяют сделать работу с браузером удобнее.

Интерфейс пользователя (User Interface)

Рис. 2.32. Интерфейс пользователя

Интерфейс пользователя (User Interface) позволяет:

● изменить тему внешнего вида;

● объединить некоторые символы в один (Code font ligatures) – это касается только стрелок направления связи;

● включить редактор нескольких запросов (Enable multi statement query editor) – браузер neo4j поддерживает выполнение нескольких запросов, которые разделяются точкой с запятой. Отключение этой функции не позволит выполнять такие запросы.

Предпочтения (Preferences)

Рис. 2.33. Предпочтения

Данный раздел содержит две настройки:

● Первоначальная команда для выполнения (Initial command to execute) – команда, которая будет выполняться при запуске браузера neo4j. Команды начинаются с двоеточия. Например, можно вызывать историю команд :history или подсказку по командам :help.

● Таймаут подключения в миллисекундах (Connection timeout (ms)) – определяет, сколько времени браузер будет ожидать при подключении к базе данных.

Рамки с результатами (Result Frames)

Рис. 2.34. Рамки с результатами

В результате запроса Cypher в браузере neo4j создается отдельная рамка со строкой запроса и результатом. Данный раздел определяет следующие параметры:

● Максимальное количество рамок с результатами (Maximum number of result frames) – задает количество новых рамок с запросами, при превышении которого старые будут удаляться.

● Максимальная длина истории (Max history length) – задает количество запросов в истории, после чего они будут затираться. Историю можно вызвать командой :history.

Визуализация графа (Graph Visualization)

Рис. 2.35. Настройки визуализации графа

В данном разделе определяются настройки визуализации графа:

● Количество узлов при отображении графа (Initial Node Display) – определяет максимальное количество узлов при отображении графа.

● Максимальное количество соседей (Max neighbours from vis interaction) – определяет максимальное количество соседних узлов для одного узла.

● Максимальное количество строк для просмотра (Result view max rows) – максимальное количество строк в результате для просмотра в режиме таблицы или текста.

● Максимальное количество полей (Max record fields) – максимальная длина возвращаемого списка. Если длина списка будет превышена, будут выданы только первые записи, а остальные будут отброшены.

● Связи результирующих узлов (Connect result nodes) – отображает все связи между узлами, полученными в результате запроса, даже те, которые не были запрошены.

● Показывать подсказку использования масштабирования (Show zoom interactions hint) – при включенном параметре, если граф получается большим, всплывает окно с подсказкой, как можно пользоваться масштабированием.

Настройки можно посмотреть с помощью команды :config.

Рис. 2.36. Результат выполнения команды :config

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

:config maxHistory:50

Рабочая область

Справа от меню расположена рабочая область, которая состоит из двух элементов:

● главная строка запроса;

● рамки выполненных запросов (рис. 2.37).

Рис. 2.37. Рабочая область

Главная строка запроса

Рис. 2.38. Главная строка запроса

Главная строка запроса состоит из следующих элементов:

● Поле ввода запросов или команд (поддерживает многострочные запросы, для перехода на новую строку используется сочетание клавиш Shift + Enter).

● Кнопка Выполнить (Run) предназначена для выполнения запроса или команды. То же самое можно сделать клавишами Enter или CTRL + Enter для многострочных запросов.

● Кнопка Полноэкранный режим (Fullscreen) разворачивает строку запроса в полноэкранный режим.

● Кнопка Очистить (Clear) очищает строку запроса.

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

Получить историю можно с помощью команды :history.

Рис. 2.39. Вызов истории выполненных запросов

Данная команда покажет последние 30 запросов, данный параметр определяется в настройке Max history length. Нажатие на любой из запросов вставит его в главное окно запросов.

Рамки (Frame)

Каждый выполненный запрос в главной строке запроса открывает отдельную область. По умолчанию neo4j открывает 15 таких областей, но их количество может быть изменено в параметре Maximum number of result frames. Команда :clear удалит все рамки из рабочей области.

Рис. 2.40. Рамка с результатами запроса

Элементы управления рамкой

В правом верхнем углу расположены элементы управления рамкой:

Рис. 2.41. Элементы управления рамкой

● Кнопка Закрепить наверху (Pin at top) позволяет закрепить рамку под основной строкой запроса, все последующие запросы будут располагаться под ней.

● Кнопка Свернуть (Collapse) сворачивает рамку, оставляя только строку запроса.

● Кнопка Полноэкранный режим (Fullscreen) разворачивает рамку на все окно браузера.

● Кнопка Закрыть (Close) закрывает текущую рамку.

Строка запроса

Рис. 2.42. Строка запроса в рамке

Строка запроса работает так же, как и основная. Позволяет работать с графом, не запуская новую рамку.

Следующая кнопка Сохранить как Избранное (Save as Favorite) позволяет добавить запрос в избранные.

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

Рис. 2.43. Добавление запроса в избранное

Заключительный элемент поля запроса – кнопка Экспорт (Exports) – позволяет экспортировать результаты запроса в другие типы файлов. Варианты экспорта зависят от выбора вывода информации:

● граф – в виде картинки в формате SVG или PNG;

● таблица, текст, код – в текстовом виде в формате JSON или CSV.

Формат вывода информации

Слева от поля вывода располагаются варианты отображения информации.

● Граф (Graph) – визуальное построение графа с узлами и связями.

● Таблица (Table) – табличное представление данных при запросе свойств узлов и связей или в виде форматированного JSON при запросе графа.

● Текст (Text) – представление данных в неформатированном виде.

● Код (Code) – представление данных в виде HTTP-запроса и ответа в формате JSON.

Рис. 2.44. Формат вывода результатов

В большинстве случаев браузер neo4j автоматически выбирает режим между графом и текстом.

Поле вывода

Рис. 2.45. Поле вывода информации

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

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

Рис. 2.46. Изменение внешнего вида узла

Масштабирование

При выводе информации в виде графа в правом нижнем углу появляется управление масштабированием графа со следующими функциями:

● увеличение;

● уменьшение;

● выравнивание и отцентровка всего графа.

Рис. 2.47. Управление масштабом графа

Установка плагинов

Neo4j имеет большое количество различных плагинов, которые позволяют улучшить работу с запросами. В книге используется только базовый APOC, поэтому рассмотрим установку плагинов на его примере.

Зайдем на официальный GitHub neo4j с плагинами[10] и скачаем двоичный jar-файл apoc-4.4.0.1-all.jar. Поместим его в папку $NEO4J_HOME/plugins. В моем случае это C: \Tools\Neo4j\plugins.

Информация

Поскольку APOC использует внутренние API neo4j, необходимо использовать правильную версию APOC для вашей установки neo4j. APOC использует согласованную схему управления версиями: <neo4j-версия>.<apoc> версия. Завершающая часть номера версии <apoc> будет увеличиваться с каждым новым выпуском APOC.

Если сейчас мы перезагрузим neo4j и попытаемся воспользоваться процедурами из плагина, то получим ошибку.

Рис. 2.48. Ошибка некорректной настройки процедур

Чтобы исправить ее, нужно открыть файл конфигурации $NEO4J_HOME/conf/neo4j.conf на редактирование. Убрать комментарий со строки dbms.directories.plugins=plugins, затем найти строку dbms.security.procedures.unrestricted=…, после нее добавить строку dbms.security.procedures.unrestricted=algo.*, apoc.* и перезапустить службу:

C: \Tools\Neo4j\bin\neo4j.bat restart

В дальнейшем мы столкнемся с процедурами из этого плагина, а пока перейдем к другой теме.

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

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

Чтобы создать резервную копию, нам необходимо остановить базу данных. Для этого перейдем в директорию $NEO4J_HOME/bin и выполним команду:

.\neo4j.bat stop

Если neo4j запущен в режиме консоли, то можно нажать Сtrl+С или просто закрыть окно.

Теперь создадим резервную копию текущей базы данных, указав имя базы данных neo4j и путь до файла резервной копии:

.\neo4j-admin.bat dump -database="neo4j" -to="c: \tools\neo4j.dump"

После успешного завершения создания резервной копии запустим neo4j:

.\neo4j.bat start

Рис. 2.49. Создание резервной копии

Процедура восстановления похожа на создание копий, изменяется только команда с dump на load. Останавливаем neo4j:

.\neo4j.bat stop

Выполняем команду восстановления данных из дампа, указав полный путь до файла дампа, созданного ранее:

.\neo4j-admin.bat load -database="neo4j" -from="c: \tools\neo4j.dump" -force

Стоит обратить внимание на использование ключа -force: в случае его отсутствия neo4j сообщит, что база данных neo4j уже существует.

Рис. 2.50. Сообщение об ошибке

После успешного восстановления из резервной копии запустим neo4j:

.\neo4j.bat start

Рис. 2.51. Успешное восстановление данных

03. Дрессируем собаку. Язык запросов Cypher

SharpHound, BloodHound и neo4j – это инструменты для сбора, хранения и визуализации информации. Основная магия – это язык запросов Cypher. В интернете можно найти уже готовые запросы и использовать их, но в дополнение к этому хорошо бы разбираться, как они работают, и уметь разрабатывать их самому. В этом разделе мы рассмотрим основные принципы построения запросов и синтаксис языка запросов Cypher.

Внимание

Данный раздел содержит только необходимую для работы с BloodHound информацию. Для более глубокого изучения языка запросов Cypher стоит обратиться к официальной документации[11].

Обычно результаты запросов представляются в виде графов, что удобно для обнаружения связей между объектами. В некоторых случаях данные могут представляться в виде таблиц, что удобно для анализа данных. В BloodHound объекты домена являются вершинами (узлами) графа, а отношения (связи) между этими объектами – ребрами. Также в neo4j узлы графа маркируются меткой (label) по общему принципу. Это позволяет делать выборку только из необходимой группы объектов. Названия связей определяются по типу (type).

Совет

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

Основные принципы

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

Рис. 3.1. Простой граф

Каждый узел в neo4j обладает определенными параметрами:

● Идентификатор (ID) – это обязательный параметр порядкового номера узла.

● Метка (Label) – необязательный, но важный параметр, позволяет объединять узлы по определенному признаку. Например, User или Computer.

● Свойства (Properties) – необязательный параметр, например objectid или distingueshedname. Данный параметр придает каждому узлу индивидуальность. На основании свойств можно сужать выборку данных или получать узлы, имеющие общие свойства.

Связь также имеет свои параметры:

● Идентификатор (ID) – обязательный параметр порядкового номера связи.

● Название (Type) – не обязательный, но важный параметр, позволяет объединять узлы по определенному признаку. Например, MemberOf или GenericAll.

● Свойства (Properties) – необязательный параметр. Данный параметр придает каждой связи индивидуальность. В BloodHound связи обладают разными свойствами, например isacl и isinherited. В дальнейшем мы тоже будем добавлять различные свойства связям.

Общий вид запроса выглядит следующим образом:

Рис. 3.2. Общий вид запроса

В качестве операторов могут выступать:

● CREATE и MERGE – создание нового элемента;

● MATCH – выполнение выборки;

● RETURN – возврат результата (может быть как в начале, так и в конце запроса);

● DELETE – удаление узла или связи (может быть как в начале, так и в конце запроса).

Другие операторы:

● SET и REMOVE – добавление или удаление свойств;

● WHERE – добавление условия к шаблону.

Внимание

Необходимо запомнить, что Cypher чувствителен к регистру. Это важно для названий меток и связей, названий и значений свойств, но операторов это правило не касается.

Возвращаясь к рисунку 3.2, получаем следующий формат: на первом месте идет оператор MATCH, MERGE и др., следующим – начальный узел, затем название связи (опционально), конечный узел, условия (опционально), и завершается запрос оператором RETURN. В некоторых случаях оператор RETURN может опускаться, например при добавлении свойства узлу, или заменяться оператором WITH, если необходимо изменить область видимости.

Стрелки определяют направление и могут быть направлены как слева направо, так и в обратную сторону. Изменение направления стрелок может быть полезно при составлении запроса. Например, мы хотим узнать, на каких компьютерах у пользователя есть сессия. По правилам BloodHound связь HasSession устанавливается от компьютера к пользователю.

Рис. 3.3. Стрелка слева направо

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

Рис. 3.4. Стрелка справа налево

Два шаблона абсолютно идентичны, разница только в интерпретации вопроса и удобстве чтения.

Внимание

В Cypher нет двунаправленных стрелок, но могут быть запросы, где направление не указано.

В браузере neo4j есть справочник по командам и операторам. Получить справку по операторам можно с помощью команды :help:

:help <Оператор>

:help MATCH

Рис. 3.5. Справка по оператору MATCH

Оператор MATCH

Для поиска по базе в neo4j используется оператор MATCH, следом идет шаблон поиска, условия выборки с помощью оператора WHERE, и завершается запрос выводом результатов RETURN.

Рис. 3.6. Оператор MATCH

Наша задача – создать правильный шаблон с добавлением условий. Неверно составленный запрос выдаст неверную информацию.

Рассмотрим простой запрос Cypher, где будут выбираться пользователи, у которых есть права локального администратора на компьютеры:

MATCH (u: User)-[r: AdminTo]->(c: Computer) RETURN u,r,c

Здесь переменным u, r и c будут передаваться результаты выборки, это не обязательно, если не нужно выделять какие-то особые условия, но для возврата данных все равно нужно определить переменную. Такой запрос нельзя профилировать и оптимизировать, а если добавить еще несколько узлов и связей, то перечисление будет требовать дополнительных затрат.

Выходом из этой ситуации будет назначить общую переменную для всего запроса. Запрос приобретет следующий вид.

Рис. 3.7. Добавление общей переменной в запрос

Если выполнить измененный запрос, то результат будет аналогичным:

MATCH p=(:User)-[: AdminTo]->(:Computer) RETURN p

Различные варианты использования оператора RETURN мы рассмотрим позже.

Указание метки будет влиять на скорость выполнения запроса, но и результат может быть другим. Например, в запросе выше упускается тот факт, что группы тоже могут иметь связь CanRDP. Таким образом, в запросе можно опустить указание метки User, и он будет выглядеть следующим образом:

MATCH p=(u)-[r: AdminTo]->(c: Computer) RETURN p

Варианты запросов

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

MATCH (u: User)

MATCH (c: Computer)

MATCH p=(u)-[r: AdminTo]->(c) RETURN p

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

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

MATCH (u: User),

(c: Computer),

p=(u)-[r: AdminTo]->(c) RETURN p

Браузер neo4j пометит, что данный запрос не очень удачный и его обработка может потребовать больше времени и ресурсов, тем не менее задача будет выполнена.

Объединение связей

Запрос может быть сложным: первым шагом мы можем запросить один шаблон, вторым – уже другой. Самый обычный пример для BloodHound – это поиск различных прав через членство в группах.

Рис. 3.8. Двухэтапный запрос

Возьмем все тот же пример с правами локального администратора. Посмотрим, какие группы имеют права локального администратора и какие пользователи входят в эти группы.

Информация

В сложных запросах стоит идти от конечной цели (последнего запроса) к начальному узлу.

MATCH p=(u: User)-[: MemberOf]-(g: Group)-[: AdminTo]->(c: Computer) RETURN p

Внимание

Данный запрос не вернет информацию, если пользователь имеет права локального администратора напрямую.

Мы можем объединить связи в этом запросе, используя логический оператор ИЛИ (представлен как |).

MATCH p=(u: User)-[: MemberOf|AdminTo]->(c: Computer) RETURN p

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

Рис. 3.9. Объединение связей

И тогда наш запрос примет вид

MATCH p=(u: User)-[: MemberOf|AdminTo*1..]->(c: Computer) RETURN p

В предыдущем примере мы использовали запись *1.. – указание количества промежуточных узлов, к которым может применяться шаблон, в данном случае – от одного перехода до бесконечности. Число переходов здесь – это количество различных промежуточных узлов от начального узла до конечного.

Ниже приведены две таблицы, в которых описаны различные варианты синтаксиса.

Без указания типа связи:

Приведу несколько примеров. Все прямые связи между узлами:

MATCH p=(u: User)->(c: Group) RETURN p

MATCH p=(u: User)-[]->(c: Group) RETURN p

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

MATCH p=(u: User)-[*1..2]->(c: Group) RETURN p

С указанием типа связей:

Пример – получить всех пользователей и их членство в группах:

MATCH p=(u: User)-[: MemberOf*1..]->(g: Group) RETURN p

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

MATCH p=(u: User)-[: MemberOf|AdminTo*1..]->(c: Computer) RETURN p

Короткие пути

Построение графа со всеми непрямыми связями потребует большого количества ресурсов, и, скорей всего, база не выдержит и упадет. Для решения этой проблемы можно использовать операторы ShortestPath и AllShortestPaths. Разница между ними в том, что первый находит один короткий путь, а второй – все, при условии, что они существуют.

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

MATCH p=ShortestPath((u: User)-[*1..]->(c: Group)) RETURN p

Совет

Ограничение количества переходов поможет найти самые короткие пути.

В интерфейсе BloodHound в форме Поиск путей (Pathfinding) используется оператор AllShortestPaths, который применяется для поиска коротких путей между двумя указанными узлами.

Оператор OPTIONAL MATCH

Оператор OPTIONAL MATCH работает точно так же, как и MATCH; разница в том, что при использовании OPTIONAL MATCH будет добавлять NULL для недостающих элементов.

Рассмотрим два примера, в которых будем искать локальных администраторов на компьютерах с помощью операторов MATCH и OPTIONAL MATCH.

MATCH (u: User)

MATCH (u)-[r: AdminTo]->(c: Computer)

RETURN u.name, c.name

Рис. 3.10. Результат с MATCH

MATCH (u: User)

OPTIONAL MATCH (u)-[r: AdminTo]-(c: Computer)

RETURN u.name, c.name

Рис. 3.11. Результат с OPTIONAL MATCH

Как можно увидеть на рисунке 3.11, там, где нет прав локального администратора, neo4j выставил null. То же самое в графическом представлении: мы получим всех пользователей, и только у некоторых будет связь AdminTo с компьютерами.

Рис. 3.12. Графическое представление OPTIONAL MATCH

Условия фильтрации запросов

Ранее мы уже использовали фильтры по меткам User и Computer, но только в некоторых случаях этого будет достаточно. Для указания более точных критериев поиска применяется оператор WHERE, который может относиться ко всем свойствам узла и связи.

Обычно оператор WHERE используется после формирования шаблона, но он может быть применен и внутри узла, и все условия будут относиться только к этому узлу:

MATCH (g: Group) WHERE g.name = "DOMAIN ADMINS@DOMAIN.LOCAL" RETURN g.name

MATCH (g: Group WHERE g.name = "DOMAIN ADMINS@DOMAIN.LOCAL") RETURN g.name

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

Внимание

Помним, что Cypher чувствителен к регистру для свойств узла.

Рассмотрим синтаксис использования оператора WHERE на простых примерах.

Оператор сравнения

Для указания точного вхождения используется оператор сравнения =.

Например, найти всех членов группы администраторов домена:

MATCH (u: User)-[r: MemberOf*0..]->(g: Group) g.name = "DOMAIN ADMINS@DOMAIN.LOCAL" RETURN u.name

Кроме строковых значений могут приниматься булевы значения FALSE и TRUE или числовые. Например, найти все незаблокированные учетные записи компьютеров:

MATCH (c: Computer) WHERE c.enabled = TRUE return c

Другой способ использовать точное вхождение – это указать фильтр запроса в узлах. Фильтр задается в фигурных скобках по шаблону <свойство>:<значение>. Например, определить, на каких машинах используется LAPS, можно следующим образом:

MATCH (c: Computer {haslaps: true}) return c

Можно добавить несколько условий. В таком случае они разделяются запятой. Например, выбрать только незаблокированные объекты и компьютеры, на которых используется LAPS.

MATCH (c: Computer {enabled: true, haslaps: true}) return c

Если мы хотим указать метку в операторе WHERE, то вместо знака равенства используется двоеточие.

MATCH (c) WHERE c: User RETURN c

Имя связи тоже можно указывать в WHERE. В данном случае используется type(), так как у связи нет свойства имени.

MATCH p=(c: Computer)-[r]->(u: User) WHERE type(r) = "HasSession" RETURN p

Как и узлы, связь может иметь свойства, и фильтрация по ним будет иметь точно такой же принцип. Например, найти все связи между пользователями и компьютерами, где связь имеет свойство isacl в значении TRUE:

MATCH p=(u: User)-[r]-(c: Computer) WHERE r.isacl = TRUE RETURN p

Оператор «не равно»

Оператор не равно <> противоположен предыдущему оператору. Например, найти все группы, которые не являются контроллерами домена:

MATCH (g: Group) WHERE g.name <> "DOMAIN CONTROLLERS@DOMAIN.LOCAL" RETURN g.name

Операторы арифметического сравнения

Операторы арифметического сравнения > (больше чем), >= (больше или равно), < (меньше чем) и <= (меньше или равно) используются для сравнения чисел. В BloodHound, наверное, нет числовых значений, кроме результатов работы с датами или сложных запросов с использованием подсчета. Примеры работы с датами будут рассмотрены далее.

Оператор NOT

Логический оператор отрицания NOT инвертирует условие. Например, при анализе атрибута description было обнаружено, что некоторые учетные записи имеют описание, связанное с заявками в Help Desk. Заявки имеют идентификационный номер, который администраторы иногда вносят в этот атрибут. Идентификационный номер часто имеет буквенный префикс (например, HDQ), задача состоит в том, чтобы исключить из вывода все такие записи.

MATCH(u: User) WHERE (NOT(u.description CONTAINS "HDQ")) RETURN u.name, u.description

Оператор NOT может применяться в сочетании с другими операторами. Например, с оператором сравнения:

MATCH (g: Group) WHERE NOT g.name = "DOMAIN CONTROLLERS@DOMAIN.LOCAL" RETURN g.name

Оператор IS NULL

Оператор нулевого значения IS NULL проверяет, что свойство не имеет значения. В качестве примера выполним поиск потенциальных пресозданных машин, часто такие машины имеют пустой атрибут operatingsystem:

MATCH (c: Computer) WHERE c.operatingsystem IS NULL RETURN c.name

Оператор IS NOT NULL

Оператор проверки на ненулевое значение IS NOT NULL противоположен предыдущему оператору и проверяет наличие значения в свойстве. По факту это объединение двух операторов – NOT и IS NULL. В качестве примера можно выполнить проверку непустого атрибута description для поиска в нем полезной информации:

MATCH (u: User) WHERE u.description IS NOT NULL RETURN u.name

Оператор CONTAINS

Оператор проверки вхождения CONTAINS проверяет наличие передаваемого значения в свойстве. Например, найти доменные группы, в имени которых встречается слово DBA (администраторы баз данных):

MATCH (g: Group) WHERE g.name CONTAINS "DBA" RETURN g.name

Другой пример – найти все хосты, где установлен Windows Server:

MATCH (c: Computer) WHERE c.operatingsystem CONTAINS 'Server' RETURN c.name

Оператор STARTS WITH

Строковый оператор STARTS WITH позволяет указывать префикс для значения свойства. Имеет смысл использовать для свойств имени – например, найти все учетные записи с префиксом ADM:

MATCH (u: User) WHERE u.name STARTS WITH "ADM" RETURN u.name

Оператор ENDS WITH

Строковый оператор ENDS WITH аналогичен предыдущему, но с указанием постфикса. Этот оператор удобно использовать для указания имени домена или RID. В качестве примера требуется подсчитать, столько учетных записей пользователей есть в домене CHILD.DOMAIN.LOCAL. Запрос в Cypher будет следующим:

MATCH (u: User) WHERE u.name ENDS WITH "CHILD.DOMAIN.LOCAL" RETURN count(u)

Или найти всех пользователей, входящих в группу доменных администраторов. В данном случае будем указывать не имя, а RID, который всегда одинаков и имеет значение 512:

MATCH (u: User)-[r: MemberOf]->(g: Group) WHERE g.objectid ENDS WITH '-512' RETURN u.name, g.name

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

Логические операторы OR и AND

При объединении условий фильтрации запросов можно использовать логические операторы AND и OR. Например, необходимо проверить, какие активные учетные записи будут входить в группу доменных администраторов. Запрос в Cypher будет следующим:

MATCH (u: User)-[r: MemberOf*0..]->(g: Group)

WHERE u.enabled=TRUE

AND g.name = "DOMAIN ADMINS@DOMAIN.LOCAL" RETURN u.name

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

MATCH (u: User)-[r: MemberOf*0..]->(g: Group)

WHERE g.name = "ADMINISTRATORS@DOMAIN.LOCAL" OR g.name = "DOMAIN ADMINS@DOMAIN.LOCAL"

RETURN u.name, g.name

Можно группировать логические операторы с помощью круглых скобок. Например, нужно получить все скомпрометированные объекты домена, и неважно, будет это пользователь или компьютер:

MATCH (n) WHERE n.owned = TRUE AND (n: User OR n: Computer) RETURN n

Оператор EXISTS

Оператор EXISTS может использоваться для проверки существования свойства или связи. Например, получить всех пользователей, у которых есть свойство hasspn, для выполнения техники Kerberoasting. Запрос в Cypher будет выглядеть следующим образом:

MATCH (u: User) WHERE EXISTS(u.password) RETURN u

Информация

Браузер neo4j считает использование EXISTS устаревшим, этот оператор будет удален в будущих версиях neo4j. Вместо EXISTS предлагается использовать IS NULL.

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

MATCH (u: User) WHERE EXISTS {(u)-[r: GenericAll]->(g: Group)} RETURN u.name

Использование регулярных выражений

Оператор регулярных выражений =~ позволяет использовать регулярные выражения в запросах. Например, чтобы не писать полное название группы, можно задать только определенное значение.

MATCH (g: Group) WHERE g.name =~ '(?i)domain user.*' RETURN g

Параметр (?i) сообщает, что регистр не учитывается.

В регулярном выражении можно использовать логическое ИЛИ в виде символа |, чтобы искать по нескольким значениям. Например, нужно посмотреть, есть ли в атрибуте description слова «пароль» или «password», тогда запрос в Cypher может иметь следующий вид:

MATCH(u: User) WHERE u.description =~ "(?i).*(парол|passw).*" RETURN u.name, u.description

Оператор RETURN

Для возврата результатов используется оператор RETURN. В некоторых случаях RETURN может заменяться на WITH, если результаты запроса используются в другом запросе.

Внимание

В одном запросе может быть только один RETURN, за исключением использования операторов CALL или UNION и UNION ALL.

Cypher поддерживает несколько типов возврата результатов. Рассмотрим их.

Примеры вывода результатов

В виде узла

MATCH (d: Domain) RETURN d

В виде графа

MATCH p=(g: Group)-[: GenericAll]-(c: Computer) RETURN p

или

MATCH (g: Group)-[r: GenericAll]-(c: Computer) RETURN g, r, c

Получить все узлы и связи:

MATCH (g: Group)-[r: GenericAll]-(c: Computer) RETURN *

В виде таблицы свойств объектов

MATCH (u: User) RETURN u.name, u.description

По умолчанию название таблицы выводится в виде переменной и названия свойства через точку. Это отображение можно изменить с помощью оператора AS:

MATCH (u: User) RETURN u.name as Name, u.description AS Description

В виде списка

MATCH(u: User) RETURN collect(u.name)

Получить список компьютеров, на которых пользователь имеет права локального администратора:

MATCH (u: User)-[r: MemberOf|AdminTo*1..]->(c: Computer) RETURN u.name, collect(c.name)

Работа со списками будем рассматриваться дальше.

Уникальные записи, оператор DISTINCT

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

MATCH (u: User)-[r: MemberOf|AdminTo*1..]->(c: Computer) RETURN u.name, collect(DISTINCT c.name)

Оператор COUNT

Для подсчета элементов используется оператор COUNT. Например, для подсчета количества пользователей в базе данных будет использоваться следующий Cypher-запрос:

MATCH (u: User) RETURN count(u)

Другой пример – нужно посчитать, какие узлы имеют права Owns и на скольких узлах:

MATCH (u)-[r: Owns]->(c) RETURN u.name, count(c)

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

MATCH p=(g: Group)-[r: GenericAll]->(c: Computer) return count(r)

Еще один интересный запрос:

MATCH p=AllShortestPaths((g: User)-[*1..3]->(c: Computer)) return count(p)

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

Оператор ORDER BY

Оператор ORDER BY позволяет упорядочивать вывод данных, а оператор DESC позволяет изменять направление от большего к меньшему. Например, вывести имена всех пользователей домена в алфавитном порядке:

MATCH (u: User) RETURN u.name ORDER BY u.name

Или узнать, у какого пользователя больше связей AdminTo:

MATCH (u: User)-[r: MemberOf|AdminTo*1..]->(c: Computer) RETURN u.name, count(c) AS Comps ORDER BY Comps DESC

Оператор LIMIT

Оператор LIMIT используется для ограничения количества выводимых элементов. Например, нужно получить первые пять узлов, в которых есть слово ADMIN:

MATCH (n) WHERE n.name CONTAINS "ADMIN" RETURN n LIMIT 5

Как говорилось в разделе описания интерфейса BloodHound, поиск по части названия ограничен 10 узлами, и там как раз используется оператор LIMIT.

Оператор SKIP

Как говорилось ранее, по умолчанию браузер neo4j выводит первую тысячу записей, для просмотра следующих можно изменить параметр Result view max rows в настройках или использовать оператор SKIP. Например, получить список пользователей, у которых атрибут description не пустой, и пропустить первую тысячу записей:

MATCH (u: User) WHERE u.description IS NOT NULL RETURN u.name, u.description SKIP 1000

Случайная выборка

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

MATCH (g: Group) RETURN g, rand() as rand ORDER BY rand LIMIT 5

Операторы UNION и UNION ALL

Операторы UNION и UNION ALL используются для объединения результатов двух разных запросов (рис. 3.13–3.14). Важное условие использования UNION заключается в том, чтобы названия результатов при выводе совпадали.

Например, выведем пользователей и компьютеры двумя разными запросами:

MATCH (m: User) RETURN m

UNION

MATCH (m: Computer) RETURN m

Разница между UNION и UNION ALL в том, что в первом случае повторы опускаются, а во втором они показываются.

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

MATCH (u: User)-[r: MemberOf]->(g: Group) WHERE g.objectid ENDS WITH '-512' RETURN u.name

UNION

MATCH (u: User)-[r: MemberOf]->(g: Group) WHERE g.objectid ENDS WITH '-513' RETURN u.name

Рис. 3.13. Результат использования UNION

MATCH (u: User)-[r: MemberOf]->(g: Group) WHERE g.objectid ENDS WITH '-512' RETURN u.name

UNION ALL

MATCH (u: User)-[r: MemberOf]->(g: Group) WHERE g.objectid ENDS WITH '-513' RETURN u.name

Рис. 3.14. Результат использования UNION ALL

Во втором случае пользователи ADMIN и ADMINISTRATOR в списке появляются дважды.

Объединение путей

Может возникнуть ситуация, в которой нам нужно получить граф, основываясь на результатах другого запроса. В качестве примера выполним два запроса. В первом получим членов группы доменных администраторов, а во втором – на каких компьютерах группа имеет права локального администратора, и результат выведем в одном вызове RETURN:

MATCH p1=(u: User)-[r: MemberOf*1..]->(g: Group {name: "DOMAIN ADMINS@DOMAIN.LOCAL"})

MATCH p2=(g)-[r1:AdminTo]->(c: Computer)

RETURN p1, p2

Оператор WITH

Как говорилось ранее, в некоторых запросах оператор RETURN может быть заменен на WITH. Оператор WITH позволяет передавать результаты из одного запроса в другой, где они используются для определения отправных узлов.

Внимание

Оператор WITH влияет на область видимости переменных. Если не указать их, то Cypher выдаст ошибку, что переменные не определены. Можно использовать * для указания всех переменных в предыдущем запросе.

Оператор WITH можно использовать для манипуляций данными перед выводом информации.

В некоторых ситуациях нам нужно получить граф, основываясь на результатах другого запроса. В качестве примера рассмотрим ситуацию, в которой нам нужно получить компьютеры, к которым не применяется доменная политика DEFAULT DOMAIN CONTROLLERS POLICY.

MATCH (o: GPO)-[r: GPLink*0..]->(n) WHERE o.gpcpath CONTAINS "6AC1786C-016F-11D2–945F-00C04FB984F9"

MATCH (n)-[r1:Contains*1..]->(c: Computer) WITH collect (c.name) AS c1

MATCH (c2:Computer) WHERE NOT c2.name IN c1

RETURN c2.name

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

Совет

В некоторых случаях использование WITH не очевидно, но ошибки, выдаваемые neo4j, могут указать на использование оператора WITH.

Определение переменных

Оператор WITH может применяться для определения переменных, которые затем будут использоваться в условиях. Например:

WITH 'USER@DOMAIN.LOCAL' AS varUser

MATCH (u: User) WHERE u.name = varUser RETURN u

Или в сочетании с регулярными выражениями:

WITH '(?i)domain user.*' as regex

MATCH (g: Group) WHERE g.name =~ regex RETURN g

Добавление и изменение свойств

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

ToBoolean принимает как строковое, так и числовое значение.

У времени есть другие типы данных – Date, Duration, LocalTime, LocalDateTime и Time. Если строка не соответствует формату даты и времени, ее нельзя преобразовать в тип даты или времени простым способом, потребуются манипуляции со строкой для преобразования ее в нужный формат.

WITH [i in split("20/03/2024", "/") | toInteger(i)] AS dateComponents

RETURN date({day: dateComponents[0], month: dateComponents[1], year: dateComponents[2]}) AS date

Работа с большинством типов будет рассмотрена далее.

Получение всех свойств узла

Задача не частая, но можно получить все свойства узла в RETURN, указав properties():

MATCH (u: User) RETURN properties(u) LIMIT 1

Изменение свойств

Для изменения свойства объекта используется оператор SET. Самый простой пример – изменение свойства Compromised. В базе neo4j это свойство называется owned, оно определяется булевым значением false или true. Чтобы изменить это свойство, нужно выполнить запрос Cypher:

MATCH (u: User) WHERE u.name = "USER@DOMAIN.LOCAL" SET u.owned = TRUE RETURN u.name, u.owned

Добавление нового свойства

Новое свойство также добавляется оператором SET. Например, после выполнения техники Kerberoasting был успешно подобран пароль, и эту информацию можно добавить в базу neo4j. Запрос Cypher будет таким:

MATCH (u: User) WHERE u.name = "USER@DOMAIN.LOCAL" SET u.password = "Qwerty123" RETURN u.name, u.password

Интерфейс BloodHound отображает новые свойства в разделе Extra Properties, поэтому изменять код самого BloodHound для отображения новых свойств не требуется.

Внимание

Новые свойства отображаются только после повторного запроса узла.

Можно одновременно добавить или изменить несколько свойств. Свойства и их значения указываются через запятую. Например, можно добавить свойство пароль и сразу указать, что узел скомпрометирован:

MATCH (u: User) WHERE u.name = "USER@DOMAIN.LOCAL" SET u.password = "Qwerty123", u.owned=TRUE RETURN u.name, u.password, u.owned

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

MATCH (u: User) WHERE u.name = "USER@DOMAIN.LOCAL" SET u.‛user password‛ = "Qwerty123" RETURN u.name, u.‛user password‛

Удаление свойств

Если свойство объекта больше не требуется, его можно удалить. Например, во время работы мы обнаружили пароль от учетной записи и решили добавить его в базу, но через какое-то время пользователь поменял свой пароль, и информация стала неактуальной. Поэтому можно удалить свойство password. Для удаления свойства в Cypher используется оператор REMOVE:

MATCH (u: User) WHERE u.name = "USER@DOMAIN.LOCAL" REMOVE u.password RETURN u.name, u.password

Вместо удаления свойства можно установить для него нулевое значение:

MATCH (u: User) WHERE u.name = "USER@DOMAIN.LOCAL" SET u.password = NULL RETURN u.name, u.password

Внимание

Установка нулевого значения может быть не всегда удачным вариантом, если используется оператор EXISTS для проверки наличия свойства.

Добавление метки к узлу

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

Например, мы можем добавить дополнительную метку для всех серверов (имеется в виду Windows Server) и уже оперировать данными на основе этой метки:

MATCH (c: Computer) WHERE c.operatingsystem CONTAINS "Server" SET c: Server

А теперь проверим, какие пользователи и группы имеют права локального администратора на серверах:

MATCH p=(n)-[r: MemberOf|AdminTo*1..]->(m: Server) RETURN p

Рис. 3.15. Результат запроса

В разделе Overview можно заметить, что появилась метка Server.

Можно указать несколько меток, которые также будут разделяться двоеточиями.

Удаление метки

Метка удаляется, так же как и свойство, с помощью оператора REMOVE.

MATCH (n: Server) REMOVE n: Server

Несколько меток удаляются через разделение двоеточием.

Работа со списками

Списки позволяют хранить несколько значений в одном поле. В BloodHound таким полем является serviceprincipalnames.

Выводятся списки в квадратных скобках в кавычках, через запятую: ["var1","var2"]. Как и в других языках, список имеет индекс, и к элементам списка можно обращаться по его индексу.

Метки объектов тоже являются списком. Например, если выполнен запрос для поиска, какие объекты имеют привилегии локального администратора, и нужно понять, что это за объект – пользователь, группа или компьютер, можно выполнить запрос Cypher:

MATCH(n)-[r: AdminTo]->(c: Computer) RETURN n.name, labels(n), c.name

Рис. 3.16. Получение меток

В выводе мы видим, что второе поле содержит список. Первое значение будет компьютером, пользователем или группой, а второе имеет значение Base. Чтобы избавиться от этого значения, можно поставить индекс первого элемента labels(n)[0]. В результате вывод будет более красивым.

Рис. 3.17. Получение метки по индексу

Размер списка

С помощью функции size можно получить размер списка, другими словами, получить количество элементов в списке. Например, получить количество записей в serviceprincipalnames:

MATCH (c: Computer) RETURN c.name, size(c.serviceprincipalnames)

Рис. 3.18. Результаты подсчета элементов в списке

Добавление списка к свойству

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

MATCH (c: Computer {name: "COMP.DOMAIN.LOCAL"}) SET c.ports = ["445", "3389"]

RETURN c.name, c.ports

Рис. 3.19. Результат добавления списка к свойству

Добавление элементов в список

Добавить элемент в список можно с помощью знака плюс (+). Например, добавим еще один порт к нашему списку:

MATCH (c: Computer {name: "COMP.DOMAIN.LOCAL"}) SET c.ports = c.ports + "5985"

RETURN c.name, c.ports

Рис. 3.20. Добавление элемента в список

Аналогично можно добавить в список несколько элементов:

MATCH (c: Computer {name: "COMP.DOMAIN.LOCAL"}) SET c.ports = c.ports + ["5985","5986"]

RETURN c.name, c.ports

Удаление элемента из списка

Возможно, в каких-то ситуациях придется удалить элемент из списка. Существует несколько способов, один из которых – использовать процедуру apoc.coll.remove из плагина APOC. Запрос с использованием процедуры будет следующим:

MATCH (c: Computer {name: "COMP.DOMAIN.LOCAL"})

SET c.ports = apoc.coll.remove(c.ports,2)

RETURN c.name, c.ports

В данном запросе мы удаляем третий элемент из списка, имеющий индекс 2.

Замена элементов в списке

Может потребоваться заменить значение одного элемента другим, это также можно сделать с помощью процедуры apoc.coll.remove из плагина APOC:

MATCH (c: Computer {name: "COMP.DOMAIN.LOCAL"})

SET c.ports = apoc.coll.set(c.ports,1,"5985")

RETURN c.name, c.ports

Здесь мы заменяем порт 3389, который имеет индекс 1, на порт 5985.

Рис. 3.21. Результат замены элемента

Функция COLLECT

В разделе про вывод результатов мы уже видели возможность создания списка. Для этого используется функция collect. Например, нужно собрать всех пользователей, которые являются администраторами домена, в список. Запрос в Cypher будет следующим:

MATCH (u: User)-[r: MemberOf*1..]->(g: Group {name: "DOMAIN ADMINS@DOMAIN.LOCAL"}) return collect(u.name)

Рис. 3.22. Результат в виде списка

Объединение списков

При выводе результатов в виде списков их можно объединить в общий список.

MATCH (u: User) WITH collect (u.name) as col1

MATCH (c: Computer) WITH col1, collect (c.name) AS col2

WITH col1 + col2 AS result

RETURN result

Оператор UNWIND

Для разбора списка используется оператор UNWIND. Чтобы получить данные SPN в виде отдельных записей, можно выполнить запрос Cypher.

MATCH (c: Computer) WHERE c.name CONTAINS "COMP"

UNWIND c.serviceprincipalnames AS spn

RETURN c.name, spn

В результате каждая SPN-запись будет содержаться в отдельной строчке.

Рис. 3.23. Результат использования UNWIND

Для поиска по определенному SPN необходимо разобрать свойство SPN для каждого компьютера и отфильтровать по необходимому значению. Например, нужно найти все доменные компьютеры, у которых в атрибуте serviceprincipalname есть MSSQL. Запрос Cypher будет следующим:

MATCH (c: Computer)

UNWIND c.serviceprincipalnames AS spn

WITH c, spn

WHERE spn CONTAINS "SQL"

RETURN DISTINCT c.name

Оператор IN

Оператор IN проверяет вхождение переданного значения в список. Вернем наши порты в исходное состояние.

MATCH (c: Computer {name: "COMP.DOMAIN.LOCAL"}) SET c.ports = ["445", "3389"]

Теперь получим компьютеры, у которых в свойствах есть порт 445:

MATCH (c: Computer) WHERE '445' IN (c.ports)

RETURN c.name

Неудобство в том, что в примере выше нужно передать полное значение свойства, и тут можно использовать внутреннее условие:

MATCH (c: Computer)

WHERE ANY (x IN c.serviceprincipalnames WHERE x CONTAINS "ldap")

RETURN c.name

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

MATCH(c: Computer)

WHERE ANY (x IN c.serviceprincipalnames WHERE x =~ "(?i).*ldap.*")

RETURN c.name

Для реверсивного поиска (найти все машины, у которых нет SPN ldap) ключевое слово ANY в предыдущем запросе нужно заменить на NOT ANY или NONE.

MATCH (c: Computer)

WHERE NONE (x IN c.serviceprincipalnames WHERE x =~ "(?i).*ldap.*")

RETURN c.name

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

Кроме ANY и NONE есть и другие функции, ALL и SINGLE.

● ANY – возвращает true, если хотя бы один элемент в коллекции соответствует правилу;

● ALL – возвращает true, если все элементы в коллекции соответствуют правилу;

● NONE – возвращает true, если ни один элемент в коллекции не соответствует правилу;

● SINGLE – возвращает true, если правилу соответствует ровно один элемент.

С помощью оператора IN можно удалять элементы из списка:

MATCH (c: Computer {name: "COMP.DOMAIN.LOCAL"}) SET c.ports = [x in c.ports WHERE x <> "5985"]

RETURN c.name, c.ports

Создание черных списков

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

MATCH p=shortestPath((n)-[*1..]->(m: Group {name: "DOMAIN ADMINS@DOMAIN.LOCAL"}))

WHERE NOT n=m RETURN p

Для наглядности выполним его в BloodHound через Raw Query:

Рис. 3.24. Поиск короткого пути

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

MATCH p=shortestPath((n)-[*1..]->(m: Group {name: "DOMAIN ADMINS@DOMAIN.LOCAL"}))

WHERE NOT n=m AND NONE (x IN nodes(p) WHERE x.name = "DC.DOMAIN.LOCAL") RETURN p

Рис. 3.25. Результат запроса без контроллера домена

Чтобы не перечислять все узлы, которые нужно исключить из запроса, можно добавить новое свойство, например blacklisted:

MATCH (c: Computer {name: "DC.DOMAIN.LOCAL"})

SET c.blacklisted = TRUE

RETURN c.name, c.blacklisted

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

MATCH p=shortestPath((n)-[*1..]->(m: Group {name: "DOMAIN ADMINS@DOMAIN.LOCAL"}))

WHERE NOT n=m AND NONE(x IN nodes(p) WHERE x.blacklisted IS NOT NULL)

RETURN p

Результат будет аналогичен изображенному выше. В данном запросе мы проверяем наличие свойства blacklisted, а не его значение. Но если нам потребуется изменить значение blacklisted с true на false, то данный запрос будет выполнен неправильно. Давайте добавим узлу comp.domain.local свойство blacklisted в значении false и повторим запрос:

MATCH (c: Computer {name: "COMP.DOMAIN.LOCAL"})

SET c.blacklisted = FALSE

RETURN c.name, c.blacklisted

Результат будет совершенно другим, а не таким, как мы ожидали.

Рис. 3.26. Результат запроса с разными значениями blacklisted

И теперь придется или удалять свойство для узла со значением true свойства blacklisted, или использовать дополнительную выборку.

MATCH (wo {blacklisted: TRUE})

MATCH p=shortestPath((n)-[*1..]->(m: Group {name: "DOMAIN ADMINS@DOMAIN.LOCAL"}))

WHERE NOT n=m AND NONE(x IN nodes(p) WHERE x=wo)

RETURN p

Первым запросом мы выбираем из базы все узлы со значением false для свойства blacklisted, а вторым уже запрашиваем короткий путь до группы доменных администраторов, исключая из него полученные в первом запросе узлы (рис. 3.27).

Добавлять свойство blacklisted можно и связям. Установим это свойство для связи между comp.domain.local и admin (рис. 3.28):

MATCH p=(c: Computer {name: "COMP.DOMAIN.LOCAL"})-[r: HasSession]-(u: User {name: "ADMIN@DOMAIN.LOCAL"}) SET r.blacklisted = TRUE RETURN p

А теперь выполним запрос, где исключим из результата связи, помеченные свойством blacklisted.

MATCH p=shortestPath((n)-[*1..]->(m: Group {name: "DOMAIN ADMINS@DOMAIN.LOCAL"}))

WHERE NOT n=m AND NONE (x IN relationships(p) WHERE x.blacklisted IS NOT NULL)

RETURN p

Оператор FOREACH

Единственный случай, когда вам нужно выполнить итерацию, – это работа с коллекциями. В предыдущей главе мы видели, что Cypher может использовать разные виды коллекций – коллекции узлов, отношений и свойств. Иногда вам может потребоваться перебрать коллекцию и последовательно выполнить некоторую операцию записи. Для этой цели существует операция FOREACH.

MATCH (u: User) WITH collect(u) AS User

FOREACH (n IN User | SET n.test = TRUE)

Функции HEAD, TAIL и LAST

Функция HEAD возвращает первое значение в списке, TAIL – все остальные (кроме первого), а LAST – последнее значение (рис. 3.29).

MATCH (c: Computer) WITH collect(c.name) AS comps

RETURN HEAD(comps), TAIL(comps), LAST(comps)

Рис. 3.27. Результат исправленного запроса

Рис. 3.28. Результат запроса с blacklisted связью

Рис. 3.29. Результат использования HEAD, TAIL и LAST

Условие «если… то»

Во время анализа и обновления данных может потребоваться условие «если… то». В Cypher нет привычных if и else, тут используется другая конструкция:

CASE WHEN

THEN

ELSE

END

В качестве примера рассмотрим ситуацию, в которой у нас есть учетные записи пользователей, от которых известен пароль, и нам нужно установить для них свойство компрометации узла. В разделе изменения свойств мы уже добавляли и удаляли свойство password. Поэтому для рассмотрения условия «если… то» добавим пароли для двух учетных записей:

MATCH (u: User) WHERE u.name =~ "(ADMIN@).*" SET u.password = "Password1";

MATCH (u: User) WHERE u.name =~ "(USER@).*" SET u.password = "Password2"

Теперь установим свойству owned значение TRUE для всех пользователей, у которых есть ненулевое свойство password, а для остальных пользователей – значение FALSE. Сделаем это с помощью следующего запроса Cypher (рис. 3.30):

MATCH(u: User) WITH *,

CASE WHEN u.password IS NOT NULL

THEN TRUE

ELSE FALSE

END as result

SET u.owned = result

RETURN u.name, u.password, u.owned

В сочетании с оператором IN можно изменить значение элемента в списке:

MATCH (c: Computer) WHERE c.objectid ENDS WITH "-1103"

SET c.ports = [x IN c.ports | CASE WHEN x = "3389" THEN "5985" ELSE x END]

RETURN c.name, c.ports

Рис. 3.30. Результат выполнения запроса

Работа со временем

В neo4j есть встроенные функции для работы со временем. Например:

RETURN date(), datetime(), time()

Существует ряд других типов данных, таких как Time, LocalTime, LocalDateTime и Timestamp. Из функций также можно извлекать отдельные свойства, например год:

RETURN datetime(). year

Чтобы получить разницу между двумя объектами, используется функция duration:

WITH date('2024–03–16') AS date1, date('2024–04–16') AS date2

RETURN duration.between(date1, date2)

Или то же самое, но в днях:

WITH date('2024–03–16') AS date1, date('2024–04–16') AS date2

RETURN duration.inDays(date2, date1). days

Также можно прибавлять или убавлять дни, месяцы, года и т. д.:

WITH date('2024–03–16') AS date1, date('2024–04–16') AS date2

RETURN date1 – duration({days:5}), date2 + duration({months:6})

В пользовательском интерфейсе BloodHound даты отображаются в удобочитаемом формате. Однако в базе данных они хранятся в формате эпохи (unix time), поэтому в запросах со свойствами pwdlastset, lastlogon, lastlogontimestamp, whencreated необходимо использовать формат эпохи.

RETURN datetime(). epochseconds AS epoch

Перевести формат epoch обратно в datetime можно с помощью следующего запроса:

RETURN datetime({epochseconds:1710747114}) AS DateTime

Разница между датами в эпохах может быть получена следующим образом:

RETURN (datetime()-duration({days:90})). epochseconds

Или мы можем просто вычесть количество секунд в 90 днях:

RETURN datetime(). epochseconds – (90*24*60*60) AS Ago90

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

MATCH(u: User) u.pwdlastset IS NOT NULL

RETURN u.name, datetime({epochseconds: toInteger(u.pwdlastset)}) as pwdlastset

В этом запросе мы переводим свойство pwdlastset в тип Long с помощью toInteger, так как сейчас это свойство определяется как Double.

Существует вариант с использованием встроенных процедур:

MATCH(u: User) RETURN u.name, apoc.date.toISO8601(u.pwdlastset,'s') as pwdlastset

Здесь используется процедура apoc.date.toISO8601 для перевода эпохи в читаемое время в формате ISO8601.

Теперь вернемся к разнице и посмотрим, для каких учетных записей пароль устанавливался больше чем 10 дней назад:

MATCH (u: User) WHERE u.pwdlastset < (datetime()-duration({days:10})). epochseconds RETURN u.name

К предыдущему запросу добавим информацию, когда пароль сменился, в днях:

MATCH (u: User) WHERE u.pwdlastset < (datetime()-duration({days:10})). epochseconds

RETURN u.name, duration.inDays(datetime({epochseconds: toInteger(u.pwdlastset)}), datetime()). days

Тот же самый запрос, но с использованием процедуры apoc.date.toISO8601:

MATCH (u: User) WHERE u.pwdlastset < (datetime()-duration({days:10})). epochseconds

RETURN u.name AS Name, duration.inDays(datetime(apoc.date.toISO8601(u.pwdlastset, 's')), datetime()). days as Days

Можно добавить свойство даты и времени компрометации узла вместе с установкой свойства owned в значение TRUE.

Если точное время не требуется и будет достаточно даты, то запрос будет следующим:

MATCH (u: User {name: "USER@DOMAIN.LOCAL"}) SET u.owned = TRUE, u.owneddate = datetime(). epochseconds RETURN u

Или можно установить точную дату и время с использованием формата даты ISO8601:

WITH '2024–04–15T18:33:05' AS owneddate

MATCH (u: User {name: "USER@DOMAIN.LOCAL"})

SET u.owned = TRUE, u.owneddate = datetime(owneddate). epochseconds RETURN u

Информация

Хотя перевод в эпохи необязателен, здесь мы просто придерживаемся общего принципа работы с датами для BloodHound.

Функции для работы со строками

Cypher имеет функции для работы со строками, при обычном использовании BloodHound они не используются, но при разработке собственных утилит и запросов могут потребоваться. Не будем глубоко погружаться в эту тему и быстро пройдемся по основным функциям.

Функция REPLACE

Функция replace замещает один текст в строке другим. Простой пример – заменим в строке одно слово другим:

WITH 'BloodHound' AS Original

RETURN Original, replace(Original, 'Blood', 'Sharp') AS Replaced

Рис. 3.31. Результат замены

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

MATCH (c) SET c.name = replace(c.name, "CORP.LOCAL", "DOMAIN.LOCAL")

Функция SPLIT

Мы уже сталкивались с этой функцией в разделе про работу с датами. Функция split разбивает строку по разделителю и создает список. Например, мы хотим получить свойство name пользователя без указания домена. В качестве разделителя будет выступать @, и запрос будет выглядеть следующим образом:

MATCH (u: User {name: "USER@DOMAIN.LOCAL"})

RETURN u.name AS name, SPLIT(u.name,'@') AS list

Рис. 3.32. Результат использования SPLIT

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

MATCH (u: User {name: "USER@DOMAIN.LOCAL"})

RETURN u.name AS name, split (u.name,'@')[0] AS name_list

Рис. 3.33. Получение имени из списка

Функции TOLOWER и TOUPPER

Функция ToLower переводит строку в строчные буквы, а функция ToUpper – в заглавные. Функция ToLower полезна при формировании данных в табличном режиме для размещения в отчете, так как заглавные буквы смотрятся громоздкими, в то время как строчные буквы более аккуратны. Например, выгрузим имена компьютеров и отобразим их строчными буквами:

MATCH(c: Computer) RETURN toLower(c.name) AS name

Рис. 3.34. Использование функции ToLower

В BloodHound значимые свойства узла пишутся заглавными буквами, и функция ToUpper будет полезна при добавлении свойств к новым узлам. Например, свойство objectid используется для идентификации узла, для пользователей, компьютеров и групп используется sid в качестве идентификатора, для всех остальных объектов используется guid, и свойство objectid записывается заглавными буквами. В neo4j есть функция randomUUID для генерации guid, которую можно использовать при создании новых узлов, и, чтобы следовать стандарту BloodHound, ее надо записать заглавными буквами. Вот так будет выглядеть запрос с использованием функции ToUpper:

RETURN toUpper(randomUUID()) AS objectid

Рис. 3.35. Использование функции ToUpper

Функции LTRIM, RTRIM и TRIM

Функция ltrim удаляет все пробелы слева от строки, rtrim – справа, а trim – одновременно справа и слева:

WITH " BloodHound " as string

RETURN ltrim(string) AS 'Left Trim', trim(string) AS 'Trim', rtrim(string) AS 'Rigth Trim'

Рис. 3.36. Результат использования функции trim

Функции LEFT и RIGHT

Функция left отсчитывает слева указанное количество символов и возвращает их как результат, все остальное отбрасывается. Функция right делает все то же самое, только справа:

WITH "BloodHound" as string

RETURN left(string, 5), right(string,5)

Рис. 3.37. Результат использования функций left и right

Функция SUBSTRING

Функция substring возвращает от строки подстроку с указанной позицией и длиной. Например:

WITH "BloodHound" as string

RETURN string, substring(string, 3,4)

Рис. 3.38. Результат использования функции substring

Объединение строк

Может возникнуть ситуация, когда потребуется объединить две строки в одну. Для этого можно использовать знак +. Например, объединим два слова в одно, тогда запрос Cypher получится следующий:

WITH "Blood" AS s1, "Hound" AS s2

RETURN s1, s2, s1+s2 AS string

Рис. 3.39. Объединение двух слов

Функция TOSTRING

Функция ToString приводит к типу данных string. Рассмотрим простой пример: запросим текущую дату, воспользуемся функцией ToString и посмотрим на результаты. Чтобы получить тип данных, воспользуемся процедурой apoc.meta.type из плагина APOC:

WITH Date() AS date

RETURN date, apoc.meta.type(date), apoc.meta.type(toString(date))

Рис. 3.40. Результат использования функции ToString

Создание и удаление узлов и связей

Базовое использование Bloodhound не подразумевает создания новых элементов, тем не менее это важный функционал для будущих работ. Как ни странно, создание связей требует понимания работы оператора MATCH, условий выборки и установки свойств для узлов, и поэтому в книге этот раздел идет предпоследним в теме языка запросов Cypher.

Перед началом удалим все данные в базе, и на этом этапе можно воспользоваться функционалом BloodHound.

Оператор CREATE

Для создания узлов используется оператор CREATE.

Рис. 3.41. Оператор Create

Запрос на создание узлов состоит из следующих элементов:

● var – переменная;

● Label – метка, необязательный элемент;

● prop1 и prop2 – названия свойств, а val – их значения после двоеточия.

Свойства указываются в фигурных скобках и разделяются запятыми.

Внимание

Если идет цепочка запросов, то переменные должны отличаться или каждый запрос завершается точкой с запятой.

Теперь создадим узел с меткой User и свойством name, которое имеет значение TEST. Запрос Cypher будет следующим:

CREATE (u: User {name: "TEST"})

Если повторить этот запрос, то будет создан еще один пользователь с тем же именем, отличаться будет только id.

Выполним запрос, чтобы получить все созданные узлы.

MATCH (u: User) RETURN u

Рис. 3.42. Два новых пользователя

В инфраструктуре Active Directory нет одинаковых объектов. Даже если имена у них будут совпадать, все равно найдутся отличительные признаки, например SID или Distinguishedname.

Чтобы избежать создания повторных узлов, в ранних версиях использовалась связка CREATE UNIQUE, теперь ту же роль играет оператор MERGE.

Оператор MERGE

Принцип работы оператора MERGE точно такой же, как и у CREATE, с одним условием: он проверяет, существует ли такой элемент в базе.

Рис. 3.43. Оператор MERGE

Если мы выполним следующий запрос Cypher, то ничего не произойдет.

MERGE(u: User {name: "TEST"})

Давайте добавим новые элементы, но введем еще одно свойство sid:

MERGE(m: User {name: "TEST", sid:'U-001'})

MERGE(n: User {name: "TEST", sid:'U-002'})

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

Добавление свойств при создании

Как говорилось выше, свойства передаются в фигурных скобках, но в операторе MERGE есть функция ON CREATE SET, которая добавляет свойства узла при его создании. Однако при этом придется передать индивидуальный идентификатор.

MERGE (m: User {sid: "U-003"})

ON CREATE SET

m.name='TEST'

В BloodHound уникальный идентификатор – свойство sid, и neo4j будет проверять его на совпадение, если такого узла нет, то создаст его, а после создания добавит переданные свойства.

Если мы сейчас выполним запрос пользователей, у которых есть свойство sid, из базы, то получим три узла с одинаковым именем, но с разными идентификаторами.

MATCH(u: User) WHERE u.sid IS NOT NULL RETURN u.name,

u.sid

Рис. 3.44. Проверка создания узлов

Добавление свойств при совпадении

Кроме ON CREATE SET в MERGE есть функция ON MATCH SET, которая добавляет свойства к ранее созданному узлу, попадающему под определенные условия. Если узла не существует, то он будет создан, с указанным идентификатором, но без свойств.

MERGE(m: User {sid: "U-004"})

ON MATCH SET

m.name='TEST'

RETURN m.name, m.sid

Рис. 3.45. Результат выполнения запроса

Но стоит повторить этот запрос, и появится свойство name.

Рис. 3.46. Результат повторного выполнения запроса

С функцией ON MATCH SET нужно быть аккуратным, в случае неверно созданного запроса данные изменятся необратимо. Вот пример неправильного запроса:

MERGE(m: User)

ON MATCH SET

m.sid='U-005'

RETURN m.name, m.sid

При выполнении этого запроса у всех узлов с меткой User свойство sid станет одинаковым.

Оператор DELETE

Узел можно удалить с помощью оператора DELETE.

Рис. 3.47. Оператор DELETE

Запрос выполняется в два этапа: сначала поиск узлов по условиям, затем удаление. Метка и свойства – необязательные параметры, они дают более точный критерий удаления.

Внимание

Нельзя удалить узлы, которые имеют связи с другими узлами. Сначала нужно удалить связь, а потом узел.

Создадим элементы, отличные от созданных ранее. В качестве метки будем использовать Computer.

MERGE (m: Computer {name: "COMP1", sid:'C-001'})

MERGE (n: Computer {name: "COMP2", sid:'C-002'})

MERGE (s: Computer {name: "COMP2", sid:'C-003'})

MERGE (s: Computer {name: "COMP3", sid:'C-004'})

MERGE (s: Computer {name: "COMP4", sid:'C-005'})

Внимание

Мы создали два узла с одинаковым именем, и это не опечатка.

Теперь начнем выполнять удаление по разным критериям. Сначала удалим узел, который имеет определенный идентификатор:

MATCH (m: Computer {sid:'C-001'}) DELETE m

Удаление узлов по общему свойству:

MATCH (m: Computer {name:'COMP2'}) DELETE m

Удаление узлов по общей метке:

MATCH (m: Computer) DELETE m

Удаление всех узлов:

MATCH (m) DELETE m

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

MATCH (n: User {name: "USER@DOMIAN.LOCAL"})

DETACH DELETE n

Создание связей

В предыдущем разделе мы удалили все узлы, и теперь нам нужно создать несколько новых узлов:

MERGE (u1:User {name: "USER"})

MERGE (u2:User {na

me: "ADMIN"})

MERGE (g: Group {name: "GROUP"})

MERGE (c1:Computer {name: "COMP"})

MERGE (c2:Computer {name: "SERVER"})

Проверим, что все узлы создались корректно:

MATCH (n) RETURN n

В результате у нас есть два пользователя, одна группа и два компьютера.

Связь между узлами создается с помощью оператора MERGE. Данный оператор будет проверять существование связи, и в случае ее отсутствия она будет создана. Можно использовать CREATE, но все с тем же условием, что связь должна быть уникальной. Общая схема создания связи будет следующей:

Рис. 3.48. Схема создания связи

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

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

Сейчас будем создавать связи между созданными узлами. Будем использовать принятые в BloodHound типы связей.

Пользователь USER имеет членство в группе GROUP. Запрос Cypher будет следующим:

MATCH (u: User {name: "USER"})

MATCH (g: Group {name: "GROUP"})

MERGE (u)-[r: MemberOf]->(g)

Группа GROUP имеет привилегии локального администратора на компьютере COMP:

MATCH (g: Group {name: "GROUP"})

MATCH (c: Computer {name: "COMP"})

MERGE (g)-[r: AdminTo]->(c)

Пользователь ADMIN имеет права GenericAll на пользователя USER и права локального администратора на компьютере SERVER. Также добавим связи GenericAll свойство isacl в значении TRUE:

MATCH (u1:User {name: "ADMIN"})

MATCH (u2:User {name: "USER"})

MATCH (c: Computer {name: "SERVER"})

MERGE (u1)-[r1:GenericAll]->(u2) SET r1.isacl = TRUE

MERGE (u1)-[r2:AdminTo]->(c)

Теперь можно выполнить проверку всего, что у нас получилось. Если в настройках браузера neo4j установлен флаг Connect result nodes, то будет достаточно выполнить следующий запрос:

MATCH (n) RETURN n

Или создать полный запрос:

MATCH p=(n)-[r: AdminTo|MemberOf|GenericAll]->(m) RETURN p

Рис. 3.49. Результат добавления связей между узлами

Как можно увидеть, задача не очень сложная, но при наличии большого количества объектов и связей она будет кропотливой. Любая ошибка приведет к неверному толкованию.

Удаление связей

Как и узлы, связи удаляются с помощью оператора DELETE.

Рис. 3.50. Схема удаления связей

Для начала выбирается шаблон поиска, а затем выполняется удаление связи между узлами.

Совет

Для корректного удаления связи между узлами сначала можно выполнить RETURN, а затем уже DELETE.

Рассмотрим несколько примеров. Удаление одной связи по ее наименованию:

MATCH (m)-[r: MemberOf]->(n) DELETE r

Удаление двух и более связей по их наименованию:

MATCH (m)-[r: MemberOf|GenericAll]->(n) DELETE r

Удаление связи по ее свойству:

MATCH (m)-[r]->(n) WHERE r.isacl = TRUE DELETE r

Удаление любых связей между двумя узлами:

MATCH (m)-[r]->(n) DELETE r

Удаление цепочки узлов и связей

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

MATCH p=(n)-[*1..]-(m: Computer {name: "COMP"}) DELETE p

Очистка базы

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

MATCH (n)

DETACH DELETE n

Загрузка информации в базу данных

Перед использованием информации ее необходимо загрузить в базу данных. BloodHound использует результаты работы SharpHound, которые формируются в виде JSON определенного типа, поэтому разные версии BloodHound не принимают результаты от других версий SharpHound.

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

Загрузка данных через CSV-файл

Neo4j позволяет загружать информацию из CSV-файла. В качестве примера выгрузим из Active Directory всех пользователей с помощью Active Directory Module и сохраним их в формате csv. Для демонстрации ограничим вывод несколькими атрибутами: samaccountname, displayname, objectssid, userprincipalname, enabled и domainname.

Выполним следующую команду на контроллере домена:

Import-Module ActiveDirectory

$domain = "domain.local"

Get-ADUser -Filter * -Properties *|ForEach-Object {$_| Add-Member -NotePropertyName domainname -NotePropertyValue $domain -Force;$_}|select samaccountname, displayname, objectsid, userprincipalname, enabled, domainname|Export-Csv -Path C: \Tools\users.csv -NoTypeInformation

Очистим базу через браузер neo4j:

MATCH (n)

DETACH DELETE n

Помещаем файл users.csv в директорию $NEO4J_HOME/Import, в моем случае это c: \Tools\neo4j\Import.

Информация

Можно поменять папку импорта в настройках neo4j. Файл conf/neo4j.conf, параметр "dbms.directories.import=import". И перезагрузить neo4j.

Информация

Neo4j также поддерживает загрузку файлов по протоколам HTTP, HTTPS и FTP.

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

// Загружаем данные из CSV-файла с заголовками

LOAD CSV WITH HEADERS

// Указываем название файла, все данные будут храниться в переменной line

FROM 'file:///users.csv' as line

RETURN

// Проверяем, есть ли у пользователя атрибут UserPrincipalName

CASE WHEN line.userprincipalname IS NULL

//Если нет, то формируем его из samaccountname и имени домена

THEN toUpper(line.samaccountname) + '@' + toUpper(line. domainname)

// Если есть, то просто записываем его строчными буквами,

// вся проверка передается в переменную name

ELSE toUpper(line.userprincipalname) END as name,

// Получаем SID

line.objectsid as objectid,

// Получаем samaccountname

line.samaccountname as samaccountname,

// Получаем значение атрибута displayname

line.displayname as displayname,

// Записываем домен большими буквами

toUpper(line.domainname) as domain,

// Переводим строковое значение в булево

toBoolean(line.enabled) as enabled

В результате получаем таблицу (рис. 3.51).

Рис. 3.51. Результат разбора CSV-файла

Внимательно проверяем полученные результаты и, если они нас устраивают, переходим непосредственно к загрузке данных.

// Автоматическое подгружение данных в базу

:auto USING PERIODIC COMMIT

LOAD CSV WITH HEADERS

FROM 'file:///users.csv' as line

// Создаем узел пользователя с его SID

MERGE (u: User {objectid: line.objectsid})

// После создания узла добавляем свойства

ON CREATE SET

u.name = CASE WHEN line.userprincipalname IS NULL

THEN toUpper(line.samaccountname) + '@' + toUpper(line.domainname)

ELSE toUpper(line.userprincipalname) END,

u.samaccountname = line.samaccountname,

u.displayname = line.displayname,

u.domain = toUpper(line.domainname),

u.enabled = toBoolean(line.еnabled)

Проверим, что все данные были загружены корректно, и выполним следующий запрос в Raw Query BloodHound.

MATCH(u: User) RETURN u

Рис. 3.52. Результат добавления пользователей

Создание утилиты для загрузки данных

В предыдущем разделе мы рассмотрели вариант загрузки данных через CSV-файл, но этот метод будет требовать дополнительной работы по анализу файла, особенно когда понадобится создавать связи.

В качестве вывода данных из утилит можно сразу сформировать Cypher-запрос, который затем вставить в браузер neo4j. При большом количестве строк данный метод может быть неудобным. Однако в neo4j есть API, которое можно использовать для выполнения запросов. Воспользуемся им для загрузки наших данных и создадим скрипт (neo4j_uploaddata.ps1) для автоматизации процесса.

function UploadData {

[CmdletBinding()]

Param (

[Parameter (Mandatory=$false, Position=0)]

[string]

$file

)

# Проверка указания файла

if ($file -eq " ")

{

Write-Host -ForegroundColor Red "The filename is required!"

Break

}

# Параметры подключения к базе данных

$IP, $Port, $DB = 'localhost', '7474', 'neo4j'

$neo4j_user = 'neo4j'

$neo4j_password = 'BloodHound'

# Формирование строки для подключения к API

$uri = "http://${IP}:$Port/db/$DB/tx/commit"

# Создание токена для подключения к API

$token = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("${neo4j_user}:$neo4j_password"))

# Загружаем данные из файла с результатами

$data = Get-Content $file

# Подсчет количества строк

Write-Host -ForegroundColor Yellow "Total lines is: " $data.Length

foreach($line in $data)

{

# Формируем POST-запрос

$query = "$line"

# Формируем HTTP-заголовки

$headers = @{

'Accept' = 'application/json; charset=UTF-8'

'Content-Type' = 'application/json'

'Authorization' = "Basic $token"

}

# Формируем тело запроса и конвертируем его в JSON

$body = @{statements=@(@{statement=$query})} | convertto-Json

# Выполняем HTTP-запрос

$Call = Invoke-RestMethod -Uri $uri -Method POST -Headers $headers -body $body

# В случае возникновения ошибок выводим их в консоль

if($Call.Errors){

Write-Error $Call.errors.Message

}

}

Write-Host -ForegroundColor Green "Upload is completed"

}

Теперь заменим нашу предыдущую однострочную команду для выгрузки данных из Active Directory на простой скрипт (GetADData.ps1), но вместо CSV-файла будем формировать Cypher-запросы, которые будут записываться в файл.

Import-Module ActiveDirectory

$domain = "domain.local"

[string]$OutFile = "users.txt"

# Получить всех пользователей домена

$t = Get-ADUser -Filter * -Properties *

# В цикле разобрать атрибуты пользователей

foreach($i in $t)

{

$sid = $i.objectSid.Value

$samaccountname = $i.samaccountname

$displayname = $i.displayname

$enabled = $i.enabled

# Проверка, пустой ли userprincipalname

if($i.userprincipalname)

{

$name = $i.userprincipalname

}

else

{

$name = $i.samaccountname + "@" + $domain

}

# Сформировать строку запроса Cypher и записать ее в файл

Add-Content $OutFile "MERGE (u: User {objectid:'$sid'}) ON CREATE SET u.name=toUpper('$name'), u.samaccountname='$samaccountname', u.displayname='$displayname', u.domain = toUpper('$domain'), u.enabled = $enabled;"

}

Если присмотреться внимательно, то Cypher-запрос точно такой же, как и при загрузке данных из CSV.

Запустим скрипт GetADData.ps1 для получения информации из Active Directory на контроллере домена. Следующим шагом очистим базу через браузер neo4j.

MATCH (n)

DETACH DELETE n

Перенесем файл с контроллера домена и запустим наш загрузчик данных.

..\neo4j_uploaddata.ps1

UploadData -file.\users.txt

Рис. 3.53. Загрузка данных с помощью скрипта

И теперь посмотрим на результаты нашей работы. Запрос выполним в BloodHound в Raw Query.

MATCH (u: User) RETURN u

Рис. 3.54. Результат загрузки данных

04. Учим старую собаку новым трюкам

Стандартной сборки BloodHound достаточно для выполнения работ, но чем больше вы будете работать с этим инструментом, тем чаще вас станут посещать мысли о том, что можно расширить функционал, который поможет более эффективно выполнять работу.

Теперь, когда есть понимание интерфейса BloodHound и языка запросов Cypher, можно перейти к добавлению новых меток, связей и их свойств.

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

Настройка окружения и проверка сборки

Чтобы приступить к добавлению функционала, необходимо настроить окружение и проверить, что все работает корректно.

Настройка окружения

В первую очередь необходимо установить node.js. Скачаем дистрибутив node.js с официального сайта[12] и запустим установщик. Во время установки будет предложено установить Chocolately, это избавит нас от установки всех зависимостей вручную.

Рис. 4.1. Установка зависимостей node.js

Запустим командную строку с правами администратора и установим electron-builder:

npm install -g electron-packager

Обычно сборка приложения electron выполняется так:

electron-packager. app -platform win32 -arch x64 -out dist/

Дополнительно установим git[13] для загрузки исходников BloodHound или с помощью winget:

winget install Git.Git

Первая сборка BloodHound

Можно приступить к первой сборке приложения BloodHound. Запустим powershell и перейдем в рабочую директорию c: \Tools\. Загрузим исходники с официального GitHub[14], в книге используется версия 4.3.1.

git clone https://github.com/BloodHoundAD/BloodHound

Перейдем в директорию BloodHound и установим все необходимые зависимости для сборки. Для этого необходимо выполнить следующую команду:

npm install -force

Выполним тестовую сборку программы:

npm run build: win32

Если никаких ошибок не возникнет, то мы получим собранную версию BloodHound (рис. 4.2).

Совет

Если не требуются версии win32 и arm, то в файле package.json можно установить архитектуру только x64, для этого изменим параметр arch на -arch=x64.

Рис. 4.2. Результат сборки приложения

Рис. 4.3. Ошибка при сборке приложения

Теперь перейдем в директорию BloodHound-win32-x64 и запустим bloodhound.exe. Если все было сделано правильно, BloodHound запустится, и мы увидим или окно авторизации, или основное поле.

Ошибки

При первой сборке может возникнуть ошибка error:0308010C: digital envelope routines::unsupported, связанная с библиотеками SSL (рис. 4.3).

Для решения этой проблемы нужно установить переменную окружения для node.js для поддержки старых алгоритмов шифрования:

$env: NODE_OPTIONS = "-openssl-legacy-provider"

Изменение информации о программе (About)

Начнем с простых вещей и для начала добавим информацию о себе в About.

С помощью редактора откроем файл About.jsx, который находится в \src\components\Modals, и добавим перед лицензией строчку со своей информацией.

…

@harmj0y

</a>

</h5>

<h5>

Modified by:{'Dmitry Neverov'}

</h5>

<br />

<h5>LICENSE</h5>

<div className={styles.scroll}>{data}</div>

…

Сохраним файл и соберем приложение:

npm run build: win32

После сборки запустим BloodHound и нажмем на кнопку i в меню. Теперь можно увидеть собственное имя (рис. 4.4).

Рис. 4.4. Изменение About

Изменение запроса в Shortest Path from Owned Principals

Использование установленного флага Debug Mode позволяет отслеживать запросы Cypher. Однажды я обратил внимание на запрос Shortest Path from Owned Principals, который устанавливал конечными точками только узлы с меткой Computer. Посмотрим на этот запрос в исходном коде.

Откроем в редакторе файл PrebuildQueries.json, который находится в директории \src\components\SearchContainer\Tabs\, и найдем запрос Shortest Path from Owned Principals (рис. 4.5).

Нас интересует последний запрос в этом блоке:

MATCH p=shortestPath((a {name:$result})-[:{}*1..]->(b: Computer)) WHERE NOT a=b RETURN p

Рис. 4.5. Пресозданный запрос

Можно убедиться, что конечными узлами будут все объекты с меткой Computer. Этот запрос не очень правильный, так как у скомпрометированного ранее узла могут быть связи с другими узлами кроме компьютеров. Чтобы исправить эту ситуацию, удалим метку Computer из запроса.

MATCH p=shortestPath((a {name:$result})-[:{}*1..]->(b)) WHERE NOT a=b RETURN p

Сохраняем измененный файл и собираем приложение:

npm run build: win32

Запускаем обновленную версию BloodHound. Проверяем, что в настройках установлен флаг Debug Mode. Теперь выбираем любого пользователя, правой клавишей мыши вызываем контекстное меню и нажимаем на Set User as Owned. Переходим во вкладку Analysis и нажимаем на Shortest Path from Owned Principals. Даже если нет результатов в Raw Query, можно увидеть, что конечными узлами являются все другие узлы.

Добавление собственных запросов

Продолжаем работу с запросами. Со временем при частом использовании BloodHound будут накапливаться полезные запросы, некоторые из них можно добавить в BloodHound, чтобы каждый раз не вводить их вручную или не копировать.

Не все запросы Cypher можно использовать в BloodHound, например, некоторые данные мы получаем в виде таблиц, а BloodHound не поддерживает этот функционал.

В качестве примера будем выполнять поиск компьютеров, которые имеют привилегии локального администратора на других компьютерах. Мы рассмотрим два варианта – добавление запроса через форму и изменение исходного кода.

Настройка лаборатории

Для начала проверим, существуют ли такие связи. Запрос Cypher будет следующим:

MATCH p=(n: Computer)-[: MemberOf|AdminTo*1..]->(m: Computer) RETURN p

Если результатов нет, то создадим такие связи, одну прямую, другую – через доменную группу Domain Computers:

MATCH (c1:Computer {name: "DC.DOMAIN.LOCAL"})

MATCH (c2:Computer {name: "COMP.DOMAIN.LOCAL"})

MERGE (c1)-[: AdminTo]-(c2)

MATCH (g: Group {name: "DOMAIN COMPUTERS@DOMAIN.LOCAL"})

MATCH (c: Computer {name: "COMP.DOMAIN.LOCAL"})

MERGE (g)-[: AdminTo]-(c)

Повторим ранее созданный запрос и на этот раз получим результат.

Рис. 4.6. Компьютеры с правами локального администратора

Добавление собственных запросов через форму

Сначала добавим собственный запрос с помощью встроенной формы. При описании интерфейса BloodHound мы уже рассматривали эту форму. Открываем вкладку Analysis, пролистываем ее до Custom Queries и нажимаем на кнопку в виде карандаша. Заполняем форму, как на рисунке 4.7.

Рис. 4.7. Форма добавления собственных запросов

Запрос Cypher будет таким же, как и при настройке лаборатории. Название категории можно выбрать из списка или добавить свою. Название запроса можно сделать любым. Нажимаем на стрелку, чтобы выполнить запрос, и, если результат нас устраивает, сохраняем (Save).

Рис. 4.8. Результат добавления запроса

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

Рис. 4.9. Результат выполнения запроса

Если открыть файл customqueries.json, который находится в C: \Users\%USERNAME%\AppData\Roaming\bloodhound\, можно увидеть результат добавления.

Рис. 4.10. Файл customqueries.json

На мой взгляд, такой метод полезен при повторном использовании запросов в рамках одного конкретного проекта. Чтобы поделиться своими запросами с коллегами, нужно переносить файл customqueries.json на другой компьютер или в профиль другого пользователя. Поэтому самые популярные запросы стоит записать прямо в приложение.

Добавление собственных запросов в код приложения

Ранее мы уже изменяли файл PrebuildQueries.json, теперь добавим свой запрос, который получили в предыдущей части. Если изучить внимательно этот файл, то можно обнаружить, что все запросы похожи друг на друга. Обычно они состоят из двух-трех частей, первая определяет домен, для которого будет осуществлена выборка, вторая – уже непосредственно сам запрос с учетом домена. В некоторых запросах второй частью является выбор пользователя.

Откроем в редакторе файл PrebuildQueries.json, который находится в директории src\components\SearchContainer\Tabs\, найдем запрос Find Domain Admin Logons to non-Domain Controllers и после всего блока запроса добавим свой код.

{

"name": "Find All Computers where Computers are Local Admin",

"category": "Dangerous Privileges",

"queryList":[

{

"final":false,

"title":"Select source domain…",

"query":"MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": "MATCH p=(n: Computer)-[: MemberOf|AdminTo*1..]->(m: Computer) WHERE n.domain = $result RETURN p",

"allowCollapse":true

}

]

},

По факту это тот же код, который мы получили в customqueries.json.

Совет

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

Сохраняем измененный файл и собираем приложение.

npm run build: win32

Запускаем обновленную версию приложения, переходим во вкладку Analysis и находим наш добавленный запрос.

Рис. 4.11. Новый запрос во вкладке Analysis

Нажмем на добавленный запрос, получим результат:

Рис. 4.12. Результат добавления запроса

Теперь этот запрос всегда будет присутствовать в BloodHound вне зависимости от пользователя, который запускает приложение.

Локальная учетная запись с правами администратора

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

● Групповые политики

● Извлечение данных из LSA

● Общие файловые ресурсы

● Почтовые архивы

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

Создание новой метки

Создадим новую метку LocalUser и определим, что она будет обладать следующими свойствами: name, password и hash. Если свойства name, password и hash мы можем получить непосредственно из локальной учетной записи, то с objectid придется определяться отдельно. Для доменных учетных записей пользователей BloodHound в качестве objectid используется SID, но для локальных учетных записей на каждом компьютере будет свой SID. Поэтому в качестве значения objectid будем генерировать значение guid.

Задачу можно решить двумя способами: воспользоваться внешним источником или использовать плагины neo4j.

Для первого способа можно использовать powershell:

[guid]::NewGuid()

А второй способ генерации guid – это использовать функцию randomUUID(). Мы уже сталкивались с этой функцией при изучении Cypher. Запрос на генерацию guid с помощью этой функции будет следующий:

RETURN randomUUID() AS objectid;

Соберем все вместе и создадим новый узел с меткой LocalUser с помощью Cypher:

MERGE (m: LocalUser {name: "LADMIN", objectid: toUpper(randomUUID()), password: "Qwerty123", hash: ToUpper("329b074c0058ccf1ba2e4705382963ff")})

Проверим, что создался новый узел с указанными параметрами:

MATCH (l: LocalUser) RETURN l

Рис. 4.13. Локальный пользователь

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

Отображение метки в BloodHound

Переходим в директорию src и открываем файл index.js на редактирование. Добавим информацию о том, как новая метка будет отображаться на графе. Находим строку global.AppStore и вставляем следующий код после блока GPO:

…

global.appStore = {

dagre: true,

…

GPO:{

font: "'Font Awesome 5 Free'",

content:'\uF03A',

scale:1.25,

color:'#7F72FD',

},

LocalUser:{

font: "'Font Awesome 5 Free'",

content:'\uf007',

scale:1.5,

color:'#E69717',

},

…

Дальше ищем строку lowResPalette и добавляем название метки и цвет. Этот параметр отвечает за отображение узлов в низком разрешении.

…

lowResPalette:{

colorScheme:{

User:'#17E625',

Computer:'#E67873',

Group:'#DBE617',

Domain:'#17E6B9',

OU:'#FFAA00',

GPO:'#7F72FD',

Base:'#E6E600',

LocalUser:'#E69717',

},

…

Сохраняем измененный файл.

Теперь переходим в директорию src\js и открываем на редактирование файл utils.js. В самом начале, в разделе const labels = [, добавляем новую метку после Domain.

const labels = [

'Base',

'Container',

'OU',

'GPO',

'User',

'Computer',

'Group',

'Domain',

'LocalUser',

…

Находим строку export async function setSchema() и в массив label добавляем название новой метки.

…

export async function setSchema() {

const luceneIndexProvider = "lucene+native-3.0"

let labels = ["User", "Group", "Computer", "GPO", "OU",

"Domain", "Container", "Base", "LocalUser", …

Сохраняем измененный файл.

Теперь нужно добавить метку, чтобы она отображалась на графе. Для этого переходим в директорию components и открываем файл Graph.jsx. Находим строку switch (type) и добавляем в нее код:

…

switch (type) {

…

case 'OU':

node.type_ou = true;

break;

case 'LocalUser':

node.type_localuser = true;

break;

}

…

Сохраняем измененный файл.

Для отображения метки в строке поиска нужно открыть файл SearchRow.jsx, который находится в директории src\components\SearchContainer. Находим строку switch (type) и после блока Container добавляем код:

…

switch (type) {

…

case 'Container':

icon.className = 'fa fa-box'

break;

case 'LocalUser':

icon.className = 'fa fa-user';

break;

…

Сохраняем измененный файл.

Кроме отображения самой метки нужно добавить визуализацию свойств узла. В той же директории открываем файл TabContainer.jsx и добавим импорт вкладки после импорта OuNodeData, саму вкладку создадим позже:

…

import GpoNodeData from './Tabs/GPONodeData';

import OuNodeData from './Tabs/OUNodeData';

import LocalUserNodeData from './Tabs/LocalUserNodeData';

…

До нажатия на сам узел его свойства будут скрыты. Для этого в классе TabContainer находим строку this.state и добавляем строку:

…

class TabContainer extends Component {

constructor(props) {

super(props);

this.state = {

…

containerVisible: false,

localuserVisible: false,

…

Дальше нужно добавить обработку при нажатии на узел. Для этого находим строку nodeClickHandler(type) и добавляем код:

…

nodeClickHandler(type) {

…

} else if (type === 'GPO') {

this._gpoNodeClicked();

} else if (type === 'LocalUser') {

this._localuserNodeClicked();

…

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

…

_ouNodeClicked() {

this.clearVisible()

this.setState({

ouVisible: true,

selected:2

});

}

_localuserNodeClicked() {

this.clearVisible()

this.setState({

localuserVisible: true,

selected:2

});

}

…

И ниже в функции отображения render() находим строку NoNodeData и добавляем следующий код:

render() {

…

<NoNodeData

visible={

…

!this.state.gpoVisible &&

!this.state.ouVisible &&

!this.state.localuserVisible &&

…

И еще ниже добавим отображение вкладки со свойствами для LocalUserNodeData:

…

<OuNodeData visible={this.state.ouVisible} />

<ContainerNodeData visible={this.state.containerVisible} />

<LocalUserNodeData visible={this.state.localuserVisible} />

…

Сохраняем измененный файл.

Переходим в директорию src\components\Spotlight и открываем на редактирование файл SpotlightRow.jsx. В функции render находим строку switch (this.props.nodeType), добавляем код:

…

render() {

let nodeIcon;

let parentIcon = '';

switch (this.props.nodeType) {

…

case 'GPO':

nodeIcon = 'fa fa-list';

break;

case 'LocalUser':

nodeIcon = 'fa fa-user';

break;

default:

nodeIcon = '';

break;

}

…

Ниже находим строку switch (this.props.parentNodeType) и добавляем отображение родительской иконки:

…

switch (this.props.parentNodeType) {

…

case 'GPO':

nodeIcon = 'fa fa-list';

break;

case 'LocalUser':

parentIcon = 'fa fa-user';

break;

default:

parentIcon = '';

break;

}

…

Сохраняем измененный файл.

В заключение осталось создать вкладку с отображением свойств для локального пользователя. Переходим в директорию src\components\SearchContainer\Tabs. Создадим копию файла UserNodeData.jsx и назовем ее LocalUserNodeData.jsx. Комментарии в коде просто описывают шаги, их не стоит добавлять в код:

import React, {useEffect, useState} from 'react';

import clsx from 'clsx';

import CollapsibleSection from './Components/CollapsibleSection';

import NodeCypherLinkComplex from './Components/NodeCypherLinkComplex';

import NodeCypherLink from './Components/NodeCypherLink';

import NodeCypherNoNumberLink from './Components/NodeCypherNoNumberLink';

import MappedNodeProps from './Components/MappedNodeProps';

import ExtraNodeProps from './Components/ExtraNodeProps';

import NodePlayCypherLink from './Components/NodePlayCypherLink';

import {withAlert} from 'react-alert';

import {Table} from 'react-bootstrap';

import styles from './NodeData.module.css';

import {useContext} from 'react';

import {AppContext} from '../../../AppContext';

// Меняем название метки на LocalUserNodeData

const LocalUserNodeData = () => {

const [visible, setVisible] = useState(false);

const [objectId, setObjectId] = useState(null);

const [label, setLabel] = useState(null);

//const [domain, setDomain] = useState(null);

const [nodeProps, setNodeProps] = useState({});

const context = useContext(AppContext);

useEffect(() => {

emitter.on('nodeClicked', nodeClickEvent);

return () => {

emitter.removeListener('nodeClicked', nodeClickEvent);

};

}, []);

const nodeClickEvent = (type, id, blocksinheritance, domain) => {

// Меняем название метки LocalUser

if (type === 'LocalUser') {

setVisible(true);

setObjectId(id);

let session = driver.session();

session

// Меняем метку на LocalUser

.run('MATCH (n:LocalUser {objectid:$objectid}) RETURN n AS node', {

objectid: id,

})

.then((r) => {

let props = r.records[0].get('node'). properties;

setNodeProps(props);

setLabel(props.name || objectid);

session.close();

});

} else {

setObjectId(null);

setVisible(false);

}

};

// Здесь определяется, какие свойства узла попадут в раздел

// NODE PROPERTIES. Остальные будут отображаться в EXTRA PROPERTIES

const displayMap = {

name:'Name',

password:'Password',

objectid:'Object ID',

hash:'Hash',

};

return objectId === null? (

<div></div>

):(

<div

className={clsx(

!visible && 'displaynone',

context.darkMode? styles.dark: styles.light

)}

>

<div className={clsx(styles.dl)}>

<h5>{label || objectId}</h5>

// Удаляем раздел OVERVIEW, тут он нам не потребуется

// Раздел NODE PROPERTIES

<MappedNodeProps

displayMap={displayMap}

properties={nodeProps}

label={label}

/>

<hr></hr>

// Раздел EXTRA PROPERTIES

<ExtraNodeProps

displayMap={displayMap}

properties={nodeProps}

label={label}

/>

// Удаляем раздел GROUP MEMBERSHIP

<hr></hr>

// В разделе LOCAL ADMIN RIGHTS оставляем только прямую связь AdminTo, групп уже не будет

<CollapsibleSection header={'LOCAL ADMIN RIGHTS'}>

<div className={styles.itemlist}>

<Table>

<thead></thead>

<tbody className='searchable'>

<NodeCypherLink

property='First Degree Local Admin'

target={objectId}

baseQuery={

'MATCH p=(m: LocalUser {objectid:$objectid})-[r: AdminTo]->(n: Computer)'

}

start={label}

distinct

/>

</tbody>

</Table>

</div>

</CollapsibleSection>

// Удаляем разделы EXECUTION RIGHTS, OUTBOUND OBJECT CONTROL

// и INBOUND CONTROL RIGHTS

</div>

</div>

);

};

// Заменяем на LocalUserNodeData

LocalUserNodeData.propTypes = {};

// Заменяем на LocalUserNodeData

export default withAlert()(LocalUserNodeData);

Сохраняем измененный файл.

В дополнение добавим новую метку в файл AddNodeModal.jsx, расположенный в директории src\components\Modals. Это позволит добавлять новые узлы LocalUser с помощью интерфейса BloodHound.

Открываем файл на редактирование, находим строку <FormGroup> и добавляем новую метку в раздел NodeType, который отвечает за выпадающий список:

…

<ControlLabel>Node Type</ControlLabel>

<FormControl

…

<option value='GPO'>GPO</option>

<option value='LocalUser'>LocalUser</option>

</FormControl>

…

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

…

if (type!= 'LocalUser') {

if (type === 'Computer') {

if (!name.includes('.') || name.split('.').length < 3) {

setError(

'Computer name must be similar to COMPUTER.

DOMAIN.COM'

);

return;

}

} else {

if (!name.includes('@') || name.split('@'). length > 2) {

setError('Name must be similar to NAME@DOMAIN.COM');

return;

}

let dpart = name.split('@')[1];

if (!dpart.includes('.')) {

setError('Name must be similar to NAME@DOMAIN.COM');

return;

}

}

}

…

Сохраняем измененный файл и собираем приложение.

npm run build: win32

Запускаем новую версию BloodHound и в Raw Query выполняем запрос Cypher.

MATCH (l: LocalUser) RETURN l

Рис. 4.14. Результат добавления новой метки

Проверим добавление нового узла через форму BloodHound. Правой клавишей вызовем контекстное меню и выберем Add Node.

Рис. 4.15. Добавление локальной учетной записи через форму

Повторно выполним запрос в Raw Query.

MATCH (l: LocalUser) RETURN l

Рис. 4.16. Результат добавления локальной учетной записи

Автоматическая проверка локальной учетной записи

Автоматизируем процесс создания нового узла, а также выполним проверку, имеет ли локальная учетная запись права администратора.

Настройка лаборатории

Подготовим лабораторию:

● Включим локальную учетную запись Administrator на всех доступных хостах и установим для нее пароль Qwerty321.

● Создадим локальную учетную запись Test с паролем Qwerty123 и добавим ее в группу локальных администраторов на любом хосте.

● Удалим из базы все узлы с меткой LocalUser

MATCH (l: LocalUser) DELETE l

Сбор информации

Разработаем скрипт на Powershell и назовем его CheckLocalAdmin.ps1. Алгоритм работы скрипта будет следующим:

● Получить из параметров имя пользователя и пароль.

● Создать запрос Cypher на создание узла локального пользователя.

● Получить из домена все незаблокированные компьютеры, имеющие атрибут dnshostname.

● В качестве проверки пары логин-пароль используется подключение сетевого диска c$.

● На основании результатов подключения формируется запрос на создание связи AdminTo между узлами.

function Check-LocalAdmin{

[CmdletBinding()]

Param (

[Parameter (Mandatory=$false, Position=0)]

[string]

$User,

[Parameter (Mandatory=$false, Position=1)]

[string]

$Password

)

# Создаем файл отчета

[string]$OutFile = "CheckLocalAdmin_$User.log"

# Генерируем guid для локальной учетной записи

$userid = ([guid]::NewGuid()). toString(). toUpper()

# Формируем запрос Cypher для создания нового узла на основании информации,

# полученной ранее. Результат записывается в файл отчета

Add-Content $OutFile "MERGE (l: LocalUser {name: toUpper('$User'), objectid:'$userid', password:'$Password'});"

# С помощью ADSI-запроса получаем все незаблокированные компьютеры,

# которые имеют атрибут dnshostname

$searcher = ([adsisearcher]' (&(objectCategory=computer)(dnshostname=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))')

$searcher.PageSize = 1000

$objects = $searcher.FindAll()

foreach($object in $objects)

{

# Получаем имя компьютера

$computername = $object.Properties.name.Item(0)

# Получаем SID компьютера

$computerid = (New-Object System.Security.Principal.SecurityIdentifier($object.Properties.objectsid.Item(0),0)).Value

# Формируем имя пользователя для аутентификации

$luser = $computername + "\" + $User

# Формируем данные для аутентификации

$cred = New-Object System.Management.Automation.PSCredential $luser, ($Password | ConvertTo-SecureString -AsPlainText -Force)

# Выполняем проверку учетных данных на компьютере

try

{

# Проверяем, можем ли мы подключить сетевой диск

# с использованием учетных данных, полученных ранее

if (New-PSDrive -Name ForTest -PSProvider FileSystem -Root \\$computername\c$ -Credential $cred -ErrorAction SilentlyContinue)

{

# В случае успеха отключаем сетевой диск

Remove-PSDrive ForTest

# И записываем результат в файл отчета в виде Cypher

Add-Content $OutFile "MATCH (l: LocalUser {objectid:'$userid'}) MATCH(c: Computer {objectid:'$computerid'}) MERGE (l)-[r: AdminTo]->(c);"

}

else

{

# Обрабатываем ошибки при подключении

# В данном случае учетные данные верны, но нет прав

if ($error[0].exception -Match 'Access is denied')

{

# Записываем результаты в файл отчета,

# устанавливая флаг, что это потенциальный администратор

Add-Content $OutFile "MATCH (l: LocalUser {objectid:'$userid'}) MATCH(c: Computer {objectid:'$computerid'}) MERGE (l)-[r: AdminTo]->(c) SET r.ispotential = TRUE;"

}

}

}

catch

{

continue

}

}

}

Внимание

Данный скрипт имеет определенные ограничения, например, сетевой диск c$ может быть отключен.

Совет

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

Приступим к выполнению проверки. У нас есть две локальные учетные записи, Administrator и Test. Запустим скрипт дважды с разными учетными данными:

..\CheckLocalAdmin.ps1

Check-LocalAdmin -User Administrator -Password Qwerty321

Check-LocalAdmin -User Test -Password Qwerty123

Стоит обратить внимание, что во втором случае мы получили информацию, что учетная запись Test является потенциальным администратором на хостах, хотя он точно находится в группе локальных администраторов. Это связано с настройками безопасности Windows.

Загрузим данные с помощью скрипта для загрузки:

..\neo4j_uploaddata.ps1

UploadData -file.\users.txt

Рис. 4.17. Выполнение скриптов

Проверим, что у нас получилось:

MATCH p=(l: LocalUser)-[r: AdminTo]->(c: Computer) RETURN p

Рис. 4.18. Результат выполнения запроса

Если выполнить этот запрос в браузере neo4j и посмотреть на связь между SERVER и Test, то мы обнаружим, что данная связь имеет свойство ispotential.

Рис. 4.19. Свойство для связи AdminTo

Локальный администратор и групповые политики

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

Настройка лаборатории

Для начала подготовим лабораторию. Мы не будем создавать сами политики, просто сделаем условные обозначения:

● Создадим две OU с именами Test1 и Test2.

● В этих OU создадим по одному объекту «компьютер» с именами comp1 и comp2.

● Создадим по одной групповой политике для каждой OU. Первую назовем Set Admin Password, а вторую Create Local Admin.

● Очистим базу neo4j.

● Запустим SharpHound и загрузим новые данные в BloodHound (рис. 4.20).

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

MERGE (:LocalUser {name: "ADMINISTRATOR",

objectid: toUpper(randomUUID()), password: "Qwerty123",

hash: ToUpper("329b074c0058ccf1ba2e4705382963ff")})

MERGE (:LocalUser {name: "LOCALADMIN",

objectid: toUpper(randomUUID()), password: " P@ssw0rd",

hash: ToUpper("e19ccf75ee54e06b06a5907af13cef42")})

Рис. 4.20. Создание групповых политик

Внимание

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

Создание узлов и связей

Для начала получим свойства objectid для локальных пользователей и групповых политик (рис. 4.21).

MATCH (m: LocalUser) RETURN m.name, m.objectid

UNION

MATCH (m: GPO) WHERE m.name CONTAINS "ADMIN" RETURN m.name, m.objectid

Внимание

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

4.21. Получение objectid узлов

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

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

MATCH (g: GPO {objectid:"41A40F36-E64C-4963–9D23-B51F446A5204"})

MATCH (l: LocalUser {objectid:"6160668F-96C4–4247–9095–0C048826320E"})

MERGE (g)-[: SetPassword]-(l)

Проверим, что связь создалась:

MATCH p=((g: GPO {objectid:"41A40F36-E64C-4963–9D23-B51F446A5204"})-[r: SetPassword]->(l: LocalUser)) RETURN p

Рис. 4.22. Результат добавления новой связи

Следующим шагом свяжем узел LocalUser с компьютерами, к которым применяется групповая политика:

MATCH (g: GPO {objectid:"41A40F36-E64C-4963–9D23-B51F446A5204"})-[: GPLink|Contains*1..]->(c: Computer)

MATCH (l: LocalUser {objectid:"6160668F-96C4–4247–9095–0C048826320E"})

MERGE (l)-[: AdminTo]-(c)

Проверим, что новая связь создалась:

MATCH p=((l: LocalUser)-[r: AdminTo]->(c: Computer)) RETURN p

Рис. 4.23. Результат добавления новой связи

А теперь проверим всю цепочку:

MATCH p=((g: GPO)-[r: SetPassword|AdminTo*1..]->(c: Computer)) RETURN p

Рис. 4.24. Результат проверки всей цепочки

Для второй групповой политики все запросы будут точно такими же, только изменится objectid, а связь SetPassword будет заменена на CreateUser.

MATCH (g: GPO {objectid: "CB7B245F-20FE-44EC-A540-D4D4932EEE35"})

MATCH (l: LocalUser {objectid:"8485A134–70A4–41F7-BCAA-1DF2F4ECC36B"})

MERGE (g)-[: CreateUser]-(l)

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

MATCH (g: GPO {objectid: "CB7B245F-20FE-44EC-A540-D4D4932EEE35"})-[: GPLink|Contains*1..]->(c: Computer)

MATCH (l: LocalUser {objectid:"8485A134–70A4–41F7-BCAA-1DF2F4ECC36B"})

MERGE (l)-[: AdminTo]-(c)

И проверим всю цепочку:

MATCH p=((g: GPO)-[r: CreateUser|AdminTo*1..]->(c: Computer)) RETURN p

Рис. 4.25. Результат проверки всей цепочки

Добавление новых связей в BloodHound

Если выполнить предыдущий запрос в BloodHound через Raw Query, то мы получим аналогичный результат.

Рис. 4.26. Запрос в BloodHound

Однако если попытаться выполнить ту же операцию через поиск путей, то мы получим только то, что групповая политика связана с OU Test2, в которой находится компьютер comp2. Это связано с тем, что запросы в BloodHound выполняются с перечислением всех связей, указанных в файле AppContainer.jsx в массиве fullEdgeList.

Рис. 4.27. Запрос через поиск путей

Если связь AdminTo существует в BloodHound «из коробки», то связей SetPassword и CreateUser нет и нам требуется их добавить.

Открываем файл AppContainer.jsx, который находится в директории src, находим строчку fullEdgeList и добавляем в конец массива наши связи:

…

const fullEdgeList = [

…

'DCSync',

'SyncLAPSPassword',

'SetPassword',

'CreateUser'

];

…

Сохраняем файл и теперь открываем файл index.js в том же каталоге, находим строчку global.appStore и двигаемся до edgeScheme. Там добавляем:

…

global.appStore = {

dagre: true,

…

edgeScheme:{

…

SyncLAPSPassword:'tapered',

DumpSMSAPassword:'tapered',

SetPassword:'tapered',

CreateUser:'tapered'

},

…

Листаем код до lowResPalette и в edgeScheme добавляем:

…

lowResPalette:{

…

edgeScheme:{

…

DumpSMSAPassword:'line',

SetPassword:'line',

CreateUser:'line',

},

…

Находим строчку if (typeof conf.get('edgeincluded') и там тоже добавляем наши связи:

…

if (typeof conf.get('edgeincluded') === 'undefined') {

conf.set('edgeincluded', {

…

AZAKSContributor: true,

SetPassword: true,

CreateUser: true,

});

…

Сохраняем измененный файл и собираем приложение:

npm run build: win32

Откроем обновленную версию BloodHound и проверим, как теперь выглядит запрос пути.

Рис. 4.28. Результат добавления новых связей

Повторно используемые пароли

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

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

Открываем файл AppContainer.jsx, находим строчку fullEdgeList и добавляем в конец массива наши связи:

…

const fullEdgeList = [

…

'DCSync',

'SyncLAPSPassword',

'SetPassword',

'CreateUser',

'SharePasswordWith'

];

…

Сохраняем файл. Открываем файл index.js в том же каталоге, находим строчку global.appStore, двигаемся до edgeScheme и там добавляем:

…

global.appStore = {

dagre: true,

…

edgeScheme:{

…

SyncLAPSPassword:'tapered',

DumpSMSAPassword:'tapered',

SetPassword:'tapered',

CreateUser:'tapered',

SharePasswordWith:'tapered'

},

…

Идем ниже по коду до lowResPalette и добавляем в edgeScheme:

…

lowResPalette:{

…

edgeScheme:{

…

DumpSMSAPassword:'line',

SetPassword:'line',

CreateUser:'line',

SharePasswordWith:'line',

},

…

В этом же файле находим строчку if (typeof conf.get('edgeincluded') и добавляем нашу новую связь:

…

if (typeof conf.get('edgeincluded') === 'undefined') {

conf.set('edgeincluded', {

…

AZAKSContributor: true,

SetPassword: true,

CreateUser: true,

SharePasswordWith: true,

});

…

Сохраняем измененный файл и собираем приложение:

npm run build: win32

Теперь приступим к сбору информации и проверке.

Настройка лаборатории

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

Сбор информации

Для сбора информации напишем простой скрипт на powershell и назовем его Passwordspray.ps1, алгоритм работы будет следующим:

● Запросить все незаблокированные учетные записи из Active Directory.

● Выполнить попытку аутентификации на LDAP.

● Все успешные попытки записать в файл.

function CheckReusedPassword {

[CmdletBinding()]

Param (

[Parameter (Mandatory=$false, Position=0)]

[string]

$Password

)

# Проверка ввода параметра Password

if ($Password)

{

$Password = @($Password)

}

else

{

Write-Host -ForegroundColor Red "The -Password option is required"

break

}

# Название файла для вывода результата

[string]$OutFile = "PasswordSpray_" + $(Get-Date -f ddMMyyyyhhmmss) +".log"

# Подключение библиотеки для работы с протоколом LDAP

[System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols") | Out-Null

# Получение имени текущего домена

$domainobject = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()

$domain = $domainobject.name

# Создание объекта для идентификации Active Directory по протоколу LDAP на порту 389

$di = New-Object System.DirectoryServices.Protocols.LdapDirectoryIdentifier($domain, 389)

# ADSI-запрос для получения всех незаблокированных пользователей

$searcher = [adsisearcher]' (&(objectCategory=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'

$searcher.PageSize = 1000

$objects = $searcher.FindAll()

foreach($object in $objects)

{

# Получение имени пользователя

$samaccountname = $object.Properties.samaccountname

# Получение SID пользователя

$sid = (New-Object System.Security.Principal.SecurityIdentifier($object.Properties.objectsid.Item(0),0)).Value

# Формирование строки запроса для подключения к LDAP

$creds = New-Object System.Net.NetworkCredential($samaccountname, $Password, $domain)

# Выполнение подключения к LDAP

$conn = New-Object System.DirectoryServices.Protocols.LdapConnection($di,$creds, [System.DirectoryServices.Protocols.AuthType]::NTLM)

# Проверка подключения к LDAP и запись результата

try

{

$conn.bind()

Add-Content $OutFile "MATCH (n) WHERE n.objectid = '$sid' SET n.password = '$Password';"

}

catch

{

continue

}

}

# Создание Cypher-запроса на добавление связей между объектами с одинаковым паролем

Add-Content $OutFile "MATCH (n: User) WHERE n.password = '$Password' MATCH (m: User) WHERE m.password = '$Password' FOREACH (_ IN CASE WHEN n <> m THEN [1] END | MERGE (n)-[r: SharePasswordWith {isacl: false}]->(m));"

}

Последний запрос можно рассмотреть подробнее.

MATCH (n: User) WHERE n.password = "Qwerty123"

MATCH (m: User) WHERE m.password = "Qwerty123"

FOREACH (_ IN CASE WHEN n <> m THEN [1] END | MERGE (n)-[r: SharePasswordWith {isacl: false}]->(m))a

В нем создается два списка пользователей с одинаковым паролем (рис. 4.30). С помощью оператора FOREACH по циклу выполняется проверка, что пользователь из первого списка не является точно таким же пользователем из второго. Эта проверка выполняется для того, чтобы не назначить связь на самого себя. Если условие выполняется, то между двумя узлами добавляется связь SharePasswordWith со свойством isacl: false.

Запускаем скрипт и загружаем данные в базу (рис. 4.29):

..\Passwordspray.ps1

CheckReusedPassword -Password Qwerty123

..\neo4j_uploaddata.ps1

UploadData -file.\PasswordSpray_23032024113245.log

Рис. 4.29. Запуск скриптов

Теперь можно проверить, что у нас получилось. В браузере neo4j выполним запрос:

MATCH p=(n: User)-[r: SharePasswordWith]->(m: User) RETURN p

Рис. 4.30. Общие пароли для пользователей

Запустим обновленную версию BloodHound и проверим отображение нашей связи в поиске путей:

Рис. 4.31. Поиск пути от victim до admin

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

MATCH (n: User) WHERE n.password = "Qwerty123" SET n.owned = TRUE

И теперь можно перейти во вкладку Analysis и выполнить запрос Shortest Path from Owned Principals.

Доступность хостов

BloodHound оперирует логическими связями между объектами, однако во время работы могут возникнуть ситуации, когда объект «компьютер» будет недоступен по сети, или его порты будут закрыты для использования, или он выключен физически, хотя объект в домене включен.

Для проверки доступности хоста используется несколько способов, самый простой – это ответ на ping. Однако этот метод не дает точного результата, так как ответ на ping может быть заблокирован межсетевым экраном или разрешен, но все остальные порты заблокированы пограничным межсетевым экраном при сегментации сети. Поэтому вторым фактором доступности будут открытые порты. В среде Active Directory таким портом будет 445. Не стоит забывать, что при сегментировании сети администраторы создают так называемые jump-сервера с терминальным доступом, которые позволяют получать доступ к отдельным хостам и сетям.

Настройка лаборатории

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

Сбор информации

В нашей небольшой сети можно все проверить руками, но для больших сетей это может быть долго. Поэтому напишем свой powershell-скрипт CheckAccessible.ps1, который будет собирать всю необходимую информацию и на ее основании устанавливать свойство accessible.

Алгоритм скрипта будет следующим:

● Выбрать все незаблокированные компьютеры из домена.

● Проверить ответ на ICMP.

● Проверить сетевую доступность по портам 445 и 3389.

● Провести анализ результатов.

● Сформировать вывод результатов.

function CheckAccessible {

# Создаем файл отчета

[string]$OutFile = "CheckAccessible_" + $(Get-Date -f ddMMyyyyhhmmss) +".log"

# С помощью ADSI запрашиваем все незаблокированные

# компьютеры в Active Directory

$searcher = [adsisearcher]' (&(objectCategory=computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'

$searcher.PageSize = 1000

$computers=$searcher.FindAll()

foreach($computer in $computers)

{

# Получаем SID объекта

$SID = (New-Object System.Security.Principal.SecurityIdentifier($computer.Properties.objectsid.Item(0),0)).Value

# Получаем IP-адрес(а)

try{

$IPAddress = [System.Net.Dns]::GetHostaddresses($computer.properties.name.Item(0) +"." + $env: USERDNSDomain).IPAddressToString

} catch {

$IPAddress = " "

}

# Проверяем ответ на ICMP

$timeout = 1000

$obj = New-Object System.Net.NetworkInformation.Ping

try

{

$pingcheck = $obj.Send($computer.properties.name, $timeout)

if($pingcheck.Status -eq "Success")

{

$pingable = "True"

}

Else

{

$pingable = "False"

}

} catch {

$pingable = "False"

}

# Проверяем доступность порта 445

$tcpClient = New-Object System.Net.Sockets.TcpClient

$p445 = $tcpClient.ConnectAsync($computer.properties.name.Item(0), "445").Wait("1000")

$tcpClient.Close()

# Формирование условий доступности

if(($p445 -eq $true) -and ($pingable -eq "True"))

{

$accessible = "True"

}

elseif(($p445 -eq $false) -and ($pingable -eq "True"))

{

$accessible = "False"

}

elseif(($p445 -eq $true) -and ($pingable -eq "False"))

{

$accessible = "True"

}

else

{

$accessible = "False"

}

# Проверяем доступность порта 3389

$tcpClient = New-Object System.Net.Sockets.TcpClient

$p3389 = $tcpClient.ConnectAsync($computer.properties.name.Item(0), "3389").Wait("1000")

$tcpClient.Close()

if($p3389 -eq $true)

{

$rdp = "True"

}

else

{

$rdp = "False"

}

# Создаем Cypher-запросы

if($IPAddress -eq " ")

{

Add-Content $OutFile "MATCH (c: Computer) WHERE c.objectid = '$SID' SET c.accessible = $accessible, c.pingable = $pingable, c.rdp = $rdp;"

}

else

{

# Создаем коллекцию для IP-адресов

$collectionIP = ' " {0}"' -f ($IPAddress -join ' ","')

Add-Content $OutFile "MATCH (c: Computer) WHERE c.objectid = '$SID' SET c.accessible = $accessible, c.ipaddress = [$collectionIP], c.pingable = $pingable, c.rdp = $rdp;"

}

}

}

Запустим скрипт, дождемся завершения его работы и загрузим полученные результаты в neo4j с помощью скрипта или браузера neo4j (рис. 4.32).

Рис. 4.32. Запуск скриптов

Теперь можно проверить результат работы.

OPTIONAL MATCH (c: Computer) WHERE c.accessible = FALSE RETURN c.name, c.accessible

Рис. 4.33. Результат запроса

Добавление к узлу метки о недоступности

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

Переходим в каталог src\components\ и открываем на редактирование файл Graph.jsx. В нем находим строку node.highvalue = data.properties.highvalue; и после нее добавляем следующий код:

…

node.highvalue = data.properties.highvalue;

node.accessible = data.properties.accessible;

…

Спускаемся ниже до условия if (node.highvalue) и добавляем свой код для отображения внешнего вида нашей метки о недоступности:

…

if (node.highvalue) {

…

});

}

if (node.accessible === false) {

node.glyphs.push({

// Указаниерасположения отметки

position:'bottom-left',

font:' "Font Awesome 5 Free"',

content:'\uf05e',

fillColor:'#990000',

fontScale:2.0,

fontStyle:'900',

});

}

…

Сохраняем файл и собираем решение:

npm run build: win32

В результате если у объекта будет атрибут accessible со значением false, то в нижнем левом углу появится значок, показывающий, что доступа нет. Выполним запрос Cypher в Raw Query, показывающий все компьютеры:

MATCH (c: Computer) RETURN c

Рис. 4.34. Отображение метки на узлах

Внимание

Если у узла нет такого атрибута, то отображаться ничего не будет.

Исключение из пути

Кроме визуального отображения недоступного хоста этот факт можно использовать при формировании коротких путей, исключая из запроса узлы, у которых свойство accessible установлено в false. Аналогично с черным списком, рассматривавшимся ранее.

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

MATCH (c1:Computer {name: "DC.DOMAIN.LOCAL"})

MATCH (c2:Computer {name: "COMP.DOMAIN.LOCAL"})

MATCH (g: Group {name: "DOMAIN COMPUTERS@DOMAIN.LOCAL"})

MERGE (c1)-[: AdminTo]-(c2)

MERGE (g)-[: AdminTo]-(c2)

В BloodHound перейдем во вкладку Analysis и выберем Find All Computers where Computers are Local Admin. В результате получим следующий граф:

Рис. 4.35. Обычный запрос без исключений

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

MATCH (c: Computer) WHERE c.accessible = FALSE WITH collect(c) AS wo

MATCH p=(n: Computer)-[r: MemberOf|AdminTo*1..]->(m: Computer)

WHERE NONE (x IN nodes(p) WHERE x in wo)

RETURN p

Рис. 4.36. Запрос с исключениями

Внимание

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

Добавление отметки в контекстном меню

В ходе работ ситуация может поменяться и какие-то объекты станут доступными, а какие-то, наоборот, нет. Можно обойтись запросом в Cypher для изменения свойства accessible. Изменить свойство можно с помощью Cypher-запроса в зависимости от ситуации:

// Хост недоступен

MATCH (c: Computer) SET u.accessible = FALSE

// Хост доступен

MATCH (c::Computer) SET u.accessible = TRUE

Но мы пойдем другим путем и добавим в контекстное меню узла функцию изменения доступности хоста.

Для начала открываем файл NodeTooltip.jsx, который находится в директории src\components\Tooltips, находим условие If condition={node.highvalue === true}, связанное с добавлением отметки HighValue, и после всего блока добавляем код:

…

<If condition={node.highvalue === true}>

<Then>

…

</Else>

</If>

<If condition={node.accessible === true}>

<Then>

<li

onClick={() => {

emitter.emit('setAccessible', id, false);

}}

>

<i className='fa fa-ban' /> Set {type} as NotAccessible

</li>

</Then>

<Else>

<li

onClick={() => {

emitter.emit('setAccessible', id, true);

}}

>

<i className='fa fa-ban' /> Set {type} as Accessible

</li>

</Else>

</If>

…

Сохраняем измененный файл.

Теперь переходим в директорию на уровень выше, открываем файл Graph.jsx и начинаем его изменять. Находим строчку emitter.on('setHighVal', this.setHighVal.bind(this)); и после нее добавляем:

…

emitter.on('setHighVal', this.setHighVal.bind(this));

emitter.on('setAccessible', this.setAccessible.bind(this));

…

Дальше находим блок setHighVal(id, status) { и после него добавляем наш код управления свойством:

…

setHighVal(id, status) {

closeTooltip();

…

});

}

setAccessible(id, status) {

closeTooltip();

let instance = this.state.sigmaInstance;

let node = instance.graph.nodes(id);

node.accessible = status;

if (status === false) {

node.glyphs.push({

position:'bottom-left',

font:' "Font Awesome 5 Free"',

content:'\uf05e',

fillColor:'#990000',

fontScale:2.0,

fontStyle:'900',

});

} else {

let newglyphs = [];

$.each(node.glyphs, (_, glyph) => {

if (glyph.position!== 'bottom-left') {

newglyphs.push(glyph);

}

});

node.glyphs = newglyphs;

}

instance.renderers[0].glyphs();

instance.refresh();

let q = driver.session();

q.run(

'MATCH (n:${node.type} {objectid:$objectid}) SET n.accessible=$status',

{

objectid: node.objectid,

status: status,

}

). then((x) => {

q.close();

});

}

…

Сохраняем файл и собираем приложение:

npm run build: win32

Запускаем обновленную версию BloodHound, выбираем любой узел, правой клавишей мыши вызываем контекстное меню и видим, что появилось новое меню:

Рис. 4.37. Новая кнопка в контекстном меню

Внимание

Если вы устанавливаете отметку о доступности на узел, у которого нет свойства accessible, то сначала свойство будет установлено в значение true.

Разбор inf-файлов в групповых политиках

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

Настроенные из шаблонов групповые политики записываются в inf-файлы. Эти файлы имеют свою структуру. В этом разделе мы рассмотрим возможность прочитать эти файлы и записать их содержимое в виде связей и свойств узлов.

Настройка лаборатории

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

Назначение прав пользователя (Rights Assessments)

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

Сбор информации

Простой способ прочитать содержимое файлов gptTmpl.inf с помощью powershell: рекурсивно найти все inf-файлы в групповых политиках, отфильтровать файлы по наличию Rights Assessment, прочитать содержимое файла и записать его в файл (рис. 4.38).

Get-ChildItem -Path \\dc\SYSVOL\domain.local\Policies\ -Recurse -ea SilentlyContinue -Include ('*.inf')| Select-String -Pattern "Privilege Rights"|ForEach-Object {$name = $_.Path; $name; Get-Content $name;""}| Out-File Rights.txt

В PowerView1 есть функция Get-GptTmp[15], которая разбирает inf-файл, а результаты формирует в hashtable (рис. 4.39).

..\PowerView.ps1

(Get-GptTmpl -GptTmplPath "\\dc\SYSVOL\domain.local\Policies\{6AC1786C-016F-11D2–945F-00C04fB984F9}\MACHINE\

Рис. 4.38. Результат поиска в групповых политиках

Рис. 4.39. Поиск информации с помощью PowerView

Microsoft\Windows NT\SecEdit\GptTmpl.inf" -OutputObject).PrivilegeRights

Эту информацию можно добавить в BloodHound. В качестве примера возьмем права SeMachineAccount:

MATCH(g: GPO) WHERE g.gpcpath CONTAINS "{6AC1786C-016F-11D2–945F-00C04FB984F9}"

MATCH(g)-[: GPLink|Contains*1..]->(c: Computer)

MATCH (n) WHERE n.objectid CONTAINS "S-1–5–11"

MERGE (n)-[r: SeMachineAccount]->(c)

Проверим, что связь создалась корректно, и выполним следующий запрос в Raw Query в BloodHound:

MATCH p=(n)-[r: SeMachineAccount]->(c: Computer) RETURN p

Рис. 4.40. Проверка создания связи

Чтобы автоматизировать этот процесс, создадим скрипт GetRightsAssessments.ps1, который будет формировать запросы Cypher с информацией из inf-файла. За основу возьмем функцию PowerView Get-IniContent, которая разбирает inf-файл.

Алгоритм скрипта будет следующим:

● Получить все inf-файлы, в которых есть строка Privilege Rights.

● С помощью функции Get-IniContent разобрать по циклу полученные inf-файлы

● Выбрать интересующие права.

● Разобрать SID на отдельные записи.

● Сформировать строку Cypher, установив свойство isright=TRUE для связи.

function Get-RigthsAssesment

{

$DomainName = "domain.local"

$DC = "dc"

# Создание файла отчета

[string]$OutFile = "RigthsAssesment_" + $(Get-Date -f ddMMyyyyhhmmss) +".log"

# Поиск во всех inf-файлах строки Privilege Rights

$GPOs = Get-ChildItem -Path \\$DC\SYSVOL\$DomainName\ Policies\ -Recurse -ea SilentlyContinue -Include ('*. inf')| Select-String -Pattern "Privilege Rights"

foreach($GPO in $GPOs)

{

# Получить права и SID из групповой политики

$Rights = (Get-IniContent -filePath $GPO.Path). PrivilegeRights.PSObject.Properties| select name, value

# Получить guid групповой политики

$guid = $GPO.Path.Split('\')[6].ToUpper()

foreach ($Right in $Rights)

{

# Поиск интересующих прав

if ($Right.name -match "SeEnableDelegationPrivile ge|SeMachineAccountPrivilege|SeRestorePrivilege|SeTcb Privilege|SeBackupPrivilege|SeCreateTokenPrivilege|Se CreateGlobalPrivilege|SeDebugPrivilege|SeImpersonateP rivilege|SeLoadDriverPrivilege|SeTakeOwnershipPrivile ge|SeAssignPrimaryTokenPrivilege|SeRemoteInteractiveL ogonRight|SeInteractiveLogonRight")

{

foreach($sid in $Right.value)

{

# Подготовка переменных для строки запроса

$sid = $sid.Replace('*','')

$relation = $Right.name.Replace('Privilege','')

if($sid.Length -le 12)

{

$sid = $DomainName.ToUpper() +"-" + $sid

}

# Создание строки запроса Cypher

Add-Content $OutFile "MATCH(g: GPO) WHERE g.gpcpath CONTAINS '$guid' MATCH(g)-[: GPLink|Contains*1..]->(c: Computer) MATCH (n) WHERE n.objectid CONTAINS '$sid' MERGE (n)-[r:$relation]->(c) SET r.isright = true;"

}

}

}

}

}

# Get-IniContent из PowerView

function Get-IniContent ($filePath)

{

$IniObject = New-Object PSObject

switch -regex -file $FilePath

{

"^\[(.+)\]" # Section

{

$Section = $matches[1].Trim()

$Section = $Section.Replace(' ', '')

$SectionObject = New-Object PSObject

$IniObject | Add-Member Noteproperty $Section

$SectionObject

$CommentCount = 0

}

"^(;.*)$" # Comment

{

$value = $matches[1].Trim()

$CommentCount = $CommentCount + 1

$name = "Comment" + $CommentCount

$Name = $Name.Replace(' ', '')

$IniObject.$Section | Add-Member Noteproperty $Name $Value

}

"(.+?)\s*=(.*)" # Key

{

$Name,$Value = $matches[1..2]

$Name = $Name.Trim()

$Values = $Value.split(',') | ForEach-Object {$_. Trim()}

$Name = $Name.Replace(' ', '')

$IniObject.$Section | Add-Member Noteproperty $Name $Values

}

}

return $IniObject

}

Как вариант, можно использовать общее название связи, например HasPrivilege, а название прав определить в качестве свойства связи, например с именем rightname.

Тогда запрос Cypher будет иметь следующий вид:

"MATCH(g: GPO) WHERE g.gpcpath CONTAINS '$guid' MATCH(g)-[: GPLink|Contains*1..]->(c: Computer) MATCH (n) WHERE n.objectid CONTAINS '$sid' MERGE (n)-[r: HasPrivilege]->(c) SET r.isright = true SET r.rightname ='$relation'"

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

Совет

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

Запустим скрипт и после получения результатов загрузим их в базу BloodHound (рис. 4.41):

..\GetRightsAssessments.ps1

Get-RigthsAssesment

..\neo4j_uploaddata.ps1

UploadData -file.\RigthsAssesment_25032024024435.log

Рис. 4.41. Результат запуска скриптов

Добавление новых связей в BloodHound

Вне зависимости от выбора связей их необходимо добавить в BloodHound для отображения. Поэтому заходим в каталог src и открываем файл AppContainer.jsx, находим массив fullEdgeList и добавляем связи:

…

const fullEdgeList = [

…

'CreateUser',

'SharePasswordWith',

'SeMachineAccount',

'SeRestore',

'SeTcb',

'SeBackup',

'SeCreateToken',

'SeCreateGlobal',

'SeDebug',

'SeImpersonate',

'SeLoadDriver',

'SeTakeOwnership',

'SeAssignPrimaryToken',

'SeRemoteInteractiveLogonRight',

'SeInteractiveLogonRight'

];

…

Сохраняем файл и открываем файл index.js в том же каталоге, находим строчку global.appStore, двигаемся до edgeScheme и добавляем:

…

global.appStore = {

dagre: true,

…

edgeScheme:{

…

SharePasswordWith:'tapered',

SeEnableDelegation:'tapered',

SeMachineAccount:'tapered',

SeRestore:'tapered',

SeTcb:'tapered',

SeBackup:'tapered',

SeCreateToken:'tapered',

SeCreateGlobal:'tapered',

SeDebug:'tapered',

SeImpersonate:'tapered',

SeLoadDriver:'tapered',

SeTakeOwnership:'tapered',

SeAssignPrimaryToken:'tapered',

SeRemoteInteractiveLogonRight:'tapered',

SeInteractiveLogonRight:'tapered'

},

…

Находим строчку lowResPalette и в edgeScheme добавляем:

…

lowResPalette:{

…

edgeScheme:{

…

CreateUser:'line',

SharePasswordWith:'line',

SeEnableDelegation:'line',

SeMachineAccount:'line',

SeRestore:'line',

SeTcb:'line',

SeBackup:'line',

SeCreateToken:'line',

SeCreateGlobal:'line',

SeDebug:'line',

SeImpersonate:'line',

SeLoadDriver:'line',

SeTakeOwnership:'line',

SeAssignPrimaryToken:'line',

SeRemoteInteractiveLogonRight:'line',

SeInteractiveLogonRight:'line'

},

…

Находим строчку if (typeof conf.get('edgeincluded') и там тоже добавляем наши связи:

…

if (typeof conf.get('edgeincluded') === 'undefined') {

conf.set('edgeincluded', {

…

SetPassword: true,

CreateUser: true,

SharePasswordWith: true,

SeEnableDelegation: true,

SeMachineAccount: true,

SeRestore: true,

SeTcb: true,

SeBackup: true,

SeCreateToken: true,

SeCreateGlobal: true,

SeDebug: true,

SeImpersonate: true,

SeLoadDriver: true,

SeTakeOwnership: true,

SeAssignPrimaryToken: true,

SeRemoteInteractiveLogonRight: true,

SeInteractiveLogonRight: true

});

…

Сохраняем измененный файл и собираем приложение:

npm run build: win32

Запустим обновленную версию BloodHound и проверим, какие пользователи какие имеют права. Для этого запустим в Raw Query следующий Cypher-запрос:

MATCH p=(n)-[r]->(m) WHERE r.isright = TRUE RETURN p

Рис. 4.42. Результат выполнения запроса

Подпись SMB (SMB Signing)

Подпись SMB (SMB Signing) – это механизм, который используется в протоколе SMB для обеспечения целостности и аутентификации сообщений между клиентом и сервером. Подпись SMB предотвращает атаки Relay.

В большинстве случаев администраторы будут настраивать подпись SMB через групповые политики, следовательно, самый простой способ обнаружить машины, на которых установлена подпись, – это найти EnableSecuritySignature. Если значение стоит в 1, то подпись включена. То же самое будет с параметром RequireSecuritySignature.

Сбор информации

Сбор информации происходит аналогично с предыдущим разделом.

Get-ChildItem -Path \\dc\SYSVOL\domain.local\Policies\ -Recurse -ea SilentlyContinue -Include ('*.inf')| Select-String -Pattern "EnableSecuritySignature"|ForEach-Object {$name = $_.Path; $name; Get-Content $name;""}| Out-File Signed.txt

Внимание

В результатах будет показан GUID 6AC1786C-016F-11D2–945F-00C04fB984F9, он относится к групповой политике Default Domain Controllers Policy, на контроллерах домена подпись SMB включена по умолчанию.

Поиск с помощью PowerView:

(Get-GptTmpl -GptTmplPath "\\dc\SYSVOL\windomain.local\Policies\{6AC1786C-016F-11D2–945F-00C04fB984F9}\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" -OutputObject).RegistryValues|fl

Запрос Cypher добавления к хосту свойства smbsighing будет выглядеть следующим образом:

MATCH (c: Computer) WHERE c.name = "COMP1@DOMAIN.LOCAL" SET c.smbsighing = TRUE

Внимание

Если добавлять через powershell-скрипт, то лучшим вариантом будет использовать не имя, а SID.

Для автоматизации процесса с помощью Powershell воспользуемся скриптом, описанным в предыдущем разделе. Назовем его SMBSigningGPO.ps1. Немного изменим алгоритм:

● Получить все inf-файлы, где есть строка EnableSecuritySignature.

● С помощью функции Get-IniContent разобрать по циклу полученные inf-файлы.

● Выбрать интересующую настройку реестра.

● Сравнить второе значение из свойств реестра.

● Сформировать строку Cypher.

function Invoke-SMBSigningGPO()

{

# Создание файла отчета

[string]$OutFile = "SMBSigning.log"

$DomainName = "domain.local"

$DC = "dc"

# Поиск во всех inf-файлах строки EnableSecuritySignature

$GPOs = Get-ChildItem -Path \\$DC\SYSVOL\$DomainName\

Policies\ -Recurse -ea SilentlyContinue -Include ('*.

inf')| Select-String -Pattern "EnableSecuritySignature"

foreach($GPO in $GPOs)

{

# Получить значение свойства

$RegistryValues = (Get-IniContent -filePath $GPO.Path).

RegistryValues.PSObject.Properties| select name, value

# Получить guid групповой политики

$guid = $GPO.Path.Split('\')[6].ToUpper()

foreach ($RegistryValue in $RegistryValues)

{

# Проверка установки флага подписи

if ($RegistryValue.name -match "EnableSecuritySignature")

{

if($RegistryValue.value[1] -eq 1)

{

$signing = "TRUE"

}

else

{

$signing = "FALSE"

}

Add-Content $OutFile "MATCH(g: GPO)-

[: GPLink|Contains*1..]->(c: Computer) WHERE g.gpcpath

CONTAINS '$guid' SET c.smbsigning = $signing;"

}

}

}

}

# Get-IniContent из PowerView

function Get-IniContent ($filePath)

{

$IniObject = New-Object PSObject

switch -regex -file $FilePath

{

"^\[(.+)\]" # Section

{

$Section = $matches[1].Trim()

$Section = $Section.Replace(' ', '')

$SectionObject = New-Object PSObject

$IniObject | Add-Member Noteproperty $Section

$SectionObject

;$CommentCount = 0

}

"^(;.*)$" # Comment

{

$value = $matches[1].Trim()

$CommentCount = $CommentCount + 1

$name = "Comment" + $CommentCount

$Name = $Name.Replace(' ', '')

$IniObject.$Section | Add-Member Noteproperty

$Name $Value

}

"(.+?)\s*=(.*)" # Key

{

$Name,$Value = $matches[1..2]

$Name = $Name.Trim()

$Values = $Value.split(',') | ForEach-Object

{$_.Trim()}

$Name = $Name.Replace(' ', '')

$IniObject.$Section | Add-Member Noteproperty

$Name $Values

}

}

return $IniObject

}

Запустим скрипт и загрузим полученные данные в базу с помощью браузера neo4j.

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

MATCH (c: Computer) WHERE c.smbsigning IS NULL OR c.smbsigning = FALSE RETURN c.name

Конечно, подпись SMB может настраиваться в «золотом образе» или с помощью SCCM. Поэтому в дополнение можно провести проверку непосредственно на хостах.

В сканере nmap есть скрипт smb-security-mode, проверяющий настройки безопасности SMB.

nmap -p137,139,445 -script smb-security-mode comp1

Можно использовать powershell-скрипт Invoke-SMBNegotiate.ps1[16].

Invoke-SMBNegotiate -ComputerName dc

Invoke-SMBNegotiate -ComputerName comp

Рис. 4.43. Результат проверки подписи

Таким образом, если необходимо проверить все машины, нужно получить список всех активных машин с помощью PowerView или ADSI и в цикле проверить наличие подписи:

function CheckSMBSigning {

# Загружаем скрипт

..\Invoke-SMBNegotiate.ps1

# С помощью ADSI запрашиваем все незаблокированные

# компьютеры в Active Directory

$searcher = [adsisearcher]' (&(objectCategory=computer)

(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'

$searcher.PageSize = 1000

$computers=$searcher.FindAll()

foreach($computer in $computers)

{

# Получаем имя компьютера

$ComputerName = $computer.properties.name

Invoke-SMBNegotiate -ComputerName $ComputerName

-ErrorAction SilentlyContinue

}

}

Запустим получившийся скрипт и посмотрим на результаты:

Рис. 4.44. Результат проверки подписи SMB

Если использовать скрипт Invoke-SMBNegotiate.ps1 и данные загружать в BloodHound, то можно немного изменить скрипт:

function CheckSMBSigning

{

# Загружаем скрипт

..\Invoke-SMBNegotiate.ps1

# Создаем файл отчета

[string]$OutFile = "CheckSMBSigning_" + $(Get-Date

-f ddMMyyyyhhmmss) +".log"

# С помощью ADSI запрашиваем все незаблокированные

# компьютеры в Active Directory

$searcher = [adsisearcher]' (&(objectCategory=computer)

(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'

$searcher.PageSize = 1000

$computers=$searcher.FindAll()

foreach($computer in $computers)

{

# Получаем SID объекта

$SID = (New-Object System.Security.Principal.

SecurityIdentifier($computer.Properties.objectsid.

Item(0),0)).Value

# Получаем имя компьютера

$ComputerName = $computer.properties.name

# Выполняем проверку

$Result = Invoke-SMBNegotiate -ComputerName

$ComputerName -ErrorAction SilentlyContinue

# Обрабатываем результаты

$smbsigning = $Result.smbsigning

if($smbsigning -ne $null)

{

# Добавляем строку запроса

Add-Content $OutFile "MATCH (c: Computer) WHERE

c.objectid='$SID' SET c.smbsigning = $smbsigning;"

}

}

}

Запустим обновленный скрипт, загрузим данные в BloodHound с помощью браузера neo4 и проверим результат (рис. 4.45).

MATCH (c: Computer) WHERE c.smbsigning = FALSE RETURN c.name

Рис. 4.45. Результат выполнения запроса

Добавление атрибутов с правами WriteProperty

Тот, кто давно работает с BloodHound, знает, что собираются только самые интересные ACL, такие как GenericAll или GenricWrite. Тем не менее Active Directory позволяет настраивать права на изменение каждого атрибута отдельно, и в этом случае у BloodHound гораздо меньше возможностей, например есть WriteSPN или WriteAccountRestriction.

Настройка лаборатории

Прежде чем начать собирать и добавлять информацию, необходимо настроить нашу лабораторию. Для этого открываем ADUC на контроллере домена, во вкладке View включаем Advanced Features и выполняем следующие действия:

● Предоставим права Write scriptPath пользователю user над пользователем victim.

● Создадим новую OU Office.

● Предоставим права Write gPLink пользователю victim над OU Office.

● Создадим объект компьютера test.

● Переместим объект компьютера test в OU Office.

● Предоставим права Write userAccountControl пользователю victim над компьютером test.

● Очистим базу neo4j через BloodHound.

● Заново соберем информацию с помощью SharpHound и загрузим ее в BloodHound.

Если мы сейчас попытаемся посмотреть, какие связи есть от user до office, с помощью BloodHound, то, скорей всего, нам предоставят путь через получение привилегий доменного администратора.

Сбор информации

Для автоматизации процесса сбора информации напишем скрипт (Get-ExtendedACL.ps1) на Powershell, алгоритм которого будет следующим:

● Определим, какие атрибуты нам интересны для сбора.

● Определим класс объектов для ADSI-запроса.

● Для каждого класса объектов получим ACL.

● Выберем права WriteProperty.

● Сравним GUID атрибутов с теми, которые нам интересны.

● Сформируем строку запроса Cypher.

Есть два варианта формирования связи: первый – создать связь WriteProperty и добавить свойство property, в котором будет указано название атрибута; второй – создать связь в виде Write + Attribute.

В первом случае придется использовать браузер neo4j, чтобы увидеть, какие свойства можно изменять, во втором в код BloodHound нужно будет добавить все связи. Остановимся на втором варианте.

Список атрибутов, их название и guid можно найти на сайте Microsoft[17].

function GetWriteProperty()

{

# Сопоставление guid с названием атрибута

<#

bf9679a8–0de6–11d0-a285–00aa003049e2 – scriptPath

f30e3bbe-9ff0–11d1-b603–0000f80367c1 – gPLink

bf967a68–0de6–11d0-a285–00aa003049e2 – UserAccountControl

#>

# Создаем файл отчета

[string]$OutFile = "ExtendendACLS_" + $(Get-Date -f

ddMMyyyyhhmmss) +".log"

# Определяем имя домена

$DomainObject = [System.DirectoryServices.

ActiveDirectory.Domain]::GetCurrentDomain()

$DomainName = $DomainObject.name.toUpper()

# Фильтр для классов объектов

$Filters = @(

'(&(objectCategory=person)(objectClass=user))', # Users

'(objectCategory=computer)' # Computers

'(objectCategory=organizationalUnit)' # OU

)

foreach($filter in $filters)

{

# ADSI-запрос с использованием фильтра

$searcher = ([adsisearcher]"$filter")

$searcher.PageSize = 1000

$objects = $searcher.FindAll()

foreach($object in $objects)

{

# Получаем SID или GUID субъекта

if($object.Properties.objectsid)

{

$ID = (New-Object System.Security.Principal.

SecurityIdentifier($object.Properties.objectsid.

Item(0),0)).Value

}

else

{

$ID = [guid]$object.Properties.objectguid[0]|

Select-Object -ExpandProperty Guid

$ID = $ID.ToUpper()

}

# Получаем ACL объекта

$acls = ([ADSI]$object.path).ObjectSecurity.Access

foreach($acl in $acls)

{

# Проверка для атрибута scriptPath

if($acl.ActiveDirectoryRights -match "WriteProperty"-and $acl.ObjectType -match "bf9679a8–0de6–11d0-a285–00aa003049e2")

{

#Получаем SID объекта из его имени

$IdentityReference = new-object System.Security.Principal.NTAccount($acl.IdentityReference)

$ObjectSID = $IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]). toString()

if($ObjectSID.Length -le 12)

{

$ObjectSID = $DomainName +"-" + $ObjectSID

}

# Записываем результат в файл в виде запроса Cypher

Add-Content $OutFile "MATCH

(m {objectid:'$ObjectSID'}) MATCH

(n {objectid:'$ID'}) MERGE (m)-[r: WriteScriptPath]-

>(n) SET r.isacl=TRUE;"

}

# Проверка для атрибута gPLink

if($acl.ActiveDirectoryRights -match "WriteProperty"-and $acl.ObjectType -match "f30e3bbe-9ff0–11d1-b603–0000f80367c1")

{

#Получаем SID объекта из его имени

$IdentityReference = new-object System.Security.

Principal.NTAccount($acl.IdentityReference)

$ObjectSID = $IdentityReference.Translate([System.

Security.Principal.SecurityIdentifier]). toString()

if($ObjectSID.Length -le 12)

{

$ObjectSID = $DomainName +"-" + $ObjectSID

}

# Записываем результат в файл в виде запроса Cypher

Add-Content $OutFile "MATCH

(m {objectid:'$ObjectSID'}) MATCH

(n {objectid:'$ID'}) MERGE (m)-[r: WriteGPLink]->(n)

SET r.isacl=TRUE;"

}

# Проверка для атрибута UserAccountControl

if($acl.ActiveDirectoryRights -match "WriteProperty"

-and $acl.ObjectType -match "bf967a68–0de6–11d0-a285–

00aa003049e2")

{

#Получаем SID объекта из его имени

$IdentityReference = new-object System.Security.

Principal.NTAccount($acl.IdentityReference)

$ObjectSID = $IdentityReference.Translate([System.

Security.Principal.SecurityIdentifier]). toString()

if($ObjectSID.Length -le 12)

{

$ObjectSID = $DomainName +"-" + $ObjectSID

}

# Записываем результат в файл в виде запроса Cypher

Add-Content $OutFile "MATCH (m {objectid:'$ObjectSID'})

MATCH (n {objectid:'$ID'}) MERGE

(m)-[r: WriteUserAccountControl]->(n) SET r.isacl=TRUE;"

}

}

}

}

}

После запуска скрипта получим три строки Cypher-запроса, которые необходимо загрузить в базу. Так как запросов немного, то для загрузки данных воспользуемся браузером neo4j.

Рис. 4.46. Результат сбора информации

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

Добавление новых связей в BloodHound

Открываем файл AppContainer.jsx, который находится в src, находим массив fullEdgeList и добавляем новые связи:

…

const fullEdgeList = [

…

'SeRemoteInteractiveLogonRight',

'SeInteractiveLogonRight',

'WriteScriptPath',

'WriteUserAccountControl',

'WriteGPLink'

];

…

Сохраняем файл и открываем файл index.js в том же каталоге, находим строчку global.appStore, двигаемся до edgeScheme и добавляем:

…

global.appStore = {

dagre: true,

…

edgeScheme:{

…

SeRemoteInteractiveLogonRight:'tapered',

SeInteractiveLogonRight:'tapered',

WriteScriptPath:'tapered',

UserAccountControl:'tapered',

WriteGPLink:'tapered',

},

…

Находим строчку lowResPalette и в edgeScheme добавляем:

…

lowResPalette:{

…

edgeScheme:{

…

SeRemoteInteractiveLogonRight:'line',

SeInteractiveLogonRight:'line',

WriteScriptPath:'line',

WriteUserAccountControl:'line',

WriteGPLink:'line'

},

…

Находим строчку if (typeof conf.get('edgeincluded') и там тоже добавляем наши связи:

…

if (typeof conf.get('edgeincluded') === 'undefined') {

conf.set('edgeincluded', {

…

SeRemoteInteractiveLogonRight: true,

SeInteractiveLogonRight: true,

WriteScriptPath: true,

WriteUserAccountControl: true,

WriteGPLink: true,

});

…

Сохраняем измененный файл и собираем приложение:

npm run build: win32

Запускаем обновленную версию BloodHound и проверяем пути от user до office.

Рис. 4.47. Короткий путь от user до office

Изменение формы Добавление связей

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

Переходим в директорию src\components\Modals и открываем на редактирование файл AddEdgeModal.jsx. Находим строчку <ControlLabel>Edge Type</ControlLabel> и ниже всех в списке имен связей добавляем наши новые связи:

…

<ControlLabel>Edge Type</ControlLabel>

<FormControl

value={edgeValue}

…

<option value='DumpSMSAPassword'>

DumpSMSAPassword

</option>

<option value='WriteScriptPath'>WriteScriptPath</option>

<option value='WriteUserAccountControl'>WriteUserAccountControl</option>

<option value='WriteGPLink'>WriteGPLink</option>

</FormControl>

…

Сохраняем измененный файл и собираем приложение:

npm run build: win32

Запускаем обновленную версию BloodHound, на пустом месте нажимаем правую клавишу мыши и выбираем Add Edge. Нажмем на список типов узлов и увидим, что наши новые связи появились в этом списке.

Рис. 4.48. Добавление связей в форму

Добавление в фильтр запросов

Выполним еще одно дополнение и добавим ACL для фильтрации запроса при использовании функции поиска путей.

Открываем файл EdgeFilter.jsx, расположенный в \src\components\SearchContainer\EdgeFilter, находим строку title='ACL Edges' и добавляем в конце блока наши связи:

…

title='ACL Edges'

edges={[

…

'WriteAccountRestrictions',

'WriteScriptPath',

'WriteUserAccountControl',

'WriteGPLink',

]}

sectionName='ACL'

…

Ниже, после строки <EdgeFilterCheck name='SyncLAPSPassword' />, также добавим наши связи:

…

<EdgeFilterCheck name='SyncLAPSPassword' />

<EdgeFilterCheck name='WriteScriptPath' />

<EdgeFilterCheck name='WriteUserAccountControl' />

<EdgeFilterCheck name='WriteGPLink' />

<EdgeFilterSection

title='Containers'

…

Сохраняем измененный файл и собираем приложение:

npm run build: win32

Запустим обновленную версию BloodHound. Откроем фильтры и увидим, что появились наши добавленные значения (рис. 4.49).

Рис. 4.49. Добавленные фильтры

Создание подсказки

Как уже говорилось в разделе об интерфейсе BloodHound, связи имеют подсказку по эксплуатации. И сейчас мы создадим подсказку для WriteScriptPath.

Откроем файл HelpModal, расположенный в src\components\Modals, и в конце импорта внешних файлов добавим:

…

import AZLogicAppContributor from './HelpTexts/AZLogicAppContributor/AZLogicAppContributor';

import AZNodeResourceGroup from './HelpTexts/AZNodeResourceGroup/AZNodeResourceGroup';

import WriteScriptPath from './HelpTexts/WriteScriptPath/WriteScriptPath';

…

Находим строку const components и добавим в нее следующий код:

…

const components = {

GenericAll: GenericAll,

…

AZNodeResourceGroup: AZNodeResourceGroup,

WriteScriptPath: WriteScriptPath,

};

…

Сохраняем файл.

Теперь переходим в директорию src\components\Modals\HelpText и сделаем копию директории AdminTo. Назовем ее WriteScriptPath. Заходим в эту директорию и переименовываем файл AdminTo.jsx в WriteScriptPath.jsx.

Откроем файл WriteScriptPath.jsx и выполним изменения:

import React from 'react';

import PropTypes from 'prop-types';

import {Tabs, Tab} from 'react-bootstrap';

import General from './General';

import Abuse from './Abuse';

import Opsec from './Opsec';

import References from './References';

const WriteScriptPath = ({sourceName, sourceType, targetName, targetType}) => {

return (

<Tabs defaultActiveKey={1} id='help-tab-container' justified>

<Tab eventKey={1} title='Info'>

<General

sourceName={sourceName}

sourceType={sourceType}

targetName={targetName}

/>

</Tab>

<Tab eventKey={2} title='Abuse Info'>

<Abuse />

</Tab>

<Tab eventKey={3} title='Opsec Considerations'>

<Opsec />

</Tab>

<Tab eventKey={4} title='References'>

<References />

</Tab>

</Tabs>

);

};

WriteScriptPath.propTypes = {

sourceName: PropTypes.string,

sourceType: PropTypes.string,

targetName: PropTypes.string,

targetType: PropTypes.string,

};

export default WriteScriptPath;

Открываем файл General.jsx и изменим текст, не трогая информацию в фигурных скобках.

import React from 'react';

import PropTypes from 'prop-types';

import {groupSpecialFormat} from '../Formatter';

const General = ({sourceName, sourceType, targetName}) => {

return (

<>

<p>

{sourceName} имеет привилегии Write ScriptPath

на {targetName}.

</p>

<p>

В среде Active Directory профиль пользователяможно настроитьтаким образом, что при входе пользователя на машину автоматическибудет выполняться скрипт

или

исполняемый файл. Путь к скрипту или исполняемому файлубудет храниться в атрибуте ScriptPath пользователя.

Права Write позволяют изменять этот атрибут.

</p>

</>

);

};

General.propTypes = {

sourceName: PropTypes.string,

sourceType: PropTypes.string,

targetName: PropTypes.string,

};

export default General;

Сохраняем файл, переходим к следующему файлу References.jsx и заменяем ссылки на соответствующие теме:

import React from 'react';

const References = () => {

return (

<>

<a href='https://github.com/PowerShellMafia/ PowerSploit/blob/dev/Recon/PowerView.ps1'>

https://github.com/PowerShellMafia/PowerSploit/blob/ dev/Recon/PowerView.ps1

</a>

<br />

<a href='https://www.thehacker.recipes/ad/movement/ dacl/logon-script'>

https://www.thehacker.recipes/ad/movement/dacl/logon-script

</a>

</>

);

};

export default References;

Сохраняем, переходим к следующему файлу Abuse.jsx и меняем код текста:

…

const Abuse = () => {

return (

<>

<p>

Права Write позволяют изменять атрибут ScriptPath.

</p>

<p>

Изменить значение атрибута ScriptPathможно с помощью PowerView:

</p>

<pre>

<code>

{ "Set-DomainObject -Identity victim -Set @ {'scriptpath'='//share\script.bat'}"}

</code>

</pre>

<p>

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

</p>

<p>

После выполнения атаки следует удалить установленныйпуть с помощью PowerView:

</p>

<pre>

<code>

{ "Set-DomainObject -Identity vicitm -Clear scriptpath"}

</code>

</pre>

</>

);

};

…

Сохраняем, переходим к следующему файлу Opesec.jsx и меняем код текста:

import React from 'react';

const Opsec = () => {

return (

<>

<p>

Средствамониторингамогутбыть настроены на события, связанные с изменением атрибута ScriptPath.

</p>

</>

);

};

export default Opsec;

Сохраняем последний файл и собираем приложение:

npm run build: win32

Запускаем обновленную версию BloodHound, выполняем запрос поиска от user до victim, правой клавишей мыши нажимаем на связь и смотрим на результат нашей работы:

Рис. 4.50. Общее описание недостатка

Рис. 4.51. Описание эксплуатации

Общие файловые ресурсы

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

Создание нового узла с новой меткой

Общие файловые ресурсы не имеют собственного guid, поэтому нам потребуется сгенерировать их самостоятельно с помощью функции randomUUID. Запрос на создание нового узла с меткой Share будет выглядеть следующим образом:

MERGE (:Share {name: "Share", path: "\\\\comp1\\test", objectid: toUpper(randomUUID())})

Внимание

Для корректного отображения пути необходимо экранировать слеш.

Проверим, что создался новый узел с указанными параметрами:

MATCH (s: Share) RETURN s

Рис. 4.52. Результат добавления нового узла

Теперь добавим отображение новой метки Share в BloodHound. Весь процесс будет аналогичен отображению метки локального пользователя.

Отображение метки общего ресурса в BloodHound

Переходим в директорию src и открываем файл index.js на редактирование. Добавим информацию о том, как новая метка будет отображаться на графе. Находим строку global.AppStore и вставляем следующий код после блока LocalUser:

…

global.appStore = {

dagre: true,

…

LocalUser:{

font: "'Font Awesome 5 Free'",

content:'\uf007',

scale:1.5,

color:'#E69717',

},

Share:{

font: "'Font Awesome 5 Free'",

content:'\uf07b',

scale:1.25,

color:'#f8d775',

},

…

Дальше ищем строку lowResPalette и добавляем название метки и цвет. Этот параметр отвечает за отображение узлов в низком разрешении.

…

lowResPalette:{

colorScheme:{

…

Base:'#E6E600',

LocalUser:'#E69717',

Share:'#f8d775',

},

…

Сохраняем измененный файл.

Теперь переходим в директорию src\js и открываем на редактирование файл utils.js. В разделе const labels = [добавляем новую метку после LocalUser.

const labels = [

'Base',

'Container',

'OU',

'GPO',

'User',

'Computer',

'Group',

'Domain',

'LocalUser',

'Share',

…

Находим строку export async function setSchema() и в массив label добавляем название новой метки.

…

export async function setSchema() {

const luceneIndexProvider = "lucene+native-3.0"

let labels = ["User", … "Base", "LocalUser", "Share", …

Сохраняем измененный файл.

Теперь нужно добавить метку, чтобы она отображалась на графе, для этого переходим в директорию components и открываем файл Graph.jsx. Находим строку switch (type) и добавляем в нее код:

…

switch (type) {

…

case 'LocalUser':

node.type_localuser = true;

break;

case 'Share':

node.type_share = true;

break;

}

…

Сохраняем измененный файл.

Для отображения метки в строке поиска нужно открыть файл SearchRow.jsx, который находится в директории src\components\SearchContainer. Находим строку switch (type) и после блока Container добавляем код:

…

switch (type) {

…

case 'LocalUser':

icon.className = 'fa fa-user';

break;

case 'Share':

icon.className = 'fa fa-folder';

break;

…

Сохраняем измененный файл.

Кроме отображения самой метки нужно добавить визуализацию свойств узла. В той же директории открываем файл TabContainer.jsx и добавим импорт вкладки после импорта LocalUserNodeData:

…

import OuNodeData from './Tabs/OUNodeData';

import LocalUserNodeData from './Tabs/LocalUserNodeData';

import ShareNodeData from './Tabs/ShareNodeData';

…

До нажатия на сам узел его свойства будут скрыты, для этого в классе TabContainer находим строку this.state и добавляем строку:

…

class TabContainer extends Component {

constructor(props) {

super(props);

this.state = {

…

containerVisible: false,

localuserVisible: false,

shareVisible: false,

…

Дальше нужно добавить обработку при нажатии на узел. Для этого находим строку nodeClickHandler(type) и добавляем код:

…

nodeClickHandler(type) {

…

} else if (type === 'GPO') {

this._gpoNodeClicked();

} else if (type === 'LocalUser') {

this._localuserNodeClicked();

} else if (type === 'Share') {

this._shareNodeClicked();

…

Ниже находим изменение состояния видимости вкладки для каждой метки. Код начинается с _labelNodeClicked. Для общего ресурса код будет выглядеть следующим образом:

…

_localuserNodeClicked() {

this.clearVisible()

this.setState({

localuserVisible: true,

selected:2

});

}

_shareNodeClicked() {

this.clearVisible()

this.setState({

shareVisible: true,

selected:2

});

}

…

Ниже в функции отображения render() находим строку NoNodeData и добавляем следующий код:

…

render() {

…

<NoNodeData

visible={

…

!this.state.ouVisible &&

!this.state.localuserVisible &&

!this.state.shareVisible &&

…

И еще ниже добавим отображение вкладки со свойствами для ShareNodeData:

…

<ContainerNodeData visible={this.state.containerVisible} />

<LocalUserNodeData visible={this.state.localuserVisible} />

<ShareNodeData visible={this.state.shareVisible} />

…

Сохраняем измененный файл.

Переходим в директорию src\components\Spotlight и открываем на редактирование файл SpotlightRow.jsx. В функции render находим строку switch (this.props.nodeType) и добавляем код:

…

render() {

let nodeIcon;

let parentIcon = '';

switch (this.props.nodeType) {

…

case 'GPO':

nodeIcon = 'fa fa-list';

break;

case 'LocalUser':

nodeIcon = 'fa fa-user';

break;

case 'Share':

nodeIcon = 'fa fa-folder';

break;

default:

nodeIcon = '';

break;

}

…

Ниже находим строку switch (this.props.parentNodeType) и добавляем отображение родительской иконки:

…

switch (this.props.parentNodeType) {

…

case 'GPO':

nodeIcon = 'fa fa-list';

break;

case 'LocalUser':

parentIcon = 'fa fa-user';

break;

case 'Share':

parentIcon = 'fa fa-folder';

break;

default:

parentIcon = '';

break;

}

…

Сохраняем измененный файл.

В заключение осталось создать вкладку с отображением свойств для локального пользователя. Переходим в директорию src\components\SearchContainer\Tabs. Создадим копию файла UserNodeData.jsx и назовем его ShareNodeData.jsx. Комментарии в коде просто описывают шаги, их не стоит добавлять в код.

Сохраняем измененный файл.

Наконец, создадим вкладку с отображением свойств для общего ресурса. Для этого перейдем в директорию src\components\SearchContainer\Tabs. Создадим копию файла UserNodeData.jsx и назовем его ShareNodeData.jsx. Шаги по изменению кода будут описаны в нем самом, добавлять их не надо.

import React, {useEffect, useState} from 'react';

import clsx from 'clsx';

import CollapsibleSection from './Components/CollapsibleSection';

import NodeCypherLinkComplex from './Components/NodeCypherLinkComplex';

import NodeCypherLink from './Components/NodeCypherLink';

import NodeCypherNoNumberLink from './Components/NodeCypherNoNumberLink';

import MappedNodeProps from './Components/MappedNodeProps';

import ExtraNodeProps from './Components/ExtraNodeProps';

import NodePlayCypherLink from './Components/NodePlayCypherLink';

import {withAlert} from 'react-alert';

import {Table} from 'react-bootstrap';

import styles from './NodeData.module.css';

import {useContext} from 'react';

import {AppContext} from '../../../AppContext';

// Меняем название метки на ShareNodeData

const ShareNodeData = () => {

const [visible, setVisible] = useState(false);

const [objectId, setObjectId] = useState(null);

const [label, setLabel] = useState(null);

//const [domain, setDomain] = useState(null);

const [nodeProps, setNodeProps] = useState({});

const context = useContext(AppContext);

useEffect(() => {

emitter.on('nodeClicked', nodeClickEvent);

return () => {

emitter.removeListener('nodeClicked', nodeClickEvent);

};

}, []);

const nodeClickEvent = (type, id, blocksinheritance, domain) => {

// Меняем название метки Share

if (type === 'Share') {

setVisible(true);

setObjectId(id);

let session = driver.session();

session

// Меняем метку на Share

.run('MATCH (n:Share {objectid:$objectid}) RETURN n AS node', {

objectid: id,

})

.then((r) => {

let props = r.records[0].get('node'). properties;

setNodeProps(props);

setLabel(props.name || props.azname || objectid);

session.close();

});

} else {

setObjectId(null);

setVisible(false);

}

};

// Здесь определяется, какие свойства узла попадут в раздел

// NODE PROPERTIES, остальные будут отображаться в EXTRA PROPERTIES

const displayMap = {

name:'Name',

path:'Path',

objectid:'Object ID',

};

return objectId === null? (

<div></div>

):(

<div

className={clsx(

!visible && 'displaynone',

context.darkMode? styles.dark: styles.light

)}

>

<div className={clsx(styles.dl)}>

<h5>{label || objectId}</h5>

// Удаляем раздел OVERVIEW, тут он нам не потребуется.

// Раздел NODE PROPERTIES

<MappedNodeProps

displayMap={displayMap}

properties={nodeProps}

label={label}

/>

<hr></hr>

// Раздел EXTRA PROPERTIES

<ExtraNodeProps

displayMap={displayMap}

properties={nodeProps}

label={label}

/>

<hr></hr>

// Удаляем разделы EXECUTION RIGHTS, OUTBOUND OBJECT CONTROL

<CollapsibleSection header={'INBOUND CONTROL RIGHTS'}>

<div className={styles.itemlist}>

<Table>

<thead></thead>

<tbody className='searchable'>

<NodeCypherLink

property='Explicit Object Controllers'

target={objectId}

baseQuery={

'MATCH p=(n)-[r]->(u1:Share{objectid:$objectid}) WHERE r.isfsacl=true'

}

end={label}

distinct

/>

<NodeCypherLink

property='Unrolled Object Controllers'

target={objectId}

baseQuery={

'MATCH p=(n)-[r: MemberOf*1..]->(g: Group)-[r1:CanWrite|CanModify|FullControl]->(u: Share

{objectid:$objectid}) WITH LENGTH(p)

as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid =u.objectid) AND NOT n.objectid = u.objectid'

}

end={label}

distinct

/>

<NodePlayCypherLink

property='Transitive Object Controllers'

target={objectId}

baseQuery={

'MATCH (n) WHERE NOT n.objectid=$objectidMATCH p = shortestPath((n)-[r1:MemberOf|CanWrite|CanModify|FullControl*1..]->(u1:Share {objectid:$objectid}))'

}

end={label}

distinct

/>

</tbody>

</Table>

</div>

</CollapsibleSection>

</div>

</div>

);

};

// Заменяем на ShareNodeData

ShareNodeData.propTypes = {};

// Заменяем на ShareNodeData

export default withAlert()(ShareNodeData);

Сохраняем измененный файл и собираем приложение:

npm run build: win32

Запускаем обновленную версию BloodHound, выполняем запрос Cypher в Raw Query:

MATCH (s: Share) RETURN s

Рис. 4.53. Результат отображения общего ресурса

Настройка лаборатории

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

● На контроллере домена в корне диска C создадим директорию Share.

● В директории Share создадим директорию Backup.

● В директории Backup создадим директорию Test.

● Сделаем директорию Share общей, добавим полные права для пользователя user.

● Добавим полные права для пользователя victim на директорию Backup.

● Добавим права на запись пользователю admin на директорию Test.

Удалим наш тестовый общий ресурс:

MATCH (s: Share) DELETE s

Сбор информации

Теперь приступим к созданию автоматизированной утилиты для сбора информации об общих ресурсах и формирования Cypher-запросов. Напишем скрипт на Powershell, который назовем GetSharesInfo.ps1.

За основу возьмем один из вариантов скрипта Get-NetShare.ps1, который использует WinAPI, – NetShareEnum и добавим дополнительный функционал.

Алгоритм будет следующим:

● Получить все активные компьютеры в домене, у которых есть атрибут dnshostname.

● С помощью WinAPI NetShareEnum проверить наличие общих ресурсов на каждом компьютере.

● Проверить доступность общего ресурса для текущего пользователя.

● Получить ACL для общего ресурса.

● Если есть доступ, выполнить проверку поддиректорий с глубиной 2.

● Выполнить проверку доступности поддиректорий.

● Получить ACL для поддиректории.

● Для ACL установить атрибут isfsacl.

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

function Get-SharesInfo()

{

# Подключаем библиотеку netapi32.dll

Add-Type @"

using System;

using System.Runtime.InteropServices;

using System.Text;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]

public struct SHARE_INFO_1

{

[MarshalAs(UnmanagedType.LPWStr)]

public string shi1_netname;

public uint shi1_type;

[MarshalAs(UnmanagedType.LPWStr)]

public string shi1_remark;

}

public static class NetApi32

{

[DllImport("netapi32.dll", SetLastError = true)]

public static extern int NetApiBufferFree(IntPtr Buffer);

[DllImport("netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]

public static extern int NetShareEnum(

StringBuilder servername,

int level,

ref IntPtr bufptr,

uint prefmaxlen,

ref int entriesread,

ref int totalentries,

ref int resume_handle);

}

"@

# Получаем имя домена

$DomainObject = [System.DirectoryServices. ActiveDirectory.Domain]::GetCurrentDomain()

$DomainName = $DomainObject.name.toUpper()

# Создаем файл отчета

[string]$OutFile = "Shares_" + $(Get-Date -f ddMMyyyyhhmmss) +".log"

# Выполняем ADSI-запрос для всех незаблокированных компьютеров с атрибутом dnshostname

$computers = ([adsisearcher]' (&(objectCategory=computer) (dnshostname=*)(!(userAccountCont rol:1.2.840.113556.1.4.803:=2)))').FindAll()

foreach($computer in $computers)

{

# Получаем имя и SID компьютера

$ComputerName = $computer.Properties.name.Item(0)

$SID = (New-Object System.Security.Principal. SecurityIdentifier($computer.Properties.objectsid. Item(0),0)).Value

# Выполняем вызов Win API NetShareEnum

$pBuffer = [IntPtr]::Zero

$entriesRead = $totalEntries = $resumeHandle = 0

$result = [NetApi32]::NetShareEnum(

$ComputerName, # servername

1, # level

[Ref] $pBuffer, # bufptr

[UInt32]::MaxValue, # prefmaxlen

[Ref] $entriesRead, # entriesread

[Ref] $totalEntries, # totalentries

[Ref] $resumeHandle # resumehandle

)

if (($result -eq 0) -and ($pBuffer -ne [IntPtr]::Zero) -and ($entriesRead -eq $totalEntries))

{

$offset = $pBuffer.ToInt64()

for ($i = 0; $i -lt $totalEntries; $i++)

{

$pEntry = New-Object IntPtr($offset)

$shareInfo = [Runtime.InteropServices.Marshal]::PtrTo Structure($pEntry, [Type] [SHARE_INFO_1])

$offset += [Runtime.InteropServices. Marshal]::SizeOf($shareInfo)

# Генерируем guid для общего ресурса

$objectid = ([guid]::NewGuid()). toString(). toUpper()

# Из результатов перечисления получаем имя, описание и тип общего ресурса

$shareName = $shareInfo.shi1_netname.ToUpper()

$shareDescription = $shareInfo.shi1_remark

$shareType = $shareInfo.shi1_type

# Проверяем доступ к общему ресурсу для текущего пользователя

try{

$TargetPath = ("\\" + $ComputerName + "\" + $shareName).ToUpper()

$Null = [IO.Directory]::GetFiles($TargetPath)

$ShareAccess = "TRUE"

}catch{

$ShareAccess = "FALSE"

}

# Составляем строку запроса для создания узла

Add-Content $OutFile "MERGE (s: Share {objectid:'$objectid', name:'$shareName', des cription:'$shareDescription', type:$shareType, accsessible:$ShareAccess, domain:'$DomainName', path:'\\$TargetPath'});"

Add-Content $OutFile "MATCH (s: Share {objectid:'$objectid'}) MATCH(c: Computer {objectid:'$SID'}) MERGE (s)-[r: HostedOn]->(c);"

# Получаем ACL для общего ресурса

$Acl = Get-Acl $TargetPath -ErrorAction SilentlyContinue

foreach ($Access in $acl.Access)

{

if($Access.FileSystemRights -match "Write|FullControl|Modify")

{

# Получаем SID для объекта

$ID = new-object System.Security.Principal. NTAccount($Access.IdentityReference.Value)

$ObjectSID = $ID.Translate([System.Security. Principal.SecurityIdentifier]). toString()

if($ObjectSID.Length -le 12)

{

$ObjectSID = $DomainName +"-" + $ObjectSID

}

# Формируем права доступа для создания связей

if($Access.FileSystemRights -match "Write")

{

$Right = "CanWrite"

}

if($Access.FileSystemRights -match "Modify")

{

$Right = "CanModify"

}

if($Access.FileSystemRights -match "FullControl")

{

$Right = "FullControl"

}

# Отбрасываем системные учетные записи

if(($ObjectSID -notmatch "S-1–3–0|S-1–5–18|S-1–5–9") -and ($Access.IdentityReference.Value -notmatch "TrustedInstaller"))

{

# Проверяем наследование

$IsInherited = $Access.IsInherited

# Формируем строку запроса для связей ACL

Add-Content $OutFile "MATCH (m {objectid:'$ObjectSID'}) MATCH (s: Share {objectid:'$objectid'}) MERGE (m)-[r:$Right]->(s) SET r.isinherited = $IsInherited, r.isfsacl = TRUE;"

}

}

}

# Получаем директории с исключением доменных и у которых есть права доступа

if(($shareName -notmatch "\$|SYSVOL|NETLOGON") -and ($ShareAccess -eq "TRUE"))

{

# Получаем поддиректории с глубиной две директории

$FolderPath = Get-ChildItem -Path $TargetPath -Recurse -Directory -Depth 2

Foreach ($Folder in $FolderPath)

{

# Получаем свойства поддиректорий: имя, полное имя, guid и полный путь

$name = $Folder.Name.toUpper()

$fullname = $Folder.FullName.toUpper()

$objectid = ([guid]::NewGuid()). toString(). toUpper()

$parentpath = ([IO.Directory]::GetParent($fullname). fullname). toUpper()

# Проверяем доступ для текущего пользователя

try{

$Null = [IO.Directory]::GetFiles($fullname)

$PathAccess = "TRUE"

}catch{

$PathAccess = "FALSE"

}

# Формируем строку запроса для создания узла поддиректории

Add-Content $OutFile "MERGE (s: Share {objectid:'$objectid', name:'$Name', accsessible:$PathAccess, domain:'$DomainName', path:'\\$fullname'});"

Add-Content $OutFile "MATCH (m: Share {path:'\\ $parentpath'}) MATCH (n: Share {objectid:'$objectid'}) MERGE (m)-[r: Contains]->(n);"

# Получаем ACL для поддиректорий

$Acl = Get-Acl $fullname -ErrorAction SilentlyContinue

foreach ($Access in $acl.Access)

{

if($Access.FileSystemRights -match "Write|FullControl|Modify")

{

# Получаем SID для объекта

$ID = new-object System.Security.Principal. NTAccount($Access.IdentityReference.Value)

$ObjectSID = $ID.Translate([System.Security. Principal.SecurityIdentifier]). toString()

if($ObjectSID.Length -le 12)

{

$ObjectSID = $DomainName +"-" + $ObjectSID

}

# Формируем права доступа для создания связей

if($Access.FileSystemRights -match "Write")

{

$Right = "CanWrite"

}

if($Access.FileSystemRights -match "Modify")

{

$Right = "CanModify"

}

if($Access.FileSystemRights -match "FullControl")

{

$Right = "FullControl"

}

# Отбрасываем привилегированные учетные записи

if(($ObjectSID -notmatch "S-1-3-0|S-1-5-18|S-1-5–9|S-1–5–32–544 |-517$|-512$|-519$|-500$") -and ($Access.IdentityReference.Value -notmatch "TrustedInstaller"))

{

# Проверяем наследование

$IsInherited = $Access.IsInherited

# Формируем строку запроса для связей ACL

Add-Content $OutFile "MATCH (m {objectid:'$ObjectSID'}) MATCH (s: Share {objectid:'$objectid'}) MERGE (m)-[r:$Right]->(s) SET r.isinherited = $IsInherited, r.isfsacl = TRUE;"

}

}

}

}

}

}

[Void] [NetApi32]::NetApiBufferFree($pBuffer)

}

}

}

После выполнения с помощью скрипта загрузим данные в базу neo4j:

..\GetSharesInfo.ps1

Get-SharesInfo

..\neo4j_uploaddata.ps1

UploadData -file.\Shares_26032024051115.log

Рис. 4.54. Результат выполнения скриптов

Проверим результат наших стараний – в Raw Query BloodHound выполним следующий Cypher-запрос:

MATCH p=(u)-[r1]-(s: Share)-[r: HostedOn|Contains*0..]->(c) RETURN p

Рис. 4.55. Результат добавления общих ресурсов

Добавление новых связей в BloodHound

В скрипте мы определили новые связи, и теперь необходимо добавить их в BloodHound. Открываем файл AppContainer.jsx, находим массив fullEdgeList и добавляем связи:

…

const fullEdgeList = [

…

'WriteUserAccountControl',

'WriteGPLink',

'HostedOn',

'CanWrite',

'CanModify',

'FullControl'

];

…

Сохраняем файл и открываем файл index.js в том же каталоге, находим строчку global.appStore, двигаемся до edgeScheme и добавляем:

…

global.appStore = {

dagre: true,

…

edgeScheme:{

…

UserAccountControl:'tapered',

WriteGPLink:'tapered',

HostedOn:'tapered',

CanWrite:'tapered',

CanModify:'tapered',

FullControl:'tapered',

},

…

Находим строчку lowResPalette и в edgeScheme добавляем:

…

lowResPalette:{

…

edgeScheme:{

…

WriteUserAccountControl:'line',

WriteGPLink:'line',

HostedOn:'line',

CanWrite:'line',

CanModify:'line',

FullControl:'line',

},

…

Находим строчку if (typeof conf.get('edgeincluded') и там тоже добавляем наши связи:

…

if (typeof conf.get('edgeincluded') === 'undefined') {

conf.set('edgeincluded', {

…

WriteUserAccountControl: true,

WriteGPLink: true,

HostedOn: true,

CanWrite: true,

CanModify: true,

FullControl: true,

});

…

Сохраним измененный файл и соберем приложение:

npm run build: win32

Чтобы проверить результат нашей работы, запустим новую версию BloodHound, в строке запроса пути введем данные для пользователя admin и директории test:

Рис. 4.56. Результат добавления новых связей

Центр сертификации

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

Для поиска информации и эксплуатации используются две утилиты – certify[18] и certipy[19]. Кроме этих утилит можно использовать встроенную в Windows утилиту certutil. В certipy есть возможность загружать полученную информацию в BloodHound. Существуют два ключа, -old-bloodhound и -bloodhound, первый в качестве отображения использует GPO, а второй уже добавляет две новые метки CA и Template, но при этом требуется версия BloodHound, написанная автором certipy.

Мы в свою очередь создадим утилиту для сбора информации, новые метки для отображения в BloodHound и в завершение добавим запросы Cypher для поиска полезной информации.

Совет

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

Настройка лаборатории

Для сбора информации потребуется центр сертификации в доменной инфраструктуре. Начнем с установки центра сертификации.

Установка центра сертификации

В качестве сервера центра сертификации будем использовать контроллер домена. Запускаем Server Manager и выбираем Add roles and features. Следуем за мастером добавления новой роли. Нажимаем кнопку Next. Предложенные по умолчанию настройки нас будут устраивать, поэтому нажимаем кнопку Next до тех пор, пока не появится окно Select server roles.

Выбираем следующую роль:

● Active Directory Certificate Service (рис. 4.57)

Нажимаем кнопку Next, пока не дойдем до Select role services, и выбираем роли для центра сертификации (рис. 4.58):

● Certification Authority;

● Certificate Enrollment Web Service;

● Certification Authority Web Enrollment.

Нажимаем кнопку Next до самого конца, пока кнопка Install не станет активной, и нажимаем ее.

После установки нажимаем на кнопку Close. В верхнем правом углу появился желтый восклицательный знак, который указывает, что роли требуют завершения настройки. Нажимая на кнопку Next, доходим до Select Role Service to configure и выбираем:

● Certification Authority;

● Certification Authority Web Enrollment (рис. 4.59).

Нажимаем на кнопку Next, доходим до тех пор, пока кнопка Configure не станет активной, оставляя все настройки по умолчанию (рис. 4.60–4.62).

Рис. 4.57. Добавление новой роли

Рис. 4.58. Установка центра сертификации, шаг 1

Рис. 4.59. Установка центра сертификации, шаг 2

Рис. 4.60. Установка центра сертификации, шаг 3

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

Рис. 4.61. Установка центра сертификации, шаг 4

Подтверждаем выбор и нажимаем на кнопку Next до тех пор, пока кнопка Configure не станет активной. Нажимаем на нее и завершаем установку центра сертификации.

Рис. 4.62. Установка центра сертификации, шаг 5

Настройка шаблонов и центра сертификации

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

Внимание

В книге рассматриваются базовые недостатки, которые были обнаружены до обновления центра сертификации. Поэтому мы пропустим настройку центра сертификации под ESC9 и ESC10. Также на момент выхода книги в свет могут быть обнаружены другие методы эксплуатации шаблонов. На контроллере домена запускаем Certification Authority, правой клавишей мыши вызываем контекстное меню для Certificate Templates и выбираем Manage.

ESC1

В открывшемся окне находим шаблон User, правой клавишей мыши вызываем контекстное меню и выбираем Duplicate Template.

Рис. 4.63. Создание дубликата шаблона

Во вкладке General устанавливаем имя ESC1. Во вкладке Subject Name устанавливаем Supply in the request и нажимаем кнопку Apply.

Рис. 4.64. Настройка шаблона сертификата ESC1

Теперь нам нужно опубликовать наш новый шаблон. Закрываем окно с шаблонами. Вызываем контекстное меню для Certificate Templates и выбираем New > Certificate Template to Issue. Находим наш шаблон ESC1, выделяем его и нажимаем на кнопку OK.

Рис. 4.65. Опубликованные шаблоны сертификатов

ESC2

Повторяем действия для создания шаблона сертификатов. Имя нового сертификата – ESC2. Во вкладке Subject Name устанавливаем Supply in the request. Переходим во кладку Extensions, выбираем Application Policies и нажимаем на кнопку Edit. У нас есть два варианта: или удалить все значения и оставить поле пустым, или добавить политику Any Purpose. Нажимаем на Apply.

Рис. 4.66. Настройка шаблона сертификата ESC2

Теперь необходимо опубликовать шаблон сертификата.

ESC3

В данном случае потребуется два шаблона сертификата. Создаем копию, как было описано ранее, называем шаблон ESC3_1. Переходим во вкладку Extensions и редактируем Application Policies. Нам необходимо удалить все поля и вместо них добавить Certificate Request Agent (рис. 4.67).

Нажимаем на кнопку Apply и закрываем окно. Теперь создадим еще один шаблон сертификата и назовем его ESC3_2. Переходим во вкладку Issuance Requirements и выполняем настройку. Установим галочку на This number of authorized signatures. В Policy type required in signature выберем Application policy, а в Application policy – Certificate Request Agent (рис. 4.68).

Нажимаем кнопку Apply и опубликовываем оба шаблона сертификата.

Рис. 4.67. Настройка шаблона сертификата ESC3_1

Рис. 4.68. Настройка шаблона сертификата ESC3_2

ESC4

Создадим еще одну копию шаблона сертификата User и назовем его ESC4. Перейдем во вкладку Security и выполним следующие настройки: группе Authenticated Users предоставим полные права на данный шаблон, а группе Domain Users добавим права Write.

Рис. 4.69. Права ACL для шаблона сертификата ESC4

Нажимаем кнопку Apply, закрываем окно и опубликовываем новый шаблон сертификата.

ESC5

В данном случае не требуется никаких дополнительных настроек. Это связано с тем, что наш центр сертификации установлен на контроллере домена, а предоставлять права локального администратора на контроллер домена кому-то кроме администраторов – плохая практика.

ESC6

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

certutil -config "dc.domain.local\domain-DC–CA" -setreg policy\EditFlags +EDITF_ATTRIBUTESUBJECTALTNAME2

Рис. 4.70. Результат выполнения команды

ESC7

Возвращаемся в Certification Authority, вызываем контекстное меню для нашего центра сертификации и выбираем Properties. Переходим во вкладку Security и добавим группу Domain User, которой предоставим права Issue and Manage Certificates и Manage CA (рис. 4.71).

После добавления прав необходимо перезагрузить центр сертификации.

ESC8

Мы уже установили Web Enrollment, и дополнительные настройки не требуются.

ESC11

В командной строке с правами администратора нужно выполнить команду (рис. 4.72):

certutil -config "dc.domain.local\domain-DC–CA" -setreg "CA\InterfaceFlags" -IF_ENFORCEENCRYPTICERTREQUEST

Рис. 4.71. Добавление прав управления

Рис. 4.72. Удаление флага из настроек

Совет

Можно запустить утилиту certify или certipy и изучить результаты.

Сбор информации

Для сбора информации напишем скрипт на Powershell и назовем его Get-ADCSInformation.ps1. Алгоритм скрипта будет следующим:

● Выгрузить из Active Directory все шаблоны сертификатов.

● На основании информации о недостатках в шаблонах сформировать узел шаблона сертификата.

● Получить владельца шаблона сертификата.

● Получить ACL для шаблонов сертификатов.

● Запросить из Active Directory все центры сертификации.

● На основании информации о недостатках центра сертификации сформировать узлы центра сертификации.

● Проверить, в каких центрах сертификации используются шаблоны сертификатов.

Внимание

Данный скрипт является рабочей демонстрацией, но при этом в нем отсутствует ряд механизмов проверок, например доступности самого центра сертификации по портам.

Некоторые комментарии по работе скрипта будут предоставлены в самом коде.

# https://www.sysadmins.lv/blog-en/how-to-convert-pkiexirationperiod-and-pkioverlapperiod-active-directory-attributes.aspx

function Convert-pKIPeriod ([Byte[]]$ByteArray)

{

[array]::Reverse($ByteArray)

$LittleEndianByte = –join ($ByteArray |%{ "{0:x2}" -f $_})

$Value = [Convert]::ToInt64($LittleEndianByte,16) * -.0000001

if (!($Value% 31536000) -and ($Value / 31536000) -ge 1) {[string]($Value / 31536000) + " years"}

elseif (!($Value% 2592000) -and ($Value / 2592000) -ge 1) {[string]($Value / 2592000) + " months"}

elseif (!($Value% 604800) -and ($Value / 604800) -ge 1) {[string]($Value / 604800) + " weeks"}

elseif (!($Value% 86400) -and ($Value / 86400) -ge 1) {[string]($Value / 86400) + " days"}

elseif (!($Value% 3600) -and ($Value / 3600) -ge 1) {[string]($Value / 3600) + " hours"}

else { "0 hours"}

}

function Get-ADCSInfo()

{

# Создаем hashtable для флагов Request Disposition

[flags()] Enum RequestDisposition

{

Pending = 0x00000000

Issue = 0x00000001

Deny = 0x00000002

UserRequestAttribute = 0x00000003

Mask = 0x000000ff

PendingFirst = 0x00000100

}

# Создаем файл отчета

[string]$OutFile = "ADCS_" + $(Get-Date -f ddMMyyyyhhmmss) +".log"

# Получаем имя домена

$DomainObject = [System.DirectoryServices. ActiveDirectory.Domain]::GetCurrentDomain()

$CurrentDomain = ([ADSI]""). distinguishedName

$DomainName = $DomainObject.name.toUpper()

# Выполняем ADSI-запрос для получения всех шаблонов сертификатов

$Objects = [ADSI]"LDAP://CN=Certificate Templates, CN=Public Key Services, CN=Services, CN=Configuration, $CurrentDomain"

$Templates = $Objects.Get_Children()

foreach($template in $templates)

{

# Получаем имя шаблона сертификата

$Name = $Template.properties.name.toUpper()

# Получаем отображаемое имя шаблона сертификата

$DisplayName = $Template.properties.displayname

# Получаем distinguishedname шаблона сертификата

$DN = $Template.properties.distinguishedname

# Получаем guid шаблона сертификата

$ObjectGUID = [guid]$Template.properties.objectGUID. value | select -ExpandProperty Guid

# Получаем размер ключа шаблона сертификата

$KeySize = $Template.properties.'msPKI–Minimal-Key-Size'

# Проверяем, можно ли экспортировать ключ сертификата

if($template.properties.'msPKI-Private-Key-Flag'.value -band "0x00000010")

{

$ExportableKey = "True"

}

else

{

$ExportableKey = "False"

}

# Проверка установки флага Key Archival для ключей

if($template.properties.'msPKI-Private-Key-Flag'.value -band "0x00000001")

{

$RequiresKeyArchival = "True"

}

else

{

$RequiresKeyArchival = "False"

}

# Конвертируем период действия сертификата в читаемый вид

$ValidityPeriod = Convert-pKIPeriod $template. properties.pKIExpirationPeriod.value

# Конвертируем период обновления сертификата в читаемый вид

$RenewalPeriod = Convert-pKIPeriod $template. properties.pKIOverlapPeriod.value

# Проверяем, содержит ли EKU значение Authenticate Client

if($template.properties.pKIExtendedKeyUsage -contains "1.3.6.1.5.5.7.3.2")

{

$AuthClient = "True"

}

Else

{

$AuthClient = "False"

}

# Проверяем наличие флага Enrollee Supplies Subject (ESC1)

if($Template.properties.'msPKI–Certificate-Name-Flag'. value -band "0x00000001")

{

$EnrolleeSuppliesSubject = "True"

}

else

{

$EnrolleeSuppliesSubject = "False"

}

# Проверяем наличие флага Any Purpose или пустое значение в EKU (ESC2)

if(($template.properties.pKIExtendedKeyUsage -contains "2.5.29.37.0") -or ($template.properties. pKIExtendedKeyUsage.Count -eq 0))

{

$AnyPurpose = "True"

}

Else

{

$AnyPurpose = "False"

}

# Проверяем наличие Request Agent в EKU (ESC3)

if($template.properties.pKIExtendedKeyUsage -contains "1.3.6.1.4.1.311.20.2.1")

{

$RequestAgent = "True"

}

Else

{

$RequestAgent = "False"

}

# Проверяем наличие флага msPKI-RA-Signature (ESC3)

if($template.properties.'msPKI-RA-Signature'.value -band "0x00000001")

{

$APSignature = "True"

}

else

{

$APSignature = "False"

}

# Проверяем наличие EnrollmentAgent в атрибутах Application Policies (ESC3)

if($template.properties.'msPKI-RA-Application-Policies' -contains "1.3.6.1.4.1.311.20.2.1")

{

$EnrollmentAgent = "True"

}

Else

{

$EnrollmentAgent = "False"

}

# Проверяем наличие флага NoSecurityExtension в атрибуте Enrollment Flag (ESC9 ESC10)

if($template.properties.'msPKI-Enrollment-Flag'.value -band "0x00080000")

{

$NoSecurityExtension = "True"

}

else

{

$NoSecurityExtension = "False"

}

# Проверяем наличие подтверждения на выпуск сертификата

if($template.properties.'msPKI-Enrollment-Flag'.value -band "0x00000002")

{

$PendAllRequests = "True"

}

else

{

$PendAllRequests = "False"

}

# Получаем время создания шаблона сертификата в формате epoch

$WhenCreated = (Get-Date $template.Properties. WhenCreated.DateTime -UFormat%s). split(',')[0]

# Получаем время изменения шаблона сертификата в формате epoch

$WhenChanged = (Get-Date $template.Properties. WhenChanged.DateTime -UFormat%s). split(',')[0]

# Создаем Cypher-запрос на создание узла шаблона сертификата

Add-Content $OutFile "MERGE (t: Template {name:'$Name', displayname:'$DisplayName', objectid:'$ObjectGUID', distinguishedname:'$DN', domain:'$DomainName', keysize:'$KeySize', exportablekey:$ExportableKey, requireskeyarchival:$RequiresKeyArchival, validityperiod:'$ValidityPeriod', renewalperiod:'$RenewalPeriod', authclient:$AuthClient, enrolleesuppliessubject:$EnrolleeSuppliesSubject, anypurpose:$AnyPurpose, requestagent:$RequestAgent, enrollmentagent:$EnrollmentAgent, apsignature:$APSignature, nosecurityextension:$NoSecurityExtension, pendallrequests:$PendAllRequests, whencreated:$WhenCreated, whenchanged:$WhenChanged});"

# Получаем SID владельца шаблона сертификата

$ID = new-object System.Security.Principal. NTAccount($template.ObjectSecurity.Owner)

$ownersid = $ID.Translate([System.Security.Principal. SecurityIdentifier]). toString()

if($ownersid.Length -le 12)

{

$ownersid = $DomainName +"-" + $ownersid

}

# Создаем Cypher-запрос на создание связи между шаблоном и владельцем

Add-Content $OutFile "MATCH (m {objectid:'$ownersid'}) MATCH (n: Template {objectid:'$ObjectGUID'}) MERGE (m)-[r: Owns]->(n) SET r.isacl=TRUE;"

# Получаем ACL для шаблона сертификата

$acls = $template.ObjectSecurity.Access

foreach($acl in $acls)

{

# Получаем SID объекта, который имеет права

$ID = new-object System.Security.Principal.NTAccount($acl.IdentityReference)

$ObjectSID = $ID.Translate([System.Security.Principal.SecurityIdentifier]). toString()

if($ObjectSID.Length -le 12)

{

$ObjectSID = $DomainName +"-" + $ObjectSID

}

# Проверяем права Enroll

if(($acl.ActiveDirectoryRights -match "ExtendedRight") -and ($acl.ObjectType -eq "0e10c968–78fb-11d2–90d4–00c04f79dc55"))

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектами с правами Enroll

Add-Content $OutFile "MATCH (m {objectid:'$ObjectSID'}) MATCH (n {objectid:'$ObjectGUID'}) MERGE (m)-[r: CanEnroll]-(n) SET r.isacl=TRUE;"

}

# Проверяем права AutoEnroll

if(($acl.ActiveDirectoryRights -match "ExtendedRight") -and ($acl.ObjectType -eq "a05b8cc2–17bc-4802-a710-e7c15ab866a2"))

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектами с правами Enroll

Add-Content $OutFile "MATCH (m {objectid:'$ObjectSID'}) MATCH (n {objectid:'$ObjectGUID'}) MERGE (m)-[r: CanAutoEnroll]-(n) SET r.isacl=TRUE;"

}

# Проверяем права WriteProperty

if(($acl.ActiveDirectoryRights -match "WriteProperty") -and ($acl.ActiveDirectoryRights -notmatch "ExtendedRight"))

{

if($acl.ObjectType -eq "00000000–0000–0000–0000–000000000000")

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектами с правами GenericWrite

Add-Content $OutFile "MATCH (m {objectid:'$ObjectSID'}) MATCH (n {objectid:'$ObjectGUID'}) MERGE (m)-[r: GenericWrite]-(n) SET r.isacl=TRUE;"

}

else

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектами с правами WriteProperty

Add-Content $OutFile "MATCH (m {objectid:'$ObjectSID'}) MATCH (n {objectid:'$ObjectGUID'}) MERGE (m)-[r: WriteProperty]-(n) SET r.isacl=TRUE;"

}

}

# Проверяем права WriteDacl

if($acl.ActiveDirectoryRights -match "WriteDacl")

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектами с правами WriteDacl

Add-Content $OutFile "MATCH (m {objectid:'$ObjectSID'}) MATCH (n {objectid:'$ObjectGUID'}) MERGE (m)-[r: WriteDacl]-(n) SET r.isacl=TRUE;"

}

# Проверяем права WriteOwner

if($acl.ActiveDirectoryRights -match "WriteOwner")

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектами с правами WriteOwner

Add-Content $OutFile "MATCH (m {objectid:'$ObjectSID'}) MATCH (n {objectid:'$ObjectGUID'}) MERGE (m)-[r: WriteOwner]-(n) SET r.isacl=TRUE;"

}

# Проверяем права GenericAll

if($acl.ActiveDirectoryRights -match "GenericAll")

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектами с правами GenericAll

Add-Content $OutFile "MATCH (m {objectid:'$ObjectSID'}) MATCH (n {objectid:'$ObjectGUID'}) MERGE (m)-[r: GenericAll]-(n) SET r.isacl=TRUE;"

}

}

}

# Выполняем ADSI-запрос для получения всех шаблонов сертификатов

$objects = [ADSI]"LDAP://CN=Enrollment Services, CN=Public Key Services, CN=Services, CN=Configuration,$CurrentDomain"

$cas = $objects.Get_Children()

foreach($ca in $cas)

{

# Получаем имя центра сертификации

$name = $ca.properties.name.ToUpper()

# Получаем сервер, на котором установлен центр сертификации

$dnshostname = $ca.dnshostname.value.ToString().ToUpper()

# Получаем guid центра сертификации

$ObjectGUID = [guid]$ca.properties.objectGUID.value | select -ExpandProperty Guid

# Проверяем наличие флага AttributeSubjectaltName2 (ESC6)

$reg=[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$dnshostname)

$key = $reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\$name\PolicyModules\CertificateAuthority_MicrosoftDefault.Policy")

if($key.GetValue('EditFlags') -band "0x00040000")

{

$AttributeSubjectaltName2 = "Enabled"

}

else

{

$AttributeSubjectaltName2 = "Disabled"

}

# Проверяем, доступен ли Web Enrollment

$URL = "http://$dnshostname/certsrv"

$Request = [System.Net.WebRequest]::Create($URL)

$Cache = New-Object System.Net.CredentialCache

$Cache.Add([System.Uri]::new($URL), "NTLM", [System.Net.CredentialCache]::DefaultNetworkCredentials)

$Request.Credentials = $Cache

$Request.Timeout = 3000

try {

$Response = $Request.GetResponse()

if($Response.StatusCode -eq [System.Net.HttpStatusCode]::OK)

{

$WebEnrollement = "Enabled"

}

}

catch {

$WebEnrollement = "Disabled"

}

# Проверяем отсутствие флага EnforceEncryptiCertRequest (ESC11)

$reg=[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$dnshostname)

$key = $reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\$name")

if($key.GetValue('InterfaceFlags') -band "512")

{

$EnforceEncryptiCertRequest = "Enabled"

}

else

{

$EnforceEncryptiCertRequest = "Disabled"

}

# Получаем значение атрибута Request Disposition в центре сертификации

$reg=[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$dnshostname)

$key = $reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\$name\PolicyModules\CertificateAuthority_MicrosoftDefault.Policy")

# Преобразуем результат в читаемый вид на основе hashtable

$vals=[RequestDisposition]$key.getvalue('RequestDisposition')

# Список для Request Disposition

$RequestDisposition = "' {0}'" -f ($vals.ToString().Replace(", ","','"))

# Получаем время создания центра сертификации в формате epoch

$WhenCreated = (Get-Date $ca.Properties.WhenCreated.DateTime -UFormat%s). split(',')[0]

# Получаем время изменения центра сертификации в формате epoch

$WhenChanged = (Get-Date $ca.Properties.WhenChanged.DateTime -UFormat%s). split(',')[0]

# Создаем Cypher-запрос на создание узла центра сертификации со всеми доступными свойствами

Add-Content $OutFile "MERGE (m: CA {objectid:'$ObjectGUID', name:'$name', dnshostname:'$dnshostname', domain:'$DomainName', webenrollement:'$WebEnrollement', attributesubjectaltname2:'$AttributeSubjectaltName2', enforceencrypticertrequest:'$EnforceEncryptiCertRequest', requestdisposition: [$RequestDisposition], whencreated:$WhenCreated, whenchanged:$WhenChanged});"

# Получаем SID владельца центра сертификации

$ID = new-object System.Security.Principal.NTAccount($ca.ObjectSecurity.Owner)

$OwnerSID = $ID.Translate([System.Security.Principal.SecurityIdentifier]). toString()

if($ownersid.Length -le 12)

{

$OwnerSID = $DomainObject.name.toUpper() +"-" + $OwnerSID

}

# Создаем Cypher-запрос на создание связи между шаблоном и владельцем

Add-Content $OutFile "MATCH (m {ObjectID:'$OwnerSID'}) MATCH (n: CA {ObjectID:'$ObjectGUID'}) MERGE (m)-[r: Owns]->(n) SET r.isacl=TRUE;"

# Получаем ACL для центра сертификации

$reg=[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$dnshostname)

$key = $reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\$name")

$Objects = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $key.getvalue('Security'), 0

foreach($Object in $Objects.DiscretionaryAcl)

{

# Получаем SID объекта, который имеет права

$ObjectID = $Object.SecurityIdentifier[0].Value

if($ObjectID.Length -le 12)

{

$ObjectID = $DomainObject.name.toUpper() +"-" + $ObjectID

}

# Проверяем права на запрос сертификата

if($Object.AccessMask -band "512")

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектом с правами на запрос сертификата

Add-Content $OutFile "MATCH (m {objectid:'$ObjectID'}) MATCH (n: CA {objectid:'$ObjectGUID'}) MERGE (m)-[r: RequestCertificates]->(n) SET r.isacl=TRUE;"

}

# Проверяем права ManageCA

if($Object.AccessMask -band "1")

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектом с правами ManageCA

Add-Content $OutFile "MATCH (m {objectid:'$ObjectID'}) MATCH (n: CA {objectid:'$ObjectGUID'}) MERGE (m)-[r: ManageCA]->(n) SET r.isacl=TRUE;"

}

# Проверяем права на управление шаблонами

if($Object.AccessMask -band "2")

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектом с правами ManageCertificates

Add-Content $OutFile "MATCH (m {objectid:'$ObjectID'}) MATCH (n: CA {objectid:'$ObjectGUID'}) MERGE (m)-[r: ManageCertificates]->(n) SET r.isacl=TRUE;"

}

# Проверяем права на чтение шаблонов сертификатов

if($Object.AccessMask -band "256")

{

# Создаем Cypher-запрос на создание связи между шаблоном и объектом с правами Read

Add-Content $OutFile "MATCH (m {objectid:'$ObjectID'}) MATCH (n: CA {objectid:'$ObjectGUID'}) MERGE (m)-[r: Read]->(n) SET r.isacl=TRUE;"

}

}

# Получаем опубликованные сертификаты

$certTemplates = $ca.properties.certificateTemplates

foreach($certTemplate in $certTemplates)

{

$certTemplateName = $certTemplate.ToUpper()

# Создаем Cypher-запрос на создание связи между шаблоном и центром сертификации, где он опубликован

Add-Content $OutFile "MATCH (m: Template {name:'$certTemplateName', domain:'$DomainName'}) MATCH (n: CA {objectid:'$ObjectGUID'}) MERGE (m)-[r: HostedOn]->(n);"

# Создаем Cypher-запрос на установку свойства enabled в значение true

Add-Content $OutFile "MATCH (m: Template {name:'$certTemplateName', domain:'$DomainName'}) SET m.enabled = True;"

}

}

}

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

..\Get-ADCSInformation.ps1

Get-ADCSInfo

.\neo4j_uploaddata.ps1

UploadData -file.\ADCS_29032024100810.log

Рис. 4.73. Выполнение скриптов

Проверим, что у нас получилось, и выполним следующий Cypher-запрос в браузере neo4j.

MATCH (n) WHERE n: Template OR n: CA RETURN n

Отображение метки шаблонов сертификатов в BloodHound

Теперь добавим отображение метки шаблонов сертификатов в BloodHound. Все действия будут аналогичны действиям с метками, сделанным ранее.

Переходим в директорию src и открываем файл index.js на редактирование. Добавим информацию о том, как новая метка будет отображаться на графе. Находим строку global.AppStore и вставляем следующий код после блока Share:

…

global.appStore = {

dagre: true,

…

Share:{

font: "'Font Awesome 5 Free'",

content:'\uf07b',

scale:1.5,

color:'#f8d775',

},

Template:{

font: "'Font Awesome 5 Free'",

content:'\uf2c2',

scale:1.25,

color:'#cc0066',

},

…

Дальше ищем строку lowResPalette и добавляем название метки и цвет. Этот параметр отвечает за отображение узлов в низком разрешении.

…

lowResPalette:{

colorScheme:{

…

Base:'#E6E600',

LocalUser:'#E69717',

Share:'#f8d775',

Template:'#cc0066',

},

…

Сохраняем измененный файл.

Теперь переходим в директорию src\js и открываем на редактирование файл utils.js. В самом начале, в разделе const labels = [, добавляем новую метку после Domain.

const labels = [

…

'Domain',

'LocalUser',

'Share',

'Template',

…

Находим строку export async function setSchema() и в массив label добавляем название новой метки.

…

export async function setSchema() {

const luceneIndexProvider = "lucene+native-3.0"

let labels = ["User", "Group", …, "Domain", "Container", "Base", "LocalUser", "Share", "Template"…

Сохраняем измененный файл.

Теперь нужно добавить метку, чтобы она отображалась на графе. Для этого переходим в директорию components и открываем файл Graph.jsx. Находим строку switch (type) и добавляем в нее код:

…

switch (type) {

…

case 'Share':

node.type_share = true;

break;

case 'Template':

node.type_template = true;

break;

}

…

Сохраняем измененный файл.

Для отображения метки в строке поиска нужно открыть файл SearchRow.jsx, который находится в директории src\components\SearchContainer. Находим строку switch (type) и после блока Container добавляем код:

…

switch (type) {

…

case 'Share':

icon.className = 'fa fa-folder';

break;

case 'Template':

icon.className = 'fa fa-id-card';

break;

…

Сохраняем измененный файл.

Кроме отображения самой метки нужно добавить визуализацию свойств узла. В той же директории открываем файл TabContainer.jsx и добавляем импорт вкладки:

…

import LocalUserNodeData from './Tabs/LocalUserNodeData';

import ShareNodeData from './Tabs/ShareNodeData';

import TemplateNodeData from './Tabs/TemplateNodeData';

…

До нажатия на сам узел его свойства будут скрыты. Для этого в классе TabContainer находим строку this.state и добавляем строку:

…

class TabContainer extends Component {

constructor(props) {

super(props);

this.state = {

…

localuserVisible: false,

shareVisible: false,

templateVisible: false,

…

Дальше нужно добавить обработку при нажатии на узел. Для этого находим строку nodeClickHandler(type) и добавляем код:

…

nodeClickHandler(type) {

…

} else if (type === 'Share') {

this._shareNodeClicked();

} else if (type === 'Template') {

this._templateNodeClicked();

…

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

…

_shareNodeClicked() {

this.clearVisible()

this.setState({

shareVisible: true,

selected:2

});

}

_templateNodeClicked() {

this.clearVisible()

this.setState({

templateVisible: true,

selected:2

});

}

…

Ниже в функции отображения render() находим строку NoNodeData и добавляем следующий код:

…

render() {

…

<NoNodeData

visible={

…

!this.state.localuserVisible &&

!this.state.shareVisible &&

!this.state.templateVisible &&

…

И еще ниже добавим отображение вкладки со свойствами для TemplateNodeData:

…

<LocalUserNodeData visible={this.state.localuserVisible} />

<ShareNodeData visible={this.state.shareVisible} />

<TemplateNodeData visible={this.state.templateVisible} />

…

Сохраняем измененный файл.

Переходим в директорию src\components\Spotlight и открываем на редактирование файл SpotlightRow.jsx. В функции render находим строку switch (this.props.nodeType) и добавляем код:

…

render() {

let nodeIcon;

let parentIcon = '';

switch (this.props.nodeType) {

…

case 'Share':

nodeIcon = 'fa fa-folder';

break;

case 'Template':

nodeIcon = 'fa fa-id-card';

break;

default:

nodeIcon = '';

break;

}

…

Ниже находим строку switch (this.props.parentNodeType) и добавляем отображение родительской иконки:

…

switch (this.props.parentNodeType) {

…

case 'Share':

parentIcon = 'fa fa-folder';

break;

case 'Template':

parentIcon = 'fa fa-id-card';

break;

default:

parentIcon = '';

break;

}

…

Сохраняем измененный файл.

B завершение создадим вкладку с отображением свойств шаблона сертификата. Переходим в директорию src\components\SearchContainer\Tabs. Скопируем файл UserNodeData.jsx и назовем его TemplateNodeData.jsx. Шаги по изменению кода будут описаны в нем самом:

import React, {useEffect, useState} from 'react';

import clsx from 'clsx';

import CollapsibleSection from './Components/CollapsibleSection';

import NodeCypherLinkComplex from './Components/NodeCypherLinkComplex';

import NodeCypherLink from './Components/NodeCypherLink';

import NodeCypherNoNumberLink from './Components/NodeCypherNoNumberLink';

import MappedNodeProps from './Components/MappedNodeProps';

import ExtraNodeProps from './Components/ExtraNodeProps';

import NodePlayCypherLink from './Components/NodePlayCypherLink';

import {withAlert} from 'react-alert';

import {Table} from 'react-bootstrap';

import styles from './NodeData.module.css';

import {useContext} from 'react';

import {AppContext} from '../../../AppContext';

// Меняем название метки на TemplateNodeData

const TemplateNodeData = () => {

const [visible, setVisible] = useState(false);

const [objectId, setObjectId] = useState(null);

const [label, setLabel] = useState(null);

const [domain, setDomain] = useState(null);

const [nodeProps, setNodeProps] = useState({});

const context = useContext(AppContext);

useEffect(() => {

emitter.on('nodeClicked', nodeClickEvent);

return () => {

emitter.removeListener('nodeClicked', nodeClickEvent);

};

}, []);

const nodeClickEvent = (type, id, blocksinheritance, domain) => {

// Меняем название метки Template

if (type === 'Template') {

setVisible(true);

setObjectId(id);

setDomain(domain);

let session = driver.session();

session

// Меняем метку на Template

.run('MATCH (n:Template {objectid:$objectid}) RETURN n AS node', {

objectid: id,

})

.then((r) => {

let props = r.records[0].get('node'). properties;

setNodeProps(props);

setLabel(props.name || props.azname || objectid);

session.close();

});

} else {

setObjectId(null);

setVisible(false);

}

};

//Здесь определяется, какие свойства узла попадут

//в раздел NODE PROPERTIES, остальные будут

//отображаться в EXTRA PROPERTIES

const displayMap = {

displayname:'Display Name',

objectid:'Object ID',

enabled:'Enabled',

};

return objectId === null? (

<div></div>

):(

<div

className={clsx(

!visible && 'displaynone',

context.darkMode? styles.dark: styles.light

)}

>

<div className={clsx(styles.dl)}>

<h5>{label || objectId}</h5>

<MappedNodeProps

displayMap={displayMap}

properties={nodeProps}

label={label}

/>

<hr></hr>

<ExtraNodeProps

displayMap={displayMap}

properties={nodeProps}

label={label}

/>

<hr></hr>

// Удаляем разделы EXECUTION RIGHTS, OUTBOUND OBJECT // CONTROL

<CollapsibleSection header={'INBOUND CONTROL RIGHTS'}>

<div className={styles.itemlist}>

<Table>

<thead></thead>

<tbody className='searchable'>

<NodeCypherLink

property='Explicit Object Controllers'

target={objectId}

baseQuery={

'MATCH p=(n)-[r]->(u1:Template {objectid: $objectid}) WHERE r.isacl=true'

}

end={label}

distinct

/>

<NodeCypherLink

property='Unrolled Object Controllers'

target={objectId}

baseQuery={

'MATCH p=(n)-[r: MemberOf*1..]->(g: Group)-[r1:GenericAll|GenericWrite|WriteProperty|WriteDacl|WriteOwner|Owns]->(u: Template {objectid:$objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid'

}

end={label}

distinct

/>

<NodePlayCypherLink

property='Transitive Object Controllers'

target={objectId}

baseQuery={

'MATCH (n) WHERE NOT n.objectid=$objectid MATCH p = shortestPath((n)-[r1:MemberOf|GenericAll|GenericWrite|WriteProperty|WriteDacl|WriteOwner|Owns*1..]->(u1:Template {objectid:$objectid}))'

}

end={label}

distinct

/>

</tbody>

</Table>

</div>

</CollapsibleSection>

</div>

</div>

);

};

// Заменяем на TemplateNodeData

TemplateNodeData.propTypes = {};

// Заменяем на TemplateNodeData

export default withAlert()(TemplateNodeData);

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

npm run build: win32

Запустим обновленную версию BloodHound и проверим, что шаблоны сертификатов отображаются, как и задумывалось. В поле поиска найдем шаблон ESC1 и посмотрим, что мы получим на выходе. Свойства и метка должны отображаться корректно.

Рис. 4.74. Результат добавления новой метки

Отображение метки центра сертификации в BloodHound

Теперь приступим ко второй части и добавим метку самого центра сертификации. Открываем файл index.js на редактирование. Добавим информацию о том, как новая метка будет отображаться на графе. Находим строку global.AppStore и вставляем следующий код после блока Template:

…

global.appStore = {

dagre: true,

…

Template:{

font: "'Font Awesome 5 Free'",

content:'\uf2c2',

scale:1.25,

color:'#cc0066',

},

CA:{

font: "'Font Awesome 5 Free'",

content:'\uf66f',

scale:1.25,

color:'#FF3333',

},

…

Дальше ищем строку lowResPalette и добавляем название метки и цвет. Этот параметр отвечает за отображение узлов в низком разрешении.

…

lowResPalette:{

colorScheme:{

…

Share:'#f8d775',

Template:'#cc0066',

CA:'#FF3333',

},

…

Сохраняем измененный файл.

Теперь переходим в директорию src\js и открываем на редактирование файл utils.js. В самом начале, в разделе const labels = [, добавляем новую метку после Domain.

const labels = [

…

'Domain',

'LocalUser',

'Share',

'Template',

'CA',

…

Находим строку export async function setSchema() и в массив label добавляем название новой метки.

…

export async function setSchema() {

const luceneIndexProvider = "lucene+native-3.0"

let labels = ["User", "Group", …, "Domain", "Container", "Base", "LocalUser", "Share", "Template", "CA",…

Сохраняем измененный файл.

Теперь нужно добавить метку, чтобы она отображалась на графе. Для этого переходим в директорию components и открываем файл Graph.jsx. Находим строку switch (type) и добавляем в нее код:

…

switch (type) {

…

case 'Template':

node.type_template = true;

break;

case 'CA':

node.type_ca = true;

break;

}

…

Сохраняем измененный файл.

Для отображения метки в строке поиска нужно открыть файл SearchRow.jsx, который находится в директории src\components\SearchContainer. Находим строку switch (type) и после блока Container добавляем код:

…

switch (type) {

…

case 'Template':

icon.className = 'fa fa-id-card';

break;

case 'CA':

icon.className = 'fa fa-landmark';

break;

…

Сохраняем измененный файл.

Кроме отображения самой метки нужно добавить визуализацию свойств узла. В той же директории открываем файл TabContainer.jsx и добавим импорт вкладки:

…

import ShareNodeData from './Tabs/ShareNodeData';

import TemplateNodeData from './Tabs/TemplateNodeData';

import CANodeData from './Tabs/CANodeData';

…

До нажатия на сам узел его свойства будут скрыты. Для этого в классе TabContainer находим строку this.state и добавляем строку:

…

class TabContainer extends Component {

constructor(props) {

super(props);

this.state = {

…

templateVisible: false,

caVisible: false,

…

Дальше нужно добавить обработку при нажатии на узел. Для этого находим строку nodeClickHandler(type) и добавляем код:

…

nodeClickHandler(type) {

…

} else if (type === 'Template') {

this._templateNodeClicked();

} else if (type === 'CA') {

this._caNodeClicked();

…

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

…

_templateNodeClicked() {

this.clearVisible()

this.setState({

templateVisible: true,

selected:2

});

}

_caNodeClicked() {

this.clearVisible()

this.setState({

caVisible: true,

selected:2

});

}

…

Ниже в функции отображения render() находим строку NoNodeData и добавляем следующий код:

…

render() {

…

<NoNodeData

visible={

…

!this.state.shareVisible &&

!this.state.templateVisible &&

!this.state.caVisible &&

…

И еще ниже добавим отображение вкладки со свойствами для CANodeData:

…

<ShareNodeData visible={this.state.shareVisible} />

<TemplateNodeData visible={this.state.templateVisible} />

<CАNodeData visible={this.state.caVisible} />

…

Сохраняем измененный файл.

Переходим в директорию src\components\Spotlight и открываем на редактирование файл SpotlightRow.jsx. В функции render находим строку switch (this.props.nodeType) и добавляем код:

…

render() {

let nodeIcon;

let parentIcon = '';

switch (this.props.nodeType) {

…

case 'Template':

nodeIcon = 'fa fa-id-card';

break;

case 'CA':

nodeIcon = 'fa fa-landmark';

break;

default:

nodeIcon = '';

break;

}

…

Ниже находим строку switch (this.props.parentNodeType) и добавляем отображение родительской иконки:

…

switch (this.props.parentNodeType) {

…

case 'Template':

parentIcon = 'fa fa-id-card';

break;

case 'CA':

parentIcon = 'fa fa-landmark';

break;

default:

parentIcon = '';

break;

}

…

Сохраняем измененный файл.

B завершение создадим вкладку с отображением свойств центра сертификации. Переходим в директорию src\components\SearchContainer\Tabs. Сделаем копию файла UserNodeData.jsx и назовем его CANodeData.jsx:

import React, {useEffect, useState} from 'react';

import clsx from 'clsx';

import CollapsibleSection from './Components/CollapsibleSection';

import NodeCypherLinkComplex from './Components/NodeCypherLinkComplex';

import NodeCypherLink from './Components/NodeCypherLink';

import NodeCypherNoNumberLink from './Components/NodeCypherNoNumberLink';

import MappedNodeProps from './Components/MappedNodeProps';

import ExtraNodeProps from './Components/ExtraNodeProps';

import NodePlayCypherLink from './Components/NodePlayCypherLink';

import {withAlert} from 'react-alert';

import {Table} from 'react-bootstrap';

import styles from './NodeData.module.css';

import {useContext} from 'react';

import {AppContext} from '../../../AppContext';

const CANodeData = () => {

const [visible, setVisible] = useState(false);

const [objectId, setObjectId] = useState(null);

const [label, setLabel] = useState(null);

const [domain, setDomain] = useState(null);

const [nodeProps, setNodeProps] = useState({});

const context = useContext(AppContext);

useEffect(() => {

emitter.on('nodeClicked', nodeClickEvent);

return () => {

emitter.removeListener('nodeClicked', nodeClickEvent);

};

}, []);

const nodeClickEvent = (type, id, blocksinheritance, domain) => {

if (type === 'CA') {

setVisible(true);

setObjectId(id);

setDomain(domain);

let session = driver.session();

session

.run('MATCH (n:CA {objectid:$objectid}) RETURN n AS node', {

objectid: id,

})

.then((r) => {

let props = r.records[0].get('node'). properties;

setNodeProps(props);

setLabel(props.name || props.azname || objectid);

session.close();

});

} else {

setObjectId(null);

setVisible(false);

}

};

const displayMap = {

objectid:'Object ID',

};

return objectId === null? (

<div></div>

):(

<div

className={clsx(

!visible && 'displaynone',

context.darkMode? styles.dark: styles.light

)}

>

<div className={clsx(styles.dl)}>

<h5>{label || objectId}</h5>

<CollapsibleSection header='OVERVIEW'>

<div className={styles.itemlist}>

<Table>

<thead></thead>

<tbody className='searchable'>

<NodeCypherLink

property='Enabled Templates'

target={objectId}

baseQuery={

'MATCH p=(m: Template)-[r: HostedOn]->(n: CA {objectid:$objectid})'

}

end={label}

/>

</tbody>

</Table>

</div>

</CollapsibleSection>

<hr></hr>

<MappedNodeProps

displayMap={displayMap}

properties={nodeProps}

label={label}

/>

<hr></hr>

<ExtraNodeProps

displayMap={displayMap}

properties={nodeProps}

label={label}

/>

<hr></hr>

<CollapsibleSection header={'INBOUND CONTROL RIGHTS'}>

<div className={styles.itemlist}>

<Table>

<thead></thead>

<tbody className='searchable'>

<NodeCypherLink

property='Explicit Object Controllers'

target={objectId}

baseQuery={

'MATCH p=(n)-[r]->(u1:CA {objectid:$objectid}) WHERE r.isacl=true'

}

end={label}

distinct

/>

<NodeCypherLink

property='Unrolled Object Controllers'

target={objectId}

baseQuery={

'MATCH p=(n)-[r: MemberOf*1..]->(g: Group)-[r1:GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns|ManageCA|ManageCertificates]->(u: CA {objectid:$objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid'

}

end={label}

distinct

/>

<NodePlayCypherLink

property='Transitive Object Controllers'

target={objectId}

baseQuery={

'MATCH (n) WHERE NOT n.objectid=$objectid MATCH p = shortestPath((n)-[r1:MemberOf|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns|ManageCA|ManageCertificates*1..]->(u1:CA {objectid:$objectid}))'

}

end={label}

distinct

/>

</tbody>

</Table>

</div>

</CollapsibleSection>

</div>

</div>

);

};

CANodeData.propTypes = {};

export default withAlert()(CANodeData);

Сохраняем измененный файл и соберем решение.

npm run build: win32

После запуска BloodHound в строке поиска наберем DOMAIN-DC–CA и посмотрим, как выглядит наш центр сертификации (рис. 4.75).

В разделе OWERVIEW можно заметить, что у нас есть 16 опубликованных шаблонов сертификатов. Если нажать на эти цифры, мы получим все опубликованные шаблоны сертификатов для данного центра сертификации (рис. 4.76):

Добавление новых связей

При сборе информации и формировании Cypher-запросов мы создали новые связи, которых до этого не было:

● CanEnroll;

● CanAutoEnroll (потенциальная);

Рис. 4.75. Центр сертификации

Рис. 4.76. Опубликованные шаблоны сертификатов

● WriteProperty (потенциальная);

● RequestCertificates;

● ManageCA;

● ManageCertificates;

● Read.

Связь HostedOn мы добавили в общих ресурсах.

При запросах без указания определенных связей BloodHound нарисует граф, но, как мы уже знаем, при поиске путей между двумя объектами ничего не произойдет. Поэтому добавим все новые связи в BloodHound.

Открываем файл AppContainer.jsx, который находится в src, находим массив fullEdgeList и добавляем связи:

…

const fullEdgeList = [

…

'FullControl',

'CanEnroll',

'CanAutoEnroll',

'WriteProperty',

'RequestCertificates',

'ManageCA',

'ManageCertificates',

];

…

Сохраняем файл и теперь открываем файл index.js в том же каталоге, находим строчку global.appStore и двигаемся до edgeScheme. Там добавляем:

…

global.appStore = {

dagre: true,

…

edgeScheme:{

…

FullControl:'tapered',

CanEnroll:'tapered',

CanAutoEnroll:'tapered',

WriteProperty:'tapered',

RequestCertificates:'tapered',

ManageCA:'tapered',

ManageCertificates:'tapered',

Read:'tapered',

},

…

Доходим до lowResPalette и добавляем:

…

lowResPalette:{

…

edgeScheme:{

…

FullControl:'line',

CanEnroll:'line',

CanAutoEnroll:'line',

WriteProperty:'line',

RequestCertificates:'line',

ManageCA:'line',

ManageCertificates:'line',

Read:'line',

},

…

Находим строчку if (typeof conf.get('edgeincluded') === 'undefined') и там тоже добавляем строчку:

…

if (typeof conf.get('edgeincluded') === 'undefined') {

conf.set('edgeincluded', {

…

FullControl: true,

CanEnroll: true,

CanAutoEnroll: true,

WriteProperty: true,

RequestCertificates: true,

ManageCA: true,

ManageCertificates: true,

Read: true,

});

…

Сохраняем измененный файл и собираем приложение:

npm run build: win32

Запустим обновленную версию BloodHound и выполним несколько запросов путей. Этого будет достаточно для проверки корректного добавления связей.

Рис. 4.77. Проверка прямой связи

Или более сложный запрос:

Рис. 4.78. Проверка непрямой связи

Создание запросов

Все, что было сделано ранее, можно отнести к подготовительной работе. Теперь стоит воспользоваться собранными и обработанными данными для получения полезной информации. Для отладки запросов будем использовать браузер neo4j.

Глобальное подтверждение на запрос сертификата

Вне зависимости от того, какие недостатки есть в настройках шаблонов сертификатов, в первую очередь стоит проверить, стоит ли флаг на подтверждение выпуска сертификатов. За это будет отвечать свойство центра сертификации requestdisposition. Если он содержит PendingFirst, то все дальнейшие шаги будут зависеть от того, есть ли права на подтверждение запроса.

MATCH(c: CA) WHERE ANY (x IN c.requestdisposition WHERE x CONTAINS 'PendingFirst') RETURN c

Проверка ESC1

Для эксплуатации ESC1 должны выполняться следующие условия:

● authclient = TRUE

● enrolleesuppliessubject = TRUE

● pendallrequests = FALSE

● enabled = TRUE

В дополнение сразу запросим, в каком центре сертификации включен шаблон. В результате Cypher-запрос будет следующим:

MATCH p=((m)-[r: CanEnroll|MemberOf*1..]->(t: Template {authclient: TRUE, enrolleesuppliessubject: TRUE, pendallrequests: FALSE})-[r1:HostedOn]->(c: CA)) RETURN p

Рис. 4.79. Результат проверки ESC1

Проверка ESC2

Для эксплуатации ESC2 должны выполняться следующие условия:

● anypurpose = TRUE

● pendallrequests = FALSE

● enabled = TRUE

В дополнение сразу запросим, в каком центре сертификации включен шаблон. Cypher-запрос будет следующим:

MATCH p=((m)-[r: CanEnroll|MemberOf*1..]->(t: Template {anypurpose: TRUE, pendallrequests: FALSE})-[r1:HostedOn]->(c: CA)) RETURN p

Рис. 4.80. Результат проверки ESC2

Проверка ESC3

Для эксплуатации ESC3 требуется два шаблона и должны выполняться несколько условий кроме прав на запрос сертификата.

У одного из шаблонов сертификатов:

● requestagent = TRUE

Для другого:

● enrollmentagent = TRUE

● authclient = TRUE

Запрос Cypher будет следующим:

MATCH p=(m)-[r: CanEnroll|MemberOf*1..]->(t: Template)-[: HostedOn]->(c: CA) WHERE t.requestagent = TRUE OR (t.enrollmentagent = TRUE AND t.authclient=TRUE) RETURN p

Рис. 4.81. Результат проверки ESC3

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

MATCH (t1:Template {requestagent: TRUE, enabled: TRUE}) MATCH (t2:Template {enrollmentagent: TRUE, authclient: TRUE, enabled: TRUE}) MERGE (t1)-[r: CanEnroll]->(t2)

Совет

Этот запрос можно добавить в скрипт для сбора информации.

Повторим предыдущий запрос, в результате получим последовательность запросов сертификатов (рис. 4.82).

Рис. 4.82. Обновленный результат проверки ESC3

Проверка ESC4

Для эксплуатации ESC4 проверяем, кто какие права имеет на объекты шаблонов сертификатов (рис. 4.83). Могут быть следующие варианты:

● GenericAll

● GenericWrite

● WriteProperty

● WriteDacl

● WriteOwner

В дополнение уберем из запроса SID следующих групп и пользователей, это позволит получить более чистый вывод:

● Domain Admin

● Enterprise Admins

● Account Operators

● Administrators

● Administrator

● krbtgt

В результате Cypher-запрос получится следующим:

MATCH p=allshortestpaths((m)-[r: MemberOf|GenericAll|WriteDacl|WriteProperty|WriteOwner|Owns*1..]->(n: Template))

WHERE m<>n and NONE (x IN nodes(p) WHERE x.objectid =~ '(?i)S-1–5-.*-512|S-1–5-.*-519 |.*-544 |.*-500 |.*-502 |.*-548') WITH n AS Templates, p as p1

MATCH p2=(Templates)-[r1:HostedOn]->(c: CA)

RETURN p1, p2

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

Рис. 4.83. Проверка ESC4

Проверка ESC5

Для эксплуатации ESC5 требуется найти всех пользователей и группы, которые имеют привилегии локального администратора в центре сертификации. Для этого в запросе Cypher будет использоваться связь AdminTo.

Внимание

Необходимо помнить, что не только пользователи могут иметь права локального администратора на хосте, но и компьютеры.

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

Таким образом, запрос будет следующим:

MATCH(m: CA) WITH collect(m.dnshostname) as ca

MATCH p=(n)-[r: MemberOf|AdminTo*1..]->(c: Computer) WHERE c.name IN ca RETURN p

Внимание

Необходимо помнить, что BloodHound не всегда дает верную информацию о правах локального администратора (AdminTo) на хостах, поэтому в некоторых случаях необходимо проверять права локального администратора вручную.

Проверка ESC6

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

MATCH (c: CA) WHERE c.attributesubjectaltname2 = 'Enabled' RETURN c

Хотя имя сервера, на котором расположен центр сертификации, указано в свойстве dnshostname, можно улучшить запрос, добавив хост, на котором расположен уязвимый центр сертификации.

MATCH (c: CA) WHERE c.attributesubjectaltname2 = 'Enabled' with c.dnshostname as name, c as ca

MATCH (m: Computer) WHERE m.name = name RETURN m, ca

Проверка ESC7

Для эксплуатации ESC7 необходимо получить все короткие пути от любых объектов до центра сертификации с перечислением прав, специфичных для центра сертификации. Запрос Cypher получается такой (рис. 4.84):

MATCH p=allshortestpaths((m)-[r: MemberOf|ManageCA|ManageCertificates*1..]->(n: CA)) WHERE m<>n RETURN p

Рис. 4.84. Результат проверки ESC7

Проверка ESC8

Аналогично с ESC6, необходимо проверить наличие у свойства центра сертификации webenrollement состояния Enabled.

MATCH (c: CA) WHERE c.webenrollement = 'Enabled' RETURN c

Дальше уже выполняется техника Relay для получения сертификата.

Внимание

Настройки сервера центра сертификации могут запрещать входящий NTLM-трафик, но это уже другие настройки, не относящиеся к текущему проекту.

Проверка ESC9 и ESC10

Эксплуатация будет зависеть от множества факторов, например настроек KDC, которые обычный пользователь не сможет увидеть, если только они не настроены через групповые политики. Второй фактор – это наличие прав на изменение параметров объектов «пользователь», а вот третий фактор мы сможем посмотреть. Для этого найдем все шаблоны сертификатов, у которых установлен флаг CT_FLAG_NO_SECURITY_EXTENSION, в нашем случае это свойство nosecurityextension. Запрос Cypher будет следующим:

MATCH p=(t: Template)-[r: HostedOn]->(c: CA) WHERE t.nosecurityextension = TRUE RETURN p

Проверка ESC11

Возможность эксплуатации ESC11 зависит от отсутствия флага IF_ENFORCEENCRYPTICERTREQUEST. По умолчанию он всегда присутствует, тем не менее можно проверить. Запрос Cypher будет следующим:

MATCH (c: CA) WHERE c.enforceencrypticertrequest = 'Disabled' RETURN c

Добавление запросов в BloodHound

Скорей всего, результаты по ADCS будут постоянными, поэтому стоит все созданные выше запросы добавить в код BloodHound на постоянной основе. Ранее мы уже рассматривали добавление запроса в Pre-Build Analytics Queries вкладки Analysis, сейчас же мы будем добавлять запросы, связанные с ADCS, разработанные нами ранее.

Откроем на редактирование файл PrebuildQueries.json, который находится в директории src\components\SearchContainer\Tabs\. Перейдем к блоку Find Shortest Paths to Domain Admins и после него добавим следующий код:

{

"name": "ESC1",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": "MATCH p=((m)-[r: CanEnroll|MemberOf*1..]->(t: Template {authclient: TRUE, enrolleesuppliessubject: TRUE, pendallrequests: FALSE})-[r1:HostedOn]->(c: CA)) WHERE t.domain = $result RETURN p",

"allowCollapse":true

}

]

},

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

Для проверки сохраним файл и соберем приложение:

npm run build: win32

Запустим обновленную версию BloodHound, перейдем во вкладку Analysis и выполним добавленный запрос.

Рис. 4.85. Результат выполнения запроса

Если результат нас удовлетворяет, добавим остальные запросы в BloodHound.

Перед ESC1 добавим еще один запрос, отвечающий за отсутствие или наличие глобального подтверждения на выпуск сертификата:

{

"name": "Global Approvement",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": "MATCH(c: CA) WHERE ANY (x IN c.requestdisposition WHERE x CONTAINS 'PendingFirst') AND c.domain = $result RETURN c",

"allowCollapse":true

}

]

},

Во всех остальных запросах будут меняться только name и второй query, поэтому добавим их одним блоком.

{

"name": "ESC2",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": "MATCH p=((m)-[r: CanEnroll|MemberOf*1..]->(t: Template {anypurpose: TRUE, pendallrequests: FALSE})-[r1:HostedOn]->(c: CA)) WHERE t.domain = $result RETURN p",

"allowCollapse":true

}

]

},

{

"name": "ESC3",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": "MATCH p=(m)-[r: CanEnroll|MemberOf*1..]->(t: Template)-[r1:HostedOn]->(c: CA) WHERE (t.requestagent = TRUE OR (t.enrollmentagent = TRUE AND t.authclient=TRUE)) AND t.domain = $result RETURN p",

"allowCollapse":true

}

]

},

{

"name": "ESC4",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": " MATCH p=allshortestpaths((m)-[r: MemberOf|GenericAll|WriteDacl|WriteProperty|WriteOwner|Owns*1..]->(n: Template)) WHERE m<>n and n.domain = $result and NONE (x IN nodes(p) WHERE x.objectid =~ '(?i)S-1–5-.*-512|S-1–5-.*-519 |.*-544 |.*-500 |.*-502 |.*-548') WITH n AS Templates, p as p1 MATCH p2=(Templates)-[r1:HostedOn]->(c: CA) RETURN p1, p2",

"allowCollapse":true

}

]

},

{

"name": "ESC5",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": "MATCH(m: CA) WHERE m.domain = $result WITH collect(m.dnshostname) as ca MATCH p=(n)-[r: MemberOf|AdminTo*1..]->(c: Computer) WHERE c.name IN ca RETURN p",

"allowCollapse":true

}

]

},

{

"name": "ESC6",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": "MATCH (c: CA) WHERE c.attributesubjectaltname2 = 'Enabled' AND c.domain = $result RETURN c",

"allowCollapse":true

}

]

},

{

"name": "ESC7",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": " MATCH p=allshortestpaths((m)-[r: MemberOf|ManageCA|ManageCertificates*1..]->(n: CA)) WHERE m<>n AND n.domain = $result RETURN p",

"allowCollapse":true

}

]

},

{

"name": "ESC8",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": "MATCH (c: CA) WHERE c.webenrollement = 'Enabled' AND c.domain = $result RETURN c",

"allowCollapse":true

}

]

},

{

"name": "ESC9 & ESC10 only template",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": "MATCH p=(t: Template)-[r: HostedOn]->(c: CA) WHERE t.nosecurityextension = TRUE AND t.domain = $result RETURN c",

"allowCollapse":true

}

]

},

{

"name": "ESC11",

"category": "ADCS Paths",

"queryList":[

{

"final":false,

"title": "Select a Domain…",

"query": "MATCH (n: Domain) RETURN n.name ORDER BY n.name DESC"

},

{

"final":true,

"query": "MATCH (c: CA) WHERE c.enforceencrypticertrequest = 'Disabled' AND c.domain = $result RETURN c",

"allowCollapse":true

}

]

}

Совет

Рекомендуется собирать приложение после каждого добавленного блока, это избавит от поиска ошибок во всем коде, если вдруг что-то пойдет не так.

Сохраняем измененный файл и собираем приложение:

npm run build: win32

После сборки запустим обновленную версию BloodHound и перейдем во вкладку Analysis:

Рис. 4.86. Встроенные запросы для работы с шаблонами сертификатов

Добавление фильтров для запросов

Ранее в разделе Добавление атрибутов с правами WriteProperty мы уже добавляли фильтры для запросов, теперь то же самое сделаем для ADCS.

Открываем файл EdgeFilter.jsx, расположенный в \src\components\SearchContainer\EdgeFilter, находим div, где расположен MS Graph App Roles, и в него добавляем следующий код:

…

<EdgeFilterCheck name='AZMGGrantAppRoles' />

<EdgeFilterCheck name='AZMGGrantRole' />

<EdgeFilterSection

title='ADCS'

edges={[

'CanEnroll',

'CanAutoEnroll',

'WriteProperty',

'RequestCertificates',

'ManageCA',

'ManageCertificates',

'Read',

'HostedOn',

]}

sectionName='adcs'

/>

<EdgeFilterCheck name='CanEnroll' />

<EdgeFilterCheck name='CanAutoEnroll' />

<EdgeFilterCheck name='WriteProperty' />

<EdgeFilterCheck name='RequestCertificates' />

<EdgeFilterCheck name='ManageCA' />

<EdgeFilterCheck name='ManageCertificates' />

<EdgeFilterCheck name='Read' />

<EdgeFilterCheck name='HostedOn' />

</div>

…

Сохраняем измененный файл и собираем приложение:

npm run build: win32

Запускаем обновленную версию BloodHound. Нажимаем на фильтр и в результате видим, что появился новый раздел ADCS (рис 4.87).

Рис. 4.87. Добавленные фильтры

Добавление статистики

Заключительным штрихом будет добавление статистики во вкладку Database Info. Для этого откроем файл DatabaseDataDisplay.jsx, который находится в директории src\components\SearchContainer\Tabs. Находим раздел ONPREM OBJECTS и после статистики о групповых политиках добавляем запросы о центрах сертификации и шаблонах сертификатов:

<CollapsibleSection header='ON-PREM OBJECTS'>

<Table hover striped responsive>

…

<DatabaseDataLabel

query={'MATCH (n: GPO) RETURN count(n) AS count'}

index={index}

label={'GPOs'}

/>

<DatabaseDataLabel

query={'MATCH (n: CA) RETURN count(n) AS count'}

index={index}

label={'CAs'}

/>

<DatabaseDataLabel

query={'MATCH (n: Template) RETURN count(n) AS count'}

index={index}

label={'Certificate Templates'}

/>

…

Сохраним измененный файл и соберем наше приложение:

npm run build: win32

Запускаем обновленную версию BloodHound. Переходим во вкладку Database Info и можем наблюдать добавленную статистику по центрам сертификации и шаблонам сертификации.

Рис. 4.88. Статистика

Вместо заключения

BloodHound и neo4j предоставляют огромные возможности для анализа данных. И хотя в книге были рассмотрены только некоторые объекты инфраструктуры, их количество зависит только от фантазии.

В книге были рассмотрены различные примеры улучшения BloodHound, начиная с создания новых меток и связей и заканчивая добавлением новых интерфейсов в управление.

Сноски

1

https://github.com/Seyaji/adelante.

2

https://github.com/vletoux/pingcastle.

3

https://github.com/BloodHoundAD/BloodHound.

4

https://github.com/davidprowe/BadBlood.

5

https://jdk.java.net/java-se-ri/11-MR2.

6

https://neo4j.com/.

7

https://github.com/BloodHoundAD/BloodHound/releases.

8

https://github.com/BloodHoundAD/SharpHound/releases.

9

https://github.com/SadProcessor/HandsOnBloodHound/blob/master/BH21/BH4_SharpHound_Cheat.pdf.

10

https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/4.4.0.1

11

https://neo4j.com/docs/cypher-manual/current/introduction/.

12

https://nodejs.org/en/download/

13

https://git-scm.com/download/win.

14

https://github.com/BloodHoundAD/BloodHound.

15

https://github.com/PowerShellMafia/PowerSploit/blob/master/Recon/PowerView.ps1.

16

https://github.com/S3cur3Th1sSh1t/Creds/blob/master/PowershellScripts/Invoke-SMBNegotiate.ps1.

17

https://learn.microsoft.com/en-us/windows/win32/ADSchema/active-directory-schema.

18

https://github.com/GhostPack/Certify.

19

https://github.com/ly4k/Certipy.

Content

Дмитрий Неверов
Идём по киберследу: Анализ защищенности Active Directory c помощью утилиты BloodHound

Вступление

01. Общая информация и настройка лаборатории

Что такое BloodHound

Область применения

Настройка лаборатории

Установка neo4j

Установка BloodHound

02. Знакомство с SharpHound, BloodHound и neo4j

SharpHound

Интерфейс BloodHound

База данных neo4j

03. Дрессируем собаку. Язык запросов Cypher

Основные принципы

Оператор MATCH

Оператор OPTIONAL MATCH

Условия фильтрации запросов

Оператор RETURN

Оператор WITH

Добавление и изменение свойств

Работа со списками

Условие «если… то»

Работа со временем

Функции для работы со строками

Создание и удаление узлов и связей

Загрузка информации в базу данных

Загрузка данных через CSV-файл

Создание утилиты для загрузки данных

04. Учим старую собаку новым трюкам

Настройка окружения и проверка сборки

Изменение информации о программе (About)

Изменение запроса в Shortest Path from Owned Principals

Добавление собственных запросов

Локальная учетная запись с правами администратора

Повторно используемые пароли

Доступность хостов

Разбор inf-файлов в групповых политиках

Добавление атрибутов с правами WriteProperty

Общие файловые ресурсы

Центр сертификации

Вместо заключения

Сноски