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

Первым запросом мы выбираем из базы все узлы со значением

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.