Проходимые и непроходимые связи в BloodHound
Специалисты, которые самостоятельно пишут Cypher запросы для коротких путей, знают, что в некоторых случаях neo4j выдает не те графы, которые хочется увидеть. Особенно, когда строятся короткие пути от общих групп до группы доменных администраторов. Часто рисуются графы, связанные с шаблонами сертификатов и членством в локальных группах и не только в привилегированных. Эти графы практически невозможно преобразовать в цепочку атаки.
В этом случае надо фильтровать связи в самом Cypher запросе при построении коротких путей. Рассмотрим два варианта Cypher запросов. В перовом варианте указываем все интересующие связи в квадратных скобках и в конце добавляем бесконечное число прыжков:
MATCH p=AllshortestPaths((c:Group {name:"DOMAIN USERS@DOMAIN.LOCAL"})-[r:GenericAll|GenericWrite|DCSync|...*1..]->(v:Group {name:"DOMAIN ADMINS@DOMAIN.LOCAL"})) RETURN p
В другом варианте будем использовать оператор IN и исключать неинтересные нам связи:
MATCH p=AllshortestPaths((c:Group {name:"DOMAIN USERS@DOMAIN.LOCAL"})-[*]->(v:Group {name:"DOMAIN ADMINS@DOMAIN.LOCAL"})) WHERE NONE(x IN relationships(p) WHERE type(x)='CrossForestTrust' OR type(x)='DelegatedEnrollmentAgent' OR ...) RETURN p
Писать такие запросы очень неудобно и можно создать шаблоны и использовать их. Но пойдем другим путем.
Разработчики BloodHound разделяют ребра (связи) графа на два типа: проходимые (traversable) и непроходимые (non-traversable). Первый тип позволяет захватывать конечный узел в графе, второй тип – это связи, которые могут не предоставлять захват конечного узла в графе.
Зная какие связи являются проходимыми к ним можно добавить свойство is_traversable и установить значение TRUE. Чтобы не писать вручную каждый Cypher запрос напишем простой ps-скрипт, который сгенерирует нам Cypher запросы на добавление свойства для каждой связи:
$relationships = @("AbuseTGTDelegation","ADCSESC1","ADCSESC10a","ADCSESC10b","ADCSESC13","ADCSESC3","ADCSESC4","ADCSESC6a","ADCSESC6b","ADCSESC9a","ADCSESC9b","AddAllowedToAct","AddKeyCredentialLink","AddMember","AddSelf","AdminTo","AllExtendedRights","AllowedToAct","AllowedToDelegate","CanPSRemote","CanRDP","ClaimSpecialIdentity","CoerceAndRelayNTLMToADCS","CoerceAndRelayNTLMToLDAP","CoerceAndRelayNTLMToLDAPS","CoerceAndRelayNTLMToSMB","CoerceToTGT","Contains","CrossForestTrust","DCFor","DCSync","DumpSMSAPassword","ExecuteDCOM","ForceChangePassword","GenericAll","GenericWrite","GoldenCert","GPLink","HasSIDHistory","HasSession","HasTrustKeys","ManageCA","ManageCertificates","MemberOf","Owns","OwnsLimitedRights","ReadGMSAPassword","ReadLAPSPassword","SameForestTrust","SpoofSIDHistory","SQLAdmin","SyncedToADUser","SyncedToEntraUser","SyncLAPSPassword","WriteAccountRestrictions","WriteDacl","WriteGPLink","WriteOwner","WriteOwnerLimitedRights","WriteSPN")
foreach($relationship in $relationships){
Write-Host "MATCH(m)-[r:$relationship]->(n) SET r.is_traversable = TRUE;"
После выполнения вставим все эти запросы в базу neo4j через browser neo4j.
Название свойства is_traversable взято из документации по OpenGraph, но в самом BloodHound CE данное свойство не используется, и запросы строятся перечислением всех проходимых связей.
Теперь чтобы воспользоваться новым свойством связи изменим наш поиск коротких путей на:
MATCH p=AllshortestPaths((c:Group {name:"DOMAIN USERS@DOMAIN.LOCAL"})-[*]->(v:Group {name:"DOMAIN ADMINS@DOMAIN.LOCAL"})) WHERE ALL(x IN relationships(p) WHERE x.is_traversable = TRUE) RETURN p
В этом запросе мы просто говорим neo4j, что у всех связей в пути должно быть свойство is_traversable в значении TRUE.
На двух изображениях одна и таже инфраструктура и узлы начала и конца, но во втором варианте мы используем только проходимые ребра, в результате картина более ясная.
Аналогично можно сделать с непроходимыми связями просто изменив набор на непроходимые связи и поставить значение свойства is_traversable в FALSE.
При построении графов нужно использовать различные варианты фильтрации запросов и тогда результат будет полным.