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


Рис. 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@D
OMAIN.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