├── .gitignore ├── images ├── logo.png └── logo-pointer.png ├── requirements.txt ├── Dockerfile ├── conf ├── languages.yaml ├── defaults.py └── constants.py ├── queries ├── tactics │ ├── discovery │ │ ├── domain_group_names.yaml │ │ ├── domain_user_names.yaml │ │ ├── domain_computer_names.yaml │ │ ├── domain_names.yaml │ │ ├── dcs.yaml │ │ ├── gpo_path.yaml │ │ ├── domain_trust_relationships.yaml │ │ ├── sql_servers.yaml │ │ ├── adcs │ │ │ ├── find_certificate_authorities.yaml │ │ │ └── find_certificate_templates.yaml │ │ ├── highvalue_users_enabled.yaml │ │ ├── spn.yaml │ │ ├── domain_administrators.yaml │ │ ├── ip_domain_computer.yaml │ │ ├── dc_administrators.yaml │ │ ├── first_three_levels_ou_hierarchy.yaml │ │ ├── home_directories.yaml │ │ ├── secrets_in_descriptions.yaml │ │ ├── highvalue_users_cannot_be_delegated.yaml │ │ ├── urls_in_descriptions.yaml │ │ ├── disabled_computers.yaml │ │ ├── user_pwd_never_expires.yaml │ │ ├── computers_and_os.yaml │ │ ├── objects_without_relationships.yaml │ │ ├── highvalue_groups_relationships.yaml │ │ ├── highvalue_users_can_be_delegated.yaml │ │ ├── inbound_domain_relationships.yaml │ │ ├── outbound_domain_relationships.yaml │ │ ├── emails.yaml │ │ ├── gpos_and_objects.yaml │ │ ├── disabled_highvalue_users.yaml │ │ ├── objects_in_tiers.yaml │ │ ├── disabled_users.yaml │ │ ├── users_and_sessions.yaml │ │ ├── disabled_users_never_logged_in.yaml │ │ ├── foreign_security_principals.yaml │ │ ├── shared_resources_in_descriptions.yaml │ │ ├── computers_and_sessions.yaml │ │ ├── domain-admins_enterprise-admins_adminitrators_users.yaml │ │ ├── ous_and_computers.yaml │ │ ├── same_user_displayname_on_other_domains.yaml │ │ ├── non-highvalue_users_admincount_enabled.yaml │ │ ├── computers_without_laps.yaml │ │ ├── same_usernames_in_different_domains.yaml │ │ ├── ad_connect_user.yaml │ │ ├── different_usernames_with_same_email.yaml │ │ ├── krbtgt_pwd_not_changed.yaml │ │ ├── users_in_privileged_groups.yaml │ │ ├── low_level_group_permissions.yaml │ │ ├── different_usernames_with_same_displayname.yaml │ │ ├── enabled_not_logged_in_last_year.yaml │ │ ├── domain_admins_and_sessions.yaml │ │ └── potential_admins_users_not_in_highvalue_groups.yaml │ ├── collection │ │ ├── descriptions.yaml │ │ └── job_title.yaml │ ├── credential_access │ │ ├── cleartext_pwd.yaml │ │ ├── users_pwd_not_required.yaml │ │ ├── read_gmsa_passwords.yaml │ │ ├── kerberoasting.yaml │ │ ├── as-rep_roasting.yaml │ │ ├── kerberoasting_high-value_users.yaml │ │ ├── non-da_with_generic_rights.yaml │ │ ├── users_same_timestamp_password.yaml │ │ └── computers_same_timestamp_password.yaml │ ├── lateral_movement │ │ ├── computer_admin_other_computers.yaml │ │ ├── users_groups_can_psremote.yaml │ │ ├── users_groups_can_rdp.yaml │ │ ├── outdated_computers.yaml │ │ ├── users_with_admin_rights_to_computers.yaml │ │ ├── non-da_rdp_dc.yaml │ │ ├── non-da_psremote_dc.yaml │ │ ├── non-da_with_admin_rights_dc.yaml │ │ ├── non-da_with_admin_rights_computers.yaml │ │ └── non-highvalue_users_can_dcsync.yaml │ ├── persistence │ │ ├── sid_history.yaml │ │ ├── constrained_domain_persistence.yaml │ │ └── unconstrained_domain_persistence.yaml │ ├── execution │ │ ├── users_with_write_permissions_to_ous.yaml │ │ └── users_with_genericwrite_permission_to_other_users.yaml │ └── privilege_escalation │ │ ├── get_dns_admins.yaml │ │ ├── get_schema_admins.yaml │ │ ├── non-highvalue_gpos_relationship.yaml │ │ ├── non-highvalue_groups_reach_admins.yaml │ │ ├── samaccountname_spoofing.yaml │ │ ├── adcs │ │ ├── find_ca_with_http_web_enrollment_esc8.yaml │ │ ├── find_misconfigured_certificate_templates_esc2.yaml │ │ ├── find_misconfigured_enrollment_agent_templates_esc3.yaml │ │ └── find_misconfigured_certificate_templates_esc1.yaml │ │ ├── non-da_with_writing_rights_to_computers.yaml │ │ ├── ghost_spn.yaml │ │ ├── non-highvalue_users_reach_da.yaml │ │ ├── non-highvalue_objects_reach_highvalue_objects.yaml │ │ ├── unconstrained_delegation.yaml │ │ ├── constrained_delegation.yaml │ │ └── resource-based_constrained_delegation.yaml └── permissions │ ├── sql_admin.yaml │ ├── add_self.yaml │ ├── can_rdp.yaml │ ├── admin_to.yaml │ ├── add_member.yaml │ ├── can_ps_remote.yaml │ ├── sync_laps_password.yaml │ ├── write_dacl.yaml │ ├── execute_dcom.yaml │ ├── read_laps_password.yaml │ ├── dcsync.yaml │ ├── read_gmsa_password.yaml │ ├── force_change_password.yaml │ ├── owner.yaml │ ├── add_key-credential-link.yaml │ ├── write_spn.yaml │ ├── allowed_to_act.yaml │ ├── allowed_to_delegate.yaml │ ├── all_extended_rights.yaml │ ├── generic_write.yaml │ └── generic_all.yaml ├── modules ├── error.py ├── view.py ├── io.py ├── presenter.py ├── controller.py └── model.py ├── .github └── workflows │ └── codeql-analysis.yml ├── rastreator.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .shell_history 3 | __pycache__ 4 | output/ 5 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e1abrador/rastreator/master/images/logo.png -------------------------------------------------------------------------------- /images/logo-pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e1abrador/rastreator/master/images/logo-pointer.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | neo4j==4.0.1 2 | prettytable==0.7.2 3 | prompt_toolkit==3.0.5 4 | pygments==2.7.4 5 | pyyaml 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | MAINTAINER RastreatorTeam 3 | 4 | ADD . /opt/ 5 | WORKDIR /opt/ 6 | 7 | RUN pip3 install -r requirements.txt 8 | 9 | ENTRYPOINT ["python","rastreator.py"] -------------------------------------------------------------------------------- /conf/languages.yaml: -------------------------------------------------------------------------------- 1 | en: 2 | RAS-A: ADMINISTRATORS 3 | RAS-DA: DOMAIN ADMINS 4 | RAS-DC: DOMAIN CONTROLLERS 5 | RAS-EA: ENTERPRISE ADMINS 6 | es: 7 | RAS-A: ADMINISTRADORES 8 | RAS-DA: ADMINS. DEL DOMINIO 9 | RAS-DC: CONTROLADORES DE DOMINIO 10 | RAS-EA: ADMINISTRADORES DE ORGANIZACIÓN 11 | -------------------------------------------------------------------------------- /queries/tactics/discovery/domain_group_names.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: domain_group_names 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get domain group names. 7 | statement: 8 | main: >- 9 | match (g:Group{domain:'RAS-DOMAIN'}) 10 | return g.name 11 | order by g.name 12 | -------------------------------------------------------------------------------- /queries/tactics/discovery/domain_user_names.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: domain_user_names 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get domain user names. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | return u.name 11 | order by u.name 12 | -------------------------------------------------------------------------------- /queries/tactics/discovery/domain_computer_names.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: domain_computer_names 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get domain computer names. 7 | statement: 8 | main: >- 9 | match (c:Computer{enabled:true, domain:'RAS-DOMAIN'}) 10 | return c.name 11 | order by c.name 12 | -------------------------------------------------------------------------------- /queries/tactics/discovery/domain_names.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: domain_names 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get domain names. 7 | statement: 8 | main: >- 9 | match (d:Domain) 10 | return d.name as Name, coalesce(d.objectsid, d.objectid) as SID, d.functionallevel as FunctionalLevel 11 | order by Name 12 | -------------------------------------------------------------------------------- /queries/tactics/discovery/dcs.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: dcs 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get Domain Controllers. 7 | statement: 8 | main: >- 9 | match (c:Computer{domain:'RAS-DOMAIN'})-[:MemberOf]->(dc:Group{name:'RAS-DC@RAS-DOMAIN'}) 10 | return c.name as Name, c.operatingsystem as OS 11 | order by Name 12 | -------------------------------------------------------------------------------- /queries/tactics/discovery/gpo_path.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: gpo_path 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get GPO and their path. 7 | statement: 8 | main: >- 9 | match (g:GPO{domain:'RAS-DOMAIN'}) 10 | return g.name, g.gpcpath 11 | order by g.name, g.gpcpath 12 | nextsteps: 13 | rt: 14 | - Review the contents of each GPO folder. 15 | -------------------------------------------------------------------------------- /queries/tactics/discovery/domain_trust_relationships.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: domain_trust_relationships 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get domain trust relationships. 7 | statement: 8 | main: >- 9 | match (d1:Domain)-[:TrustedBy]->(d2:Domain) 10 | return distinct d1.name as trustee, d2.name as truster 11 | order by trustee, truster 12 | -------------------------------------------------------------------------------- /queries/tactics/discovery/sql_servers.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: sql_servers 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get SQL servers. 7 | statement: 8 | main: >- 9 | match (n{enabled:true, domain:'RAS-DOMAIN'}) 10 | unwind n.serviceprincipalnames as spn 11 | with spn 12 | where spn =~ '(?i)MSSQLSvc/.*' 13 | return split(spn, '/')[1] as Computer 14 | -------------------------------------------------------------------------------- /queries/tactics/discovery/adcs/find_certificate_authorities.yaml: -------------------------------------------------------------------------------- 1 | author: e1abrador (Eric Labrador) 2 | name: find_certificate_authorities 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get Certificate Authorities. 7 | statement: 8 | main: >- 9 | match (ca:CA{domain:'RAS-DOMAIN'}) 10 | return distinct ca.name as `Certificate Authority` 11 | order by `Certificate Authority` asc 12 | -------------------------------------------------------------------------------- /queries/tactics/discovery/highvalue_users_enabled.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: highvalue_users_enabled 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get all enabled highvalue users. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{highvalue:true, domain:'RAS-DOMAIN'}) 10 | return distinct u.name 11 | order by u.name 12 | -------------------------------------------------------------------------------- /queries/tactics/discovery/spn.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: spn 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get users and computers with SPN. 7 | statement: 8 | main: >- 9 | match (n{enabled:true, domain:'RAS-DOMAIN'}) 10 | unwind labels(n) as type 11 | unwind n.serviceprincipalnames as spn 12 | with n, spn, type 13 | return n.name, type, spn 14 | order by n.name, type, spn 15 | -------------------------------------------------------------------------------- /queries/tactics/discovery/domain_administrators.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: domain_administrators 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get Domain Administrators. 7 | statement: 8 | main: >- 9 | match (n{enabled:true, domain:'RAS-DOMAIN'})-[r:MemberOf*1..]->(g:Group{name:'RAS-DA@RAS-DOMAIN'}) 10 | unwind labels(n) as type 11 | return distinct n.name, type 12 | order by type asc 13 | -------------------------------------------------------------------------------- /queries/tactics/collection/descriptions.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: descriptions 3 | state: enabled 4 | tactic: collection 5 | tag: analysis 6 | description: Get descriptions. 7 | statement: 8 | main: >- 9 | match (n{domain:'RAS-DOMAIN'}) 10 | where not n.description is null 11 | return n.name, n.description 12 | order by n.name 13 | nextsteps: 14 | rt: 15 | - Search for sensitive information. 16 | bt: 17 | - Remove sensitive information. 18 | -------------------------------------------------------------------------------- /queries/tactics/discovery/ip_domain_computer.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: ip_domain_computer 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get domain computer IP addresses. 7 | statement: 8 | main: >- 9 | match (c:Computer{enabled:true, domain:'RAS-DOMAIN'}) 10 | where c.name =~ '(?i)^([0-9]{1,3}\\.){3}[0-9]{1,3}$' or c.description =~ '(?i)^([0-9]{1,3}\\.){3}[0-9]{1,3}$' 11 | return c.name, c.description 12 | order by c.name 13 | -------------------------------------------------------------------------------- /queries/tactics/collection/job_title.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: job_title 3 | state: enabled 4 | tactic: collection 5 | tag: analysis 6 | description: Get job title. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | where not u.title is null 11 | return u.name, u.title 12 | order by u.name 13 | nextsteps: 14 | rt: 15 | - Perform spear phishing attacks. 16 | bt: 17 | - Check the need to have job titles in AD. 18 | -------------------------------------------------------------------------------- /queries/tactics/credential_access/cleartext_pwd.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: cleartext_pwd 3 | state: enabled 4 | tactic: credential access 5 | tag: issue 6 | description: Get cleartext passwords ('userpassword' attribute). 7 | statement: 8 | main: >- 9 | match (u:User{domain:'RAS-DOMAIN'}) 10 | where not u.userpassword is null 11 | return u.name, u.userpassword, u.enabled 12 | nextsteps: 13 | rt: 14 | - Perform lateral movement. 15 | bt: 16 | - Remove passwords. 17 | -------------------------------------------------------------------------------- /queries/tactics/discovery/dc_administrators.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: dc_administrators 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get Domain Controller Administrators. 7 | statement: 8 | main: >- 9 | match (c:Computer{domain:'RAS-DOMAIN'}), (dc:Group{name:'RAS-DC@RAS-DOMAIN'}) 10 | with c, dc 11 | match (n)-[:AdminTo]->(c)-[:MemberOf]->(dc) 12 | unwind labels(n) as type 13 | return distinct n.name, type 14 | order by type asc 15 | -------------------------------------------------------------------------------- /queries/tactics/discovery/first_three_levels_ou_hierarchy.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: first_two_levels_ou_hierarchy 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get first two levels of OU hierarchy. 7 | statement: 8 | main: >- 9 | match (:Domain{name:'RAS-DOMAIN'})-->(o1:OU) 10 | optional match (o1)-->(o2:OU) 11 | with o1, o2 12 | optional match (o2)-->(o3:OU) 13 | return o1.name as L1, coalesce(o2.name, '') as L2, coalesce(o3.name, '') as L3 14 | order by L1, L2, L3 15 | -------------------------------------------------------------------------------- /queries/tactics/discovery/home_directories.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: home_directories 3 | state: enabled 4 | tactic: discovery 5 | tag: attack 6 | description: Get home directories. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | where not u.homedirectory is null 11 | return u.name, u.homedirectory 12 | order by u.name, u.homedirectory 13 | nextsteps: 14 | rt: 15 | - Check security descriptors. 16 | - Access them. 17 | bt: 18 | - Check security descriptors. 19 | -------------------------------------------------------------------------------- /queries/tactics/discovery/secrets_in_descriptions.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: secrets_in_descriptions 3 | state: enabled 4 | tactic: discovery 5 | tag: attack 6 | description: Get secrets in descriptions. 7 | statement: 8 | main: >- 9 | match (n{domain:'RAS-DOMAIN'}) 10 | where n.description =~ '(?i).*(secret|key|pass|wd|pw)[a-zA-Z0-9_-]*[ "\']?[=:].*' 11 | return n.name, n.description 12 | order by n.name, n.description 13 | nextsteps: 14 | rt: 15 | - Use them. 16 | bt: 17 | - Remove them if not necessary. 18 | -------------------------------------------------------------------------------- /queries/tactics/discovery/highvalue_users_cannot_be_delegated.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: highvalue_users_cannot_be_delegated 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get highvalue users that cannot be delegated. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN', sensitive:true})-[:MemberOf*1..]->(g:Group{highvalue:true, domain:'RAS-DOMAIN'}) 10 | return distinct u.name 11 | order by u.name asc 12 | nextsteps: 13 | rt: 14 | - Avoid them in delegation attacks. 15 | -------------------------------------------------------------------------------- /queries/tactics/discovery/urls_in_descriptions.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: urls_in_descriptions 3 | state: enabled 4 | tactic: discovery 5 | tag: attack 6 | description: Get URLs in descriptions. 7 | statement: 8 | main: >- 9 | match (n{domain:'RAS-DOMAIN'}) 10 | where n.description =~ '(?i).*https?(://).*' and not n.description contains 'LinkId=298939' 11 | return n.name, n.description 12 | order by n.name, n.description 13 | nextsteps: 14 | rt: 15 | - Access them. 16 | bt: 17 | - Remove them if not necessary. 18 | -------------------------------------------------------------------------------- /queries/tactics/discovery/disabled_computers.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: disabled_computers 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get disabled computers. 7 | statement: 8 | main: >- 9 | match (c:Computer{enabled:false, domain:'RAS-DOMAIN'}) 10 | return c.name 11 | order by c.name asc 12 | count: 13 | - return .* order/return count(c.name) as count order 14 | - order .*/ 15 | nextsteps: 16 | bt: 17 | - Remove them after a period of time. 18 | - Use a strong computer policy. 19 | -------------------------------------------------------------------------------- /queries/tactics/discovery/user_pwd_never_expires.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: user_pwd_never_expires 3 | state: enabled 4 | tactic: discovery 5 | tag: issue 6 | description: Get users which password never expires. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN', pwdneverexpires:true}) 10 | return u.name 11 | order by u.name asc 12 | nextsteps: 13 | rt: 14 | - Perform credential stuffing attacks. 15 | bt: 16 | - Configure all users with password expiration. 17 | - Use a strong password policy. 18 | -------------------------------------------------------------------------------- /queries/tactics/credential_access/users_pwd_not_required.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: users_pwd_not_required 3 | state: enabled 4 | tactic: credential access 5 | tag: issue 6 | description: Get users which password is not required. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | where u.passwordnotreqd=true 11 | return u.name 12 | order by u.name asc 13 | nextsteps: 14 | rt: 15 | - Check if password is empty. 16 | - Perform lateral movement. 17 | bt: 18 | - Disable them if they are unneeded. 19 | -------------------------------------------------------------------------------- /queries/tactics/discovery/computers_and_os.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: computers_and_os 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get computer names and operating systems. 7 | statement: 8 | main: >- 9 | match (c:Computer{enabled:true, domain:'RAS-DOMAIN'}) 10 | return c.name as Name, c.operatingsystem as OperatingSystem 11 | order by OperatingSystem, Name asc 12 | count: 13 | - return .* order/return c.operatingsystem as OperatingSystem, count(*) as Count order 14 | - order .*/order by Count desc 15 | -------------------------------------------------------------------------------- /queries/tactics/discovery/objects_without_relationships.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: objects_without_relationships 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get objects without relationships. 7 | statement: 8 | main: >- 9 | match (n{domain:'RAS-DOMAIN'}) 10 | where not ()-->(n) and (n)-->() and (not labels(n) in [['User'], ['Computer']] or n.enabled = true) 11 | unwind labels(n) as type 12 | return n.name, type 13 | order by type, n.name 14 | nextsteps: 15 | bt: 16 | - Disable them if they are unneeded. 17 | -------------------------------------------------------------------------------- /queries/tactics/discovery/highvalue_groups_relationships.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: highvalue_groups_relationships 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Relationships between highvalue groups. 7 | statement: 8 | main: >- 9 | match (g:Group{highvalue:true, domain:'RAS-DOMAIN'})-[r]->(p:Group{highvalue:true, domain:'RAS-DOMAIN'}) 10 | return g.name, type(r), p.name 11 | order by type(r), g.name, p.name asc 12 | nextsteps: 13 | rt: 14 | - Take advantage of wrong relationships. 15 | bt: 16 | - Correct wrong relationships. 17 | -------------------------------------------------------------------------------- /queries/tactics/discovery/highvalue_users_can_be_delegated.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: highvalue_users_can_be_delegated 3 | state: enabled 4 | tactic: discovery 5 | tag: issue 6 | description: Get highvalue users that can be delegated. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN', sensitive:false})-[:MemberOf*1..]->(g:Group{highvalue:true, domain:'RAS-DOMAIN'}) 10 | return distinct u.name 11 | order by u.name asc 12 | nextsteps: 13 | rt: 14 | - Use them in delegation attacks. 15 | bt: 16 | - Remove highvalue user delegation. 17 | -------------------------------------------------------------------------------- /queries/tactics/discovery/inbound_domain_relationships.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: inbound_domain_relationships 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get inbound domain relationships. 7 | statement: 8 | main: >- 9 | match (n{enabled:true}) 10 | with n 11 | match (m{enabled:true, domain:'RAS-DOMAIN'}) 12 | where not m.domain = 'RAS-DOMAIN' 13 | with n, m 14 | match (n)-->(m) 15 | unwind labels(n) as typen 16 | unwind labels(m) as typem 17 | return typen, n.name, typem, m.name 18 | order by typen, n.name, typem, m.name 19 | -------------------------------------------------------------------------------- /queries/tactics/discovery/outbound_domain_relationships.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: outbound_domain_relationships 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get outbound domain relationships. 7 | statement: 8 | main: >- 9 | match (n{enabled:true, domain:'RAS-DOMAIN'}) 10 | with n 11 | match (m{enabled:true}) 12 | where not m.domain = 'RAS-DOMAIN' 13 | with n, m 14 | match (n)-->(m) 15 | unwind labels(n) as typen 16 | unwind labels(m) as typem 17 | return typen, n.name, typem, m.name 18 | order by typen, n.name, typem, m.name 19 | -------------------------------------------------------------------------------- /queries/tactics/lateral_movement/computer_admin_other_computers.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: computer_admin_other_computers 3 | state: enabled 4 | tactic: lateral movement 5 | tag: issue 6 | description: Get computers that are admin of other computers. 7 | statement: 8 | main: >- 9 | match (c1:Computer{domain:'RAS-DOMAIN'})-[:MemberOf|AdminTo*1..]->(c2:Computer{domain:'RAS-DOMAIN'}) 10 | return distinct c1.name, c2.name 11 | order by c1.name, c2.name asc 12 | nextsteps: 13 | rt: 14 | - Compromise them. 15 | - Perform lateral movement. 16 | bt: 17 | - Check security descriptors. 18 | -------------------------------------------------------------------------------- /queries/tactics/discovery/emails.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: emails 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get emails. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | where not u.email is null 11 | return u.name, u.email 12 | order by u.name 13 | nextsteps: 14 | rt: 15 | - Perform spear phishing attacks. 16 | - Search passwords in data breach dumps. 17 | - Perform password stuffing attacks. 18 | bt: 19 | - Check emails in data breach dumps. 20 | - Subscribe to a threat intelligence platform. 21 | -------------------------------------------------------------------------------- /queries/tactics/discovery/gpos_and_objects.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: gpos_and_objects 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get GPOs and affected object types. 7 | statement: 8 | main: >- 9 | match p=(g:GPO{domain:'RAS-DOMAIN'})-[r:GpLink|Contains*1..]->(n{enabled:true, domain:'RAS-DOMAIN'}) 10 | where n:User or n:Computer 11 | unwind labels(n) as type 12 | return g.name, type, n.name 13 | order by g.name, type, n.name asc 14 | count: 15 | - return .* order/return g.name, type, count(type) as ct order 16 | - order .*/order by ct desc, type, g.name 17 | -------------------------------------------------------------------------------- /queries/tactics/discovery/disabled_highvalue_users.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: disabled_highvalue_users 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get disabled users belonging to high value groups. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:false, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{domain:'RAS-DOMAIN', highvalue:true}) 10 | return distinct u.name 11 | order by u.name asc 12 | nextsteps: 13 | rt: 14 | - Compromise them. 15 | - Use them to hide your actions. 16 | bt: 17 | - Remove them after a period of time. 18 | - Use a strong user policy. 19 | -------------------------------------------------------------------------------- /queries/tactics/discovery/objects_in_tiers.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: objects_in_tiers 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get objects in TIERs 7 | statement: 8 | main: >- 9 | match (n{domain:'RAS-DOMAIN'}) 10 | where n.name =~ '(?i).*tier[ -_]?[0-9].*' or n.description =~ '(?i).*tier[ -_]?[0-9].*' 11 | return n.name as name, n.description as description 12 | order by name 13 | reference: 14 | - https://docs.microsoft.com/en-us/security/compass/privileged-access-access-model 15 | nextsteps: 16 | rt: 17 | - Note that communication between TIERs may be restricted. 18 | -------------------------------------------------------------------------------- /queries/tactics/lateral_movement/users_groups_can_psremote.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: users_groups_can_psremote 3 | state: enabled 4 | tactic: lateral movement 5 | tag: analysis 6 | description: Get users and groups that can PSRemote. 7 | statement: 8 | main: >- 9 | match (n{domain:'RAS-DOMAIN'})-[:PSRemote]->(c:Computer{domain:'RAS-DOMAIN'}) 10 | where (n:User) or (n:Group) 11 | unwind labels(n) as type 12 | return type, n.name, c.name 13 | order by type, c.name, n.name 14 | nextsteps: 15 | rt: 16 | - Compromise them. 17 | - Perform lateral movement. 18 | bt: 19 | - Check security descriptors. 20 | -------------------------------------------------------------------------------- /queries/tactics/discovery/disabled_users.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: disabled_users 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get disabled users. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:false, domain:'RAS-DOMAIN'}) 10 | where not u.name starts with 'KRBTGT' 11 | return u.name 12 | order by u.name asc 13 | count: 14 | - return .* order/return count(u.name) as count order 15 | - order .*/ 16 | nextsteps: 17 | rt: 18 | - Compromise them. 19 | - Use them to hide your actions. 20 | bt: 21 | - Remove them after a period of time. 22 | - Use a strong user policy. 23 | -------------------------------------------------------------------------------- /modules/error.py: -------------------------------------------------------------------------------- 1 | class Error: 2 | 3 | 4 | def __init__(self): 5 | self.data = [] 6 | 7 | 8 | def __bool__(self): 9 | return bool(self.data) 10 | 11 | 12 | def __iter__(self): 13 | el = self.data 14 | self.data = [] 15 | return iter(el) 16 | 17 | 18 | def add(self, error): 19 | if isinstance(error, str): 20 | self.data.append(error) 21 | elif isinstance(error, list): 22 | self.data += error 23 | 24 | 25 | def clean(self): 26 | self.data = [] 27 | 28 | 29 | def get(self): 30 | el = self.data 31 | self.clean() 32 | return el 33 | -------------------------------------------------------------------------------- /queries/tactics/persistence/sid_history.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: sid_history 3 | state: enabled 4 | tactic: persistence 5 | tag: issue 6 | description: Get unconstrained domain persistence. 7 | statement: 8 | main: >- 9 | match (n1{domain:'RAS-DOMAIN'})-[:HasSIDHistory]->(n2) 10 | return distinct n1.name 11 | order by n1.name 12 | reference: 13 | - https://adsecurity.org/?p=1772 14 | nextsteps: 15 | rt: 16 | - Compromise them and elevate privileges. 17 | - Jump between Active Directory domains. 18 | - Check if SID History is enabled. 19 | bt: 20 | - Check if the object was migrated. 21 | - Activate incident response procedures. 22 | -------------------------------------------------------------------------------- /queries/tactics/discovery/users_and_sessions.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: users_and_sessions 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get users and sessions. 7 | statement: 8 | main: >- 9 | match (c:Computer{enabled:true, domain:'RAS-DOMAIN'})-[:HasSession]->(u:User{domain:'RAS-DOMAIN'}) 10 | return u.name, c.name 11 | order by u.name, c.name asc 12 | count: 13 | - return .* order/return u.name, count(u.name) order 14 | - order .*/order by count(u.name) desc 15 | nextsteps: 16 | rt: 17 | - Compromise them. 18 | - Perform lateral movement to those computers. 19 | bt: 20 | - Investigate the reason for so many sessions. 21 | -------------------------------------------------------------------------------- /queries/tactics/discovery/disabled_users_never_logged_in.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: disabled_users_never_logged_in 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get disabled users who never logged in. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:false, domain:'RAS-DOMAIN'}) 10 | where u.lastlogontimestamp < 1 and not u.name starts with 'KRBTGT' 11 | return u.name 12 | order by u.name asc 13 | count: 14 | - return .* order/return count(u) order 15 | - order .*/ 16 | nextsteps: 17 | rt: 18 | - Compromise them to hide your actions. 19 | bt: 20 | - Remove them after a period of time. 21 | - Use a strong user policy. 22 | -------------------------------------------------------------------------------- /queries/tactics/discovery/foreign_security_principals.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: foreign_security_principals 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get external security principals (users, groups and computers) added to local domain groups 7 | statement: 8 | main: >- 9 | match (n)-[:MemberOf]->(g:Group{domain:'RAS-DOMAIN'}) 10 | where not toLower(n.domain) = toLower(g.domain) 11 | return g.name as Group, labels(n)[-1] as Type, n.name as FSP 12 | order by Group, Type, FSP limit 1000 13 | reference: 14 | - https://adsecurity.org/?p=3658 15 | nextsteps: 16 | rt: 17 | - Compromise them. 18 | bt: 19 | - Remove them from unnecessary groups. 20 | -------------------------------------------------------------------------------- /queries/tactics/discovery/shared_resources_in_descriptions.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: shared_resources_in_descriptions 3 | state: enabled 4 | tactic: discovery 5 | tag: attack 6 | description: Get shared resources in descriptions. 7 | statement: 8 | main: >- 9 | match (n{domain:'RAS-DOMAIN'}) 10 | where n.description =~ '.*(\\\\\\\\).*' 11 | return n.name, n.description 12 | order by n.name, n.description 13 | nextsteps: 14 | rt: 15 | - Check security descriptors. 16 | - Mount the shared resources. 17 | - Find credentials or sensitive information. 18 | - Upload special files to steal NetNTLM hashes. 19 | bt: 20 | - Check security descriptors. 21 | - Remove them if not necessary. 22 | -------------------------------------------------------------------------------- /queries/tactics/credential_access/read_gmsa_passwords.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: read_gmsa_passwords 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: Get users that can read GMSA passwords. 7 | statement: 8 | main: >- 9 | match (n{domain:'RAS-DOMAIN'})-[:ReadGMSAPassword]->(u:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | return n.name, u.name 11 | order by n.name, u.name 12 | reference: 13 | - https://cube0x0.github.io/Relaying-for-gMSA/ 14 | - https://posts.specterops.io/introducing-bloodhound-3-0-c00e77ff0aa6 15 | nextsteps: 16 | rt: 17 | - Compromise them. 18 | - Read the password. 19 | - Perform lateral movement. 20 | bt: 21 | - Check security descriptors. 22 | -------------------------------------------------------------------------------- /queries/tactics/discovery/computers_and_sessions.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: computers_and_sessions 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get computers and sessions. 7 | statement: 8 | main: >- 9 | match (c:Computer{enabled:true, domain:'RAS-DOMAIN'})-[:HasSession]->(u:User{domain:'RAS-DOMAIN'}) 10 | return c.name, u.name 11 | order by c.name, u.name asc 12 | count: 13 | - return .* order/return c.name, count(c.name) order 14 | - order .*/order by count(c.name) desc 15 | nextsteps: 16 | rt: 17 | - Compromise them. 18 | - Steal user passwords and hashes. 19 | bt: 20 | - Investigate the reason for so many sessions. 21 | - Harden those with most sessions. 22 | -------------------------------------------------------------------------------- /queries/tactics/discovery/domain-admins_enterprise-admins_adminitrators_users.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: domain-admins_enterprise-admins_adminitrators_users 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get all domain admins, enterprise admins and administrators. 7 | statement: 8 | main: >- 9 | match p=(u:User{enabled:true, domain:'RAS-DOMAIN'})-[r:MemberOf*1..]->(g:Group{domain:'RAS-DOMAIN'}) 10 | where g.name in ['RAS-DA@RAS-DOMAIN', 'RAS-EA@RAS-DOMAIN', 'RAS-A@RAS-DOMAIN'] 11 | return distinct u.name, g.name 12 | order by g.name, u.name 13 | graph: 14 | - return .*/return p 15 | nextsteps: 16 | rt: 17 | - Compromise them. 18 | bt: 19 | - Remove them from unnecessary groups. 20 | -------------------------------------------------------------------------------- /queries/tactics/lateral_movement/users_groups_can_rdp.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: users_groups_can_rdp 3 | state: enabled 4 | tactic: lateral movement 5 | tag: analysis 6 | description: Get users and groups that can RDP. 7 | statement: 8 | main: >- 9 | match (n{domain:'RAS-DOMAIN'})-[:CanRDP]->(c:Computer{domain:'RAS-DOMAIN'}) 10 | where (n:User) or (n:Group) 11 | unwind labels(n) as type 12 | return type, n.name, c.name 13 | order by type, n.name, c.name 14 | count: 15 | - return .* order/return type, n.name, count(c) as cc order 16 | - order .*/order by cc desc, type, n.name 17 | nextsteps: 18 | rt: 19 | - Compromise them. 20 | - Perform lateral movement. 21 | bt: 22 | - Check security descriptors. 23 | -------------------------------------------------------------------------------- /queries/tactics/discovery/ous_and_computers.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: ous_and_computers 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get OUs and affected computers. 7 | statement: 8 | main: >- 9 | match (o:OU{domain:'RAS-DOMAIN'})-[:Contains]->(c:Computer{enabled:true, domain:'RAS-DOMAIN'}) 10 | return o.name, coalesce(o.objectsid, o.objectid) as SID, c.name 11 | order by o.name, c.name asc 12 | count: 13 | - return .* order/return o.name, coalesce(o.objectsid, o.objectid) as SID, count(c) order 14 | - order .*/order by count(c) desc 15 | nextsteps: 16 | rt: 17 | - Compromise GPOs that affect the OUs with most sessions. 18 | bt: 19 | - Protect GPOs that affect the OUs with most sessions. 20 | -------------------------------------------------------------------------------- /queries/tactics/discovery/same_user_displayname_on_other_domains.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: same_user_displayname_on_other_domains 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get usernames with same displayname on other domains. 7 | statement: 8 | main: >- 9 | match (u1:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | with u1 11 | match (u2:User{enabled:true}) 12 | where not u2.domain = 'RAS-DOMAIN' and u2.displayname = u1.displayname 13 | with u1.name as name1, u2.name as name2 14 | return name1, name2 15 | order by name1, name2 16 | nextsteps: 17 | rt: 18 | - Compromise them. 19 | - Check if those users share the same password or hash. 20 | bt: 21 | - Check if passwords have the same hash. 22 | -------------------------------------------------------------------------------- /queries/tactics/execution/users_with_write_permissions_to_ous.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: users_with_write_permissions_to_ous 3 | state: enabled 4 | tactic: execution 5 | tag: attack 6 | description: Get users with write permissions to Organization Units (OUs). 7 | statement: 8 | main: >- 9 | match (u:User{domain:'RAS-DOMAIN'})-[r:Owns|GenericAll|GenericWrite|WriteProperty|WriteDACL|WriteOwner]->(o:OU{domain:'RAS-DOMAIN'}) 10 | return u.name, type(r) as type, o.name 11 | order by u.name, type, o.name asc 12 | reference: 13 | - https://www.youtube.com/watch?v=un2EbYjp3Zg 14 | nextsteps: 15 | rt: 16 | - Compromise them. 17 | - Modify GPO in those OUs. 18 | - Execute code on those computers. 19 | bt: 20 | - Check security descriptors. 21 | -------------------------------------------------------------------------------- /queries/tactics/discovery/non-highvalue_users_admincount_enabled.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-highvalue_users_admincount_enabled 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get all enabled non-highvalue users with admincount. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{highvalue:true, domain:'RAS-DOMAIN'}) 10 | with collect(distinct u.name) as cdu 11 | match (u:User{enabled:true, domain:'RAS-DOMAIN', admincount:true}) 12 | where not u.name in cdu 13 | return u.name 14 | order by u.name 15 | nextsteps: 16 | rt: 17 | - Check the permissions these users have (path mode). 18 | - Compromise them. 19 | bt: 20 | - Remove unnecessary permissions. 21 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/get_dns_admins.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: get_dns_admins 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Get DNS Admins that can load a DLL as system on DC. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[r:MemberOf*1..]->(g:Group{name:'DNSADMINS@RAS-DOMAIN'}) 10 | return distinct u.name 11 | order by u.name 12 | reference: 13 | - https://ired.team/offensive-security-experiments/active-directory-kerberos-abuse/from-dnsadmins-to-system-to-domain-compromise 14 | - https://cube0x0.github.io/Pocing-Beyond-DA/ 15 | nextsteps: 16 | rt: 17 | - Compromise them. 18 | - Become a domain administrator. 19 | bt: 20 | - Check security descriptors. 21 | -------------------------------------------------------------------------------- /queries/tactics/discovery/computers_without_laps.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: computers_without_laps 3 | state: enabled 4 | tactic: discovery 5 | tag: issue 6 | description: Get windows computer names and operating systems without LAPS. 7 | statement: 8 | main: >- 9 | match (c:Computer{enabled:true, domain:'RAS-DOMAIN', haslaps:false}) 10 | where c.operatingsystem =~ '(?i).*windows.*' 11 | return c.name, c.operatingsystem 12 | order by c.name, c.operatingsystem asc 13 | count: 14 | - return .* order/return count(c.name) as count order 15 | - order .*/order by count 16 | nextsteps: 17 | rt: 18 | - Compromise them. 19 | - Dump passwords and hashes from memory. 20 | - Perform lateral movement. 21 | bt: 22 | - Enable LAPS in all your computers. 23 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/get_schema_admins.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: get_schema_admins 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Get Schema Admins to modify the AD schema structure 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[r:MemberOf*1..]->(g:Group{name:'SCHEMA ADMINS@RAS-DOMAIN'}) 10 | return distinct u.name 11 | order by u.name 12 | reference: 13 | - https://cube0x0.github.io/Pocing-Beyond-DA/ 14 | nextsteps: 15 | rt: 16 | - Compromise them. 17 | - Modify the security descriptor of the Group or GPO schema. 18 | - Wait for the creation of a new group or GPO and then add a new member to the group or a startup script. 19 | bt: 20 | - Check security descriptors. 21 | -------------------------------------------------------------------------------- /queries/tactics/discovery/same_usernames_in_different_domains.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: same_usernames_in_different_domains 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get same usernames in different domains. 7 | statement: 8 | main: >- 9 | match (u2:User{enabled:true}) where not u2.domain = 'RAS-DOMAIN' 10 | with u2 11 | match (u1:User{enabled:true, domain:'RAS-DOMAIN'}) 12 | where split(u1.name,'@')[0] = split(u2.name,'@')[0] and tointeger(split(coalesce(u1.objectid, u1.objectsid), '-')[7]) >= 1000 13 | return distinct u1.name, u2.name order by u1.name, u2.name 14 | nextsteps: 15 | rt: 16 | - Compromise them. 17 | - Check if those users share the same password or hash. 18 | bt: 19 | - Check if passwords have the same hash. 20 | -------------------------------------------------------------------------------- /queries/tactics/discovery/ad_connect_user.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: ad_connect_user 3 | state: enabled 4 | tactic: discovery 5 | tag: attack 6 | description: Get AD Connect user. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | where u.name starts with 'MSOL_' or toLower(u.name) =~ '.*ad.?connect.*' or u.description contains 'Azure' 11 | with u.name as Account, replace(split(split(u.description, 'computer ')[1], ' configured')[0], '\'', '') as Computer, replace(split(split(u.description, 'tenant ')[1], '. ')[0], '\'', '') as Tenant 12 | return Account, Computer, Tenant 13 | nextsteps: 14 | rt: 15 | - Compromise the computer where this account is running. 16 | - Dump passwords and hashes from memory. 17 | - Perform DCSync attack. 18 | -------------------------------------------------------------------------------- /queries/tactics/discovery/different_usernames_with_same_email.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: different_usernames_with_same_email 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get different usernames with same email. 7 | statement: 8 | main: >- 9 | match (u1:User{enabled:true, domain:'RAS-DOMAIN'}), (u2:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | where not u1.email is null and not u1.name = u2.name and toLower(u1.email) = toLower(u2.email) 11 | return u1.name, u1.email 12 | order by u1.email, u1.name 13 | reference: 14 | - https://insomniasec.com/blog/bloodhound-shared-accounts 15 | nextsteps: 16 | rt: 17 | - Compromise them. 18 | - Check if those users share the same password or hash. 19 | bt: 20 | - Check if passwords have the same hash. 21 | -------------------------------------------------------------------------------- /queries/tactics/discovery/krbtgt_pwd_not_changed.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: krbtgt_pwd_not_changed 3 | state: enabled 4 | tactic: discovery 5 | tag: issue 6 | description: Get KRBTGT account password not changed regularly (in the last year). 7 | statement: 8 | main: >- 9 | match (u:User{domain:'RAS-DOMAIN'}) 10 | where u.name =~ '(?i).*krbtgt.*' and (u.pwdlastset = -1 or toInteger(u.pwdlastset) < (datetime().epochseconds - (365 * 86400))) 11 | return u.name, split(toString(datetime({epochSeconds:toInteger(case when u.pwdlastset is null then 0 else u.pwdlastset end)})), '.')[0] as pwdlastset 12 | order by pwdlastset desc 13 | nextsteps: 14 | rt: 15 | - Get global persistence with golden tickets. 16 | bt: 17 | - Change krbtgt password at least once a month (twice per change). 18 | -------------------------------------------------------------------------------- /queries/tactics/discovery/users_in_privileged_groups.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: users_in_privileged_groups 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get users in privileged groups. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{domain:'RAS-DOMAIN'}) 10 | where g.objectid =~ '.*-(512|517|518|519|520|525|544|548|549|550|551|555|562|571|573|574|578|1000|1102)$' 11 | or g.objectsid =~ '.*-(512|517|518|519|520|525|544|548|549|550|551|555|562|571|573|574|578|1000|1102)$' 12 | return distinct u.name, g.name 13 | order by u.name, g.name 14 | reference: 15 | - https://adsecurity.org/?p=3658 16 | nextsteps: 17 | rt: 18 | - Compromise them. 19 | bt: 20 | - Remove them from unnecessary groups. 21 | -------------------------------------------------------------------------------- /queries/tactics/discovery/low_level_group_permissions.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: low_level_group_permissions 3 | state: enabled 4 | tactic: discovery 5 | tag: attack 6 | description: Get permissions for low level groups. 7 | statement: 8 | main: >- 9 | match (g:Group{domain:'RAS-DOMAIN'})-[r]->(n{enabled:true, domain:'RAS-DOMAIN'}) 10 | where g.objectid =~ '.*-(513|514|515)$' or g.objectsid =~ '.*-(513|514|515)$' 11 | return g.name as Group, type(r) as Permission, n.name as Target 12 | order by Group, Permission, Target 13 | count: 14 | - return .* order/return distinct g.name as Group, count(distinct n.name) as Count order 15 | - order .*/order by Count desc, Group asc 16 | nextsteps: 17 | rt: 18 | - Take advantage of wrong relationships. 19 | bt: 20 | - Correct wrong relationships. 21 | -------------------------------------------------------------------------------- /queries/tactics/persistence/constrained_domain_persistence.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: constrained_domain_persistence 3 | state: enabled 4 | tactic: persistence 5 | tag: issue 6 | description: Get constrained domain persistence. 7 | statement: 8 | main: >- 9 | match (c:Computer{domain:'RAS-DOMAIN'})-[:MemberOf]->(:Group{name:'RAS-DC@RAS-DOMAIN'}) 10 | match (n{enabled:true, domain:'RAS-DOMAIN'}) 11 | where n.allowedtodelegate 12 | unwind labels(n) as type 13 | unwind n.serviceprincipalnames as spn 14 | unwind n.allowedtodelegate as atd 15 | with n, type, spn, atd 16 | where split(toUpper(atd),'/')[1] in c.name 17 | return n.name, type, spn, atd 18 | order by type, n.name, spn, atd 19 | nextsteps: 20 | rt: 21 | - Notify the White Team. 22 | bt: 23 | - Activate incident response procedures. 24 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/non-highvalue_gpos_relationship.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-highvalue_gpos_relationship 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Get non-highvalue objects with direct relationship to GPOs. 7 | statement: 8 | main: >- 9 | match (n{domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{highvalue:true, domain:'RAS-DOMAIN'}) 10 | with collect(distinct n.name) as cdn 11 | match (n{domain:'RAS-DOMAIN'})-[r]->(g:GPO{domain:'RAS-DOMAIN'}) 12 | where not n.name in cdn 13 | unwind labels(n) as type 14 | return distinct type, n.name, type(r), g.name 15 | order by type, n.name, type(r), g.name 16 | nextsteps: 17 | rt: 18 | - Compromise them. 19 | - Perform privilege escalation. 20 | bt: 21 | - Check security descriptors. 22 | -------------------------------------------------------------------------------- /queries/tactics/execution/users_with_genericwrite_permission_to_other_users.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: users_with_genericwrite_permission_to_other_users 3 | state: enabled 4 | tactic: execution 5 | tag: attack 6 | description: Get users with GenericWrite permission to other users to abuse the Remote Connection Manager (RCM). 7 | statement: 8 | main: >- 9 | match (u1:User{domain:'RAS-DOMAIN'})-[:GenericWrite]->(u2:User{domain:'RAS-DOMAIN'}) 10 | where not u1.name starts with 'MSOL_' 11 | return u1.name, u2.name 12 | order by u1.name, u2.name asc 13 | reference: 14 | - https://sensepost.com/blog/2020/ace-to-rce/ 15 | nextsteps: 16 | rt: 17 | - Check if Windows Server versions are lower than 2016. 18 | - Compromise them. 19 | - Execute code as those users. 20 | bt: 21 | - Check security descriptors. 22 | -------------------------------------------------------------------------------- /queries/tactics/lateral_movement/outdated_computers.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: outdated_computers 3 | state: enabled 4 | tactic: lateral movement 5 | tag: issue 6 | description: Get outdated computers names and operating system. 7 | statement: 8 | main: >- 9 | match (c:Computer{enabled:true, domain:'RAS-DOMAIN'}) 10 | where c.operatingsystem =~ '(?i)windows.* (7|8|8.1|2000|2003|2008|2012|me|vista|xp) .*' 11 | return c.name as Name, c.operatingsystem as OperatingSystem 12 | order by OperatingSystem, Name asc 13 | count: 14 | - return .* order/return c.operatingsystem as OperatingSystem, count(*) as Count order 15 | - order .*/order by Count desc, OperatingSystem 16 | nextsteps: 17 | rt: 18 | - Check them for known vulnerabilities. 19 | - Compromise them. 20 | bt: 21 | - Update, isolate or shutdown them. 22 | -------------------------------------------------------------------------------- /queries/tactics/lateral_movement/users_with_admin_rights_to_computers.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: users_with_admin_rights_to_computers 3 | state: enabled 4 | tactic: lateral movement 5 | tag: attack 6 | description: Get users with admin rights to computers. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf|AdminTo*1..]->(c:Computer{enabled:true, domain:'RAS-DOMAIN'}) 10 | with distinct u.name as UserName, c.name as ComputerName 11 | return UserName, ComputerName 12 | order by UserName, ComputerName asc 13 | count: 14 | - return .* order/return UserName, count(ComputerName) order 15 | - order .*/order by count(ComputerName) desc, UserName 16 | nextsteps: 17 | rt: 18 | - Compromise them. 19 | - Perform lateral movement. 20 | bt: 21 | - Check security descriptors. 22 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/non-highvalue_groups_reach_admins.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-highvalue_groups_reach_admins 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Get non-highvalue groups that reach DOMAIN ADMINS/ENTERPRISE ADMINS/ADMINISTRATORS group. 7 | statement: 8 | main: >- 9 | match (g1:Group{highvalue:false, domain:'RAS-DOMAIN'}), (g2:Group) where g2.name in ['RAS-DA@RAS-DOMAIN', 'RAS-EA@RAS-DOMAIN', 'RAS-A@RAS-DOMAIN'] 10 | with g1, g2 11 | match p=allShortestPaths((g1)-[*1..6]->(g2)) 12 | where all(x in nodes(p) where x:Group) 13 | return distinct g1.name, g2.name 14 | order by g1.name, g2.name asc 15 | graph: 16 | - return .*/return p 17 | nextsteps: 18 | rt: 19 | - Compromise them. 20 | - Become a domain administrator. 21 | bt: 22 | - Check security descriptors. 23 | -------------------------------------------------------------------------------- /queries/tactics/discovery/different_usernames_with_same_displayname.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: different_usernames_with_same_displayname 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get different usernames with same displayname. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | with u.displayname as dn, count(u.displayname) as cdn 11 | where cdn > 1 12 | with dn 13 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 14 | where tolower(u.displayname) = tolower(dn) 15 | return u.name, u.displayname 16 | order by u.displayname 17 | reference: 18 | - https://insomniasec.com/blog/bloodhound-shared-accounts 19 | nextsteps: 20 | rt: 21 | - Compromise them. 22 | - Check if those users share the same password or hash. 23 | bt: 24 | - Check if passwords have the same hash. 25 | -------------------------------------------------------------------------------- /queries/tactics/lateral_movement/non-da_rdp_dc.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-da_rdp_dc 3 | state: enabled 4 | tactic: lateral movement 5 | tag: issue 6 | description: Get list of Non-Domain Administrators who can RDP over DCs. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{domain:'RAS-DOMAIN'}) 10 | where g.name in ['RAS-DA@RAS-DOMAIN', 'RAS-EA@RAS-DOMAIN', 'RAS-A@RAS-DOMAIN'] 11 | with collect(distinct u.name) as cdu 12 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf|CanRDP*1..]->(:Computer{domain:'RAS-DOMAIN'})-[:MemberOf]->(:Group{name:'RAS-DC@RAS-DOMAIN'}) 13 | where not u.name in cdu 14 | return distinct u.name 15 | order by u.name asc 16 | nextsteps: 17 | rt: 18 | - Compromise them. 19 | - Perform lateral movement. 20 | bt: 21 | - Check security descriptors. 22 | -------------------------------------------------------------------------------- /queries/tactics/discovery/enabled_not_logged_in_last_year.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: enabled_not_logged_in_last_year 3 | state: enabled 4 | tactic: discovery 5 | tag: issue 6 | description: Get users who have logged at least one time but not in the last year. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | where u.lastlogontimestamp > 0 and toInteger(u.lastlogontimestamp) < (datetime().epochseconds - (365 * 86400)) 11 | return u.name as User, split(toString(datetime({epochSeconds:toInteger(u.lastlogontimestamp)})), '.')[0] as DateTime, coalesce(left(u.description, 50), '') as Description 12 | order by u.lastlogontimestamp desc 13 | nextsteps: 14 | rt: 15 | - Compromise them. 16 | - Use them to hide your actions. 17 | - Perform lateral phishing. 18 | bt: 19 | - Disable them. 20 | - Use a strong user policy. 21 | -------------------------------------------------------------------------------- /queries/tactics/persistence/unconstrained_domain_persistence.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: unconstrained_domain_persistence 3 | state: enabled 4 | tactic: persistence 5 | tag: issue 6 | description: Get unconstrained domain persistence. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:AllowedToAct]->(n{domain:'RAS-DOMAIN'}) 10 | where u.name =~ '(?i).*krbtgt.*' 11 | return distinct u.name, n.name 12 | order by u.name, n.name 13 | reference: 14 | - https://shenaniganslabs.io/media/Constructing%20Kerberos%20Attacks%20with%20Delegation%20Primitives.pdf 15 | - https://shenaniganslabs.io/2019/01/28/Wagging-the-Dog.html#unconstrained-domain-persistence 16 | - https://gist.github.com/mgeeky/54ced6521bcd6524d5d0e849555ee67c 17 | nextsteps: 18 | rt: 19 | - Notify the White Team. 20 | bt: 21 | - Activate incident response procedures. 22 | -------------------------------------------------------------------------------- /queries/tactics/discovery/adcs/find_certificate_templates.yaml: -------------------------------------------------------------------------------- 1 | author: e1abrador (Eric Labrador) 2 | name: find_certificate_templates 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Certificate templates. 7 | statement: 8 | main: >- 9 | match (ct:CertificateTemplate{domain:'RAS-DOMAIN'}) 10 | return distinct ct.`Template Name` as `Template Name`, ct.Enabled as Enabled, ct.`Client Authentication` as `Client Authentication`, ct.`Requires Manager Approval` as `Requires Manager Approval`, ct.highvalue as HighValue, ct.`Validity Period` as `Validity Period` 11 | order by Enabled desc, `Client Authentication` desc, `Requires Manager Approval` asc, HighValue desc, `Validity Period` asc, `Template Name` asc 12 | nextsteps: 13 | rt: 14 | - Look for ADCS vulnerabilities primarily in those templates for client authentication that do not require manager approval. 15 | -------------------------------------------------------------------------------- /queries/tactics/lateral_movement/non-da_psremote_dc.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-da_psremote_dc 3 | state: enabled 4 | tactic: lateral movement 5 | tag: issue 6 | description: Get list of Non-Domain Administrators who can PSRemote over DCs. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{domain:'RAS-DOMAIN'}) 10 | where g.name in ['RAS-DA@RAS-DOMAIN', 'RAS-EA@RAS-DOMAIN', 'RAS-A@RAS-DOMAIN'] 11 | with collect(distinct u.name) as cdu 12 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf|PSRemote*1..]->(:Computer{domain:'RAS-DOMAIN'})-[:MemberOf]->(:Group{name:'RAS-DC@RAS-DOMAIN'}) 13 | where not u.name in cdu 14 | return distinct u.name 15 | order by u.name asc 16 | nextsteps: 17 | rt: 18 | - Compromise them. 19 | - Perform lateral movement. 20 | bt: 21 | - Check security descriptors. 22 | -------------------------------------------------------------------------------- /conf/defaults.py: -------------------------------------------------------------------------------- 1 | parser = { 2 | 'ad': { 3 | 'lang': 'en' 4 | }, 5 | 'audit': { 6 | 'persistence': { 7 | 'format': 'csv' 8 | } 9 | }, 10 | 'check': { 11 | 'persistence': { 12 | 'format': 'yaml' 13 | } 14 | }, 15 | 'neo4j': { 16 | 'encrypted': 'true', 17 | 'host': 'localhost', 18 | 'password': 'neo4j', 19 | 'port': '7687', 20 | 'username': 'neo4j' 21 | }, 22 | 'output': { 23 | 'directory': 'output', 24 | 'format': 'table' 25 | }, 26 | 'path': { 27 | 'end_node': '', 28 | 'has_session': 'false', 29 | 'persistence': { 30 | 'format': 'csv' 31 | }, 32 | 'start_node': '' 33 | }, 34 | 'query': { 35 | 'mode': 'default' 36 | }, 37 | 'verbose': { 38 | 'mode': 'default' 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /queries/tactics/lateral_movement/non-da_with_admin_rights_dc.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-da_with_admin_rights_dc 3 | state: enabled 4 | tactic: lateral movement 5 | tag: issue 6 | description: Get Non-Domain Administrators with admin rights to domain controllers. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{domain:'RAS-DOMAIN'}) 10 | where g.name in ['RAS-DA@RAS-DOMAIN', 'RAS-EA@RAS-DOMAIN', 'RAS-A@RAS-DOMAIN'] 11 | with collect(distinct u.name) as cdu 12 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf|AdminTo*1..]->(c:Computer{domain:'RAS-DOMAIN'})-[:MemberOf]->(dc:Group{name:'RAS-DC@RAS-DOMAIN'}) 13 | where not u.name in cdu 14 | return distinct u.name 15 | order by u.name asc 16 | nextsteps: 17 | rt: 18 | - Compromise them. 19 | - Perform lateral movement. 20 | bt: 21 | - Check security descriptors. 22 | -------------------------------------------------------------------------------- /queries/tactics/credential_access/kerberoasting.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: kerberoasting 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: Get users with SPNs (kerberoasting). 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN', hasspn:true}) 10 | unwind u.serviceprincipalnames as spn 11 | return distinct u.name as Name, spn 12 | order by Name asc, spn 13 | count: 14 | - return .* order/return distinct u.name as Name, count(distinct spn) as Count order 15 | - order .*/order by Count desc, Name asc 16 | nextsteps: 17 | rt: 18 | - Request TGS for all user SPNs and crack them. 19 | bt: 20 | - Ensure all service accounts have long and complex passwords and rotate them periodically. 21 | - Eliminate use of insecure protocols in Kerberos removing RC4 encryption. 22 | - Use sMSA or gMSA for automatic password management. 23 | - Create service account honeypots. 24 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/samaccountname_spoofing.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: samaccountname spoofing 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: issue 6 | description: Get computers with the same name because it's a potential IOC. 7 | statement: 8 | main: >- 9 | match (c1:Computer), (c2:Computer) 10 | where (not c1.objectid = c2.objectid or not c1.objectsid = c2.objectsid) and c1.name = c2.name 11 | return distinct c1.name as ComputerName, coalesce(c1.objectid, c1.objectsid) as SID 12 | order by ComputerName, SID 13 | reference: 14 | - https://cloudbrothers.info/en/exploit-kerberos-samaccountname-spoofing/ 15 | - https://exploit.ph/cve-2021-42287-cve-2021-42278-weaponisation.html 16 | - https://github.com/cube0x0/noPac 17 | - https://www.thehacker.recipes/ad/movement/kerberos/samaccountname-spoofing 18 | nextsteps: 19 | rt: 20 | - Notify the White Team. 21 | bt: 22 | - Activate incident response procedures. 23 | -------------------------------------------------------------------------------- /queries/tactics/discovery/domain_admins_and_sessions.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: domain_admins_and_sessions 3 | state: enabled 4 | tactic: discovery 5 | tag: issue 6 | description: Get Domain Administrators and their active sessions. 7 | statement: 8 | main: >- 9 | match (c:Computer{enabled:true, domain:'RAS-DOMAIN'})-[:HasSession]->(u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(:Group{name:'RAS-DA@RAS-DOMAIN'}) 10 | return distinct u.name as User, c.name as Computer, not ((c)-[:MemberOf]->(:Group{highvalue:true})) as BreaksTier0 11 | order by User, Computer asc 12 | count: 13 | - return .* order/return u.name as User, count(u.name) as Sessions order 14 | - order .*/order by Sessions desc, User asc 15 | nextsteps: 16 | rt: 17 | - Compromise them. 18 | - Steal user passwords and hashes. 19 | - Become a domain administrator. 20 | bt: 21 | - Ensure Domain Administrators only logon to Tier 0 computers or secured systems. 22 | -------------------------------------------------------------------------------- /queries/tactics/credential_access/as-rep_roasting.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: as-rep_roasting 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: Get users with dontreqpreauth (AS-REP roasting). 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN', dontreqpreauth:true}) 10 | return u.name 11 | order by u.name asc 12 | reference: 13 | - https://www.harmj0y.net/blog/activedirectory/roasting-as-reps/ 14 | - https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html 15 | - https://github.com/Bdenneu/CVE-2022-33679 16 | nextsteps: 17 | rt: 18 | - Request TGTs and crack them. 19 | - Request TGTs specifying RC4-MD4 encryption and brute force the TGT's session key (CVE-2022-33679). 20 | bt: 21 | - Ensure all service accounts have a long, complex passwords. 22 | - Use a strong password policy. 23 | - Remove RC4 encryption via group policy. 24 | - Create a service account honeypot. 25 | -------------------------------------------------------------------------------- /queries/tactics/discovery/potential_admins_users_not_in_highvalue_groups.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: potential_admins_users_not_in_highvalue_groups 3 | state: enabled 4 | tactic: discovery 5 | tag: analysis 6 | description: Get potential admin users not in highvalue groups. 7 | statement: 8 | main: >- 9 | match (u:User{domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{domain:'RAS-DOMAIN', highvalue:true}) 10 | with collect(distinct u.name) as cdu 11 | match (u:User{domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{domain:'RAS-DOMAIN', highvalue:false}) 12 | where not u.name in cdu and ( 13 | u.name =~ '(?i)^(ADMIN|ADM)[_-]?.*' or u.name =~ '(?i).*[_-]?(ADMIN|ADM)@.*' or 14 | g.name =~ '(?i)^(ADMIN|ADM)[_-]?.*' or g.name =~ '(?i).*[_-]?(ADMIN|ADM)@.*' 15 | ) 16 | return distinct u.name as User 17 | order by User 18 | nextsteps: 19 | rt: 20 | - Search for interesting groups they belong to. 21 | - Compromise them. 22 | bt: 23 | - Remove them from unnecessary groups. 24 | -------------------------------------------------------------------------------- /queries/tactics/lateral_movement/non-da_with_admin_rights_computers.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-da_with_admin_rights_computers 3 | state: enabled 4 | tactic: lateral movement 5 | tag: attack 6 | description: Get Non-Domain Administrators with admin rights to computers. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{domain:'RAS-DOMAIN'}) 10 | where g.name in ['RAS-DA@RAS-DOMAIN', 'RAS-EA@RAS-DOMAIN', 'RAS-A@RAS-DOMAIN'] 11 | with collect(distinct u.name) as cdu 12 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf|AdminTo*1..]->(c:Computer{domain:'RAS-DOMAIN'}) 13 | where not u.name in cdu 14 | return distinct u.name, c.name 15 | order by u.name, c.name asc 16 | count: 17 | - return .* order/return distinct u.name, count(u.name) order 18 | - order .*/order by count(u.name) desc, u.name 19 | nextsteps: 20 | rt: 21 | - Compromise them. 22 | - Perform lateral movement. 23 | bt: 24 | - Check security descriptors. 25 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/adcs/find_ca_with_http_web_enrollment_esc8.yaml: -------------------------------------------------------------------------------- 1 | author: e1abrador (Eric Labrador) 2 | name: find_ca_with_http_web_enrollment_esc8 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: ESC8 occur when an Enrollment Service has installed and enabled Web Enrollment via HTTP. 7 | statement: 8 | main: >- 9 | match (ca:CA{domain:'RAS-DOMAIN'}) 10 | where ca.`Web Enrollment` = 'Enabled' 11 | return ca.`CA Name` as CA, ca.`name` as Name 12 | order by CA asc 13 | reference: 14 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=81 15 | - https://github.com/ly4k/Certipy#esc8 16 | - https://research.ifcr.dk/certipy-2-0-bloodhound-new-escalations-shadow-credentials-golden-certificates-and-more-34d1c26f0dc6 17 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=116 18 | nextsteps: 19 | rt: 20 | - NTLM Relay to AD CS HTTP Endpoints - ESC8 21 | bt: 22 | - Harden AD CS HTTP Endpoints - PREVENT8 23 | -------------------------------------------------------------------------------- /queries/tactics/credential_access/kerberoasting_high-value_users.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: kerberoasting_high-value_users 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: Get high-value users with SPNs (kerberoasting). 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN', hasspn:true})-[:MemberOf*1..]->(g:Group{highvalue:true, domain:'RAS-DOMAIN'}) 10 | unwind u.serviceprincipalnames as spn 11 | return distinct u.name as Name, spn 12 | order by Name asc, spn asc 13 | count: 14 | - return .* order/return distinct u.name as Name, count(distinct spn) as Count order 15 | - order .*/order by Count desc, Name asc 16 | nextsteps: 17 | rt: 18 | - Request TGS for all user SPNs and crack them. 19 | bt: 20 | - Ensure all service accounts have long and complex passwords and rotate them periodically. 21 | - Eliminate use of insecure protocols in Kerberos removing RC4 encryption. 22 | - Use sMSA or gMSA for automatic password management. 23 | - Create service account honeypots. 24 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/non-da_with_writing_rights_to_computers.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-da_with_writing_rights_to_computers 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Get non-domain admin users with writing rights to computers. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(:Group{name:'RAS-DA@RAS-DOMAIN'}) 10 | with collect(distinct u.name) as cdu 11 | match (n{enabled:true, domain:'RAS-DOMAIN'})-[r:GenericAll|GenericWrite|WriteProperty|WriteDACL]->(c:Computer{enabled:true, domain:'RAS-DOMAIN'}) 12 | where not n.name in cdu 13 | return n.name, type(r), c.name 14 | order by n.name, type(r), c.name 15 | reference: 16 | - https://www.ired.team/offensive-security-experiments/active-directory-kerberos-abuse/resource-based-constrained-delegation-ad-computer-object-take-over-and-privilged-code-execution 17 | nextsteps: 18 | rt: 19 | - Compromise them. 20 | - Get RCE on those computers. 21 | bt: 22 | - Check security descriptors. 23 | -------------------------------------------------------------------------------- /queries/tactics/lateral_movement/non-highvalue_users_can_dcsync.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-highvalue_users_can_dcsync 3 | state: enabled 4 | tactic: lateral movement 5 | tag: issue 6 | description: Get all non highvalue users that can DCSync. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(:Group{highvalue:true, domain:'RAS-DOMAIN'}) 10 | with collect(distinct u.name) as highvalueusers 11 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 12 | where not u.name in highvalueusers 13 | with u 14 | match p=(u)-[:MemberOf*0..6]->(:Group{domain:'RAS-DOMAIN'})-[:GetChanges|GetChangesAll|GenericAll|DCSync]->(d:Domain{name:'RAS-DOMAIN'}) 15 | where any(x in nodes(p) where ((x)-[:GetChanges]->(d) and (x)-[:GetChangesAll]->(d)) or ((x)-[:GenericAll]->(d)) or ((x)-[:DCSync]->(d))) 16 | return distinct u.name 17 | order by u.name asc 18 | nextsteps: 19 | rt: 20 | - Compromise them. 21 | - Become a domain administrator. 22 | - Get global persistence with golden tickets. 23 | bt: 24 | - Check security descriptors. 25 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/ghost_spn.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: ghost_spn.yaml 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: issue 6 | description: Get ghost SPNs (constrained delegation pointing to non-existent SPNs). 7 | statement: 8 | main: >- 9 | match (n{enabled:true, domain:'RAS-DOMAIN'}) 10 | unwind n.serviceprincipalnames as uspn 11 | with collect(distinct split(split(toUpper(uspn), '/')[1], ':')[0]) as dspn 12 | match (n{enabled:true, domain:'RAS-DOMAIN'}) 13 | unwind n.allowedtodelegate as uatd 14 | with dspn, collect(uatd) as catd 15 | unwind [atd in catd where not split(split(toUpper(atd), '/')[1], ':')[0] in dspn] as spn 16 | return distinct toUpper(spn) as ghostspn order by ghostspn 17 | reference: 18 | - https://www.semperis.com/blog/spn-jacking-an-edge-case-in-writespn-abuse/ 19 | nextsteps: 20 | rt: 21 | - Get admin to the computer with constrained delegation that uses ghost SPNs. 22 | - Get at least WriteSPN to the computer you want to compromise. 23 | bt: 24 | - Remove ghost SPNs from constrained delegation configurations. 25 | -------------------------------------------------------------------------------- /queries/permissions/sql_admin.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: sql_admin 3 | state: enabled 4 | tactic: lateral movement 5 | tag: attack 6 | description: The principal is a SQL admin on the target computer. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:SQLAdmin]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:SQLAdmin]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#sqladmin 20 | nextsteps: 21 | rt: 22 | - Connect to the target computer and get remote code execution. 23 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/non-highvalue_users_reach_da.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-highvalue_users_reach_da 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: issue 6 | description: Get non-highvalue users that reach Domain Admins. 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(:Group{highvalue:true, domain:'RAS-DOMAIN'}) 10 | with collect(distinct u.name) as cdu 11 | match (u:User{enabled:true, domain:'RAS-DOMAIN'}) 12 | where not u.name in cdu 13 | with u 14 | match p=allShortestPaths((u)-[:MemberOf|AdminTo|AllExtendedRights|AddMember|AddSelf|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|Contains|GpLink*1..]->(g:Group{domain:'RAS-DOMAIN'})) 15 | where not u.name starts with 'MSOL_' and g.name in ['RAS-DA@RAS-DOMAIN', 'RAS-EA@RAS-DOMAIN', 'RAS-A@RAS-DOMAIN'] 16 | return distinct u.name 17 | order by u.name 18 | graph: 19 | - return .*/return p 20 | nextsteps: 21 | rt: 22 | - Compromise them. 23 | - Become a domain administrator. 24 | bt: 25 | - Check security descriptors. 26 | -------------------------------------------------------------------------------- /queries/permissions/add_self.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: add_self 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: This edge indicates the principal has the ability to add itself to the target security group. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:AddSelf]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:AddSelf]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#addself 20 | nextsteps: 21 | rt: 22 | - If end node is a Group, add a user. 23 | -------------------------------------------------------------------------------- /queries/permissions/can_rdp.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: admin_to 3 | state: enabled 4 | tactic: lateral movement 5 | tag: attack 6 | description: Remote Desktop access allows you to enter an interactive session with the target computer. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:CanRDP]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:CanRDP]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#canrdp 20 | nextsteps: 21 | rt: 22 | - Connect to the target computer and escalate privileges. 23 | -------------------------------------------------------------------------------- /queries/permissions/admin_to.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: admin_to 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: The principal is a local administrator on the target computer. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:AdminTo]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:AdminTo]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#adminto 20 | nextsteps: 21 | rt: 22 | - Connect to the target computer, disable defenses and dump credentials from memory. 23 | -------------------------------------------------------------------------------- /queries/permissions/add_member.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: add_member 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: This edge indicates the principal has the ability to add arbitrary principals to the target security group. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:AddMember]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:AddMember]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#addmembers 20 | nextsteps: 21 | rt: 22 | - If end node is a Group, add a user. 23 | -------------------------------------------------------------------------------- /queries/permissions/can_ps_remote.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: can_ps_remote 3 | state: enabled 4 | tactic: lateral movement 5 | tag: attack 6 | description: PS Session access allows you to enter an interactive session with the target computer. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:CanPSRemote]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:CanPSRemote]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#canpsremote 20 | nextsteps: 21 | rt: 22 | - Connect to the target computer and escalate privileges. 23 | -------------------------------------------------------------------------------- /queries/permissions/sync_laps_password.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: sync_laps_password 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: This edge indicates the principal has the ability to retrieve the ms-MCs-AdmPwd attribute. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:SyncLAPSPassword]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, dm, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:SyncLAPSPassword]->(de{haslaps:true}) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://simondotsh.com/infosec/2022/07/11/dirsync.html 20 | nextsteps: 21 | rt: 22 | - Read the LAPS password of the affected computer accounts. 23 | -------------------------------------------------------------------------------- /queries/permissions/write_dacl.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: write_dacl 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: With write access to the target object's DACL, you can grant yourself any privilege you want on the object. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:WriteDacl]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:WriteDacl]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#writedacl 20 | nextsteps: 21 | rt: 22 | - Grant yourself GenericAll rights to perform different attacks. 23 | -------------------------------------------------------------------------------- /modules/view.py: -------------------------------------------------------------------------------- 1 | from conf.constants import banner 2 | from modules.error import Error 3 | from modules.presenter import Terminal as pTerminal 4 | 5 | 6 | class Terminal: 7 | 8 | 9 | def __init__(self, verbose, op_mode): 10 | self.op_mode = op_mode 11 | self.output = '' 12 | self.presenter = pTerminal(verbose, op_mode) 13 | self.query_sep = '\n' * 5 14 | self.verbose = verbose 15 | 16 | self.print(f'{banner}') 17 | self.print('') 18 | 19 | 20 | def print(self, data = ''): 21 | sep = False 22 | if isinstance(data, list): 23 | for query in data: 24 | self.print(query) 25 | return 26 | # Query or str 27 | if not isinstance(data, Error): 28 | if 'Query' in type(data).__name__: 29 | sep = True 30 | data = self.presenter.filter(data) 31 | self.output += self.presenter.format(data) 32 | if self.verbose != 'quiet' and self.op_mode != 'shell': 33 | if sep and self.output: 34 | self.output += self.query_sep 35 | 36 | print(self.output, end = '', flush = True) 37 | self.output = '' 38 | -------------------------------------------------------------------------------- /queries/permissions/execute_dcom.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: execute_dcom 3 | state: enabled 4 | tactic: lateral movement 5 | tag: attack 6 | description: Code execution under certain conditions by instantiating a COM object on a remote machine and invoking its methods. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:ExecuteDCOM]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:ExecuteDCOM]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#executedcom 20 | nextsteps: 21 | rt: 22 | - Connect to the target computer and escalate privileges. 23 | -------------------------------------------------------------------------------- /queries/permissions/read_laps_password.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: read_laps_password 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: This edge indicates the principal has the ability to read the password of the local administrator. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:ReadLAPSPassword]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:ReadLAPSPassword]->(de{haslaps:true}) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://www.thehacker.recipes/ad/movement/dacl/readlapspassword 20 | nextsteps: 21 | rt: 22 | - If end node is a Computer, read the password of the local administrator. 23 | -------------------------------------------------------------------------------- /queries/permissions/dcsync.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: dcsync 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: This edge indicates the principal has the ability to perform a DCSync attack to get any password hash on the domain. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:DCSync|GetChanges|GetChangesAll]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:DCSync|GetChanges|GetChangesAll]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://www.thehacker.recipes/ad-ds/movement/credentials/dumping/dcsync 20 | nextsteps: 21 | rt: 22 | - If end node is a Domain, get any password hash on the domain. 23 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/non-highvalue_objects_reach_highvalue_objects.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-highvalue_objects_reach_highvalue_objects 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: issue 6 | description: Get non-highvalue objects with direct relationship with highvalue objects. 7 | statement: 8 | main: >- 9 | match p=(n{domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(g:Group{highvalue:true, domain:'RAS-DOMAIN'}) 10 | with nodes(p) as np 11 | unwind np as unp 12 | with collect(distinct unp) as dunp 13 | match (n{domain:'RAS-DOMAIN'}), (m{domain:'RAS-DOMAIN'}) 14 | where not n in dunp and (n:Group or n.enabled = true) and m in dunp 15 | with n, m 16 | match p=(n)-[r:MemberOf|AdminTo|AllExtendedRights|AddMember|AddSelf|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|Contains|GpLink]->(m) 17 | where not n.name starts with 'MSOL_' 18 | unwind labels(n) as type 19 | unwind labels(m) as typem 20 | return distinct type, n.name, type(r), m.name 21 | order by type, n.name, type(r), m.name 22 | graph: 23 | - return .*/return p 24 | nextsteps: 25 | rt: 26 | - Compromise them. 27 | bt: 28 | - Check security descriptors. 29 | -------------------------------------------------------------------------------- /queries/permissions/read_gmsa_password.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: read_gmsa_password 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: This edge indicates the principal has the ability to read the password of a Group Managed Service Account. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:ReadGMSAPassword]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:ReadGMSAPassword]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/readgmsapassword 20 | nextsteps: 21 | rt: 22 | - If end node is a User, read the password of the Group Managed Service Account. 23 | -------------------------------------------------------------------------------- /queries/permissions/force_change_password.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: force_change_password 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: This edge indicates that the principal can reset the password of the target user without knowing the current password of that user. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:ForceChangePassword]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:ForceChangePassword]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#forcechangepassword 20 | nextsteps: 21 | rt: 22 | - If end node is a User, reset its password. 23 | -------------------------------------------------------------------------------- /queries/tactics/credential_access/non-da_with_generic_rights.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: non-da_with_generic_rights 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: Get non-domain admin users that can modify other users (Reset password or targeted kerberoasting/AS-REP roasting). 7 | statement: 8 | main: >- 9 | match (u:User{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..4]->(:Group{name:'RAS-DA@RAS-DOMAIN'}) 10 | with collect(distinct u.name) as cdu 11 | match (u1:User{enabled:true, domain:'RAS-DOMAIN'})-[r:GenericAll|GenericWrite]->(u2:User{enabled:true, domain:'RAS-DOMAIN'}) 12 | where not u1.name in cdu and not u1.name starts with 'MSOL_' 13 | return distinct u1.name, type(r), u2.name 14 | order by u1.name, type(r), u2.name 15 | count: 16 | - return .* order/return distinct u1.name as Name, count(distinct u2.name) as Count order 17 | - order .*/order by Count desc, Name asc 18 | reference: 19 | - https://www.harmj0y.net/blog/activedirectory/targeted-kerberoasting/ 20 | nextsteps: 21 | rt: 22 | - Compromise them. 23 | - Reset other users passwords. 24 | - Configure other users to perform roasting attacks. 25 | bt: 26 | - Check security descriptors. 27 | -------------------------------------------------------------------------------- /queries/permissions/owner.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: owner 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Object owners retain the ability to modify object security descriptors, regardless of permissions on the object's DACL. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:Owns|WriteOwner]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:Owns|WriteOwner]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#writeowner 20 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/grant-ownership 21 | nextsteps: 22 | rt: 23 | - Grant ownership and then perform attacks as if you have GenericAll rights. 24 | -------------------------------------------------------------------------------- /queries/tactics/credential_access/users_same_timestamp_password.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: users_same_timestamp_password 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: Get users with password set at the same time 7 | statement: 8 | main: >- 9 | match (u1:User{enabled:true, domain:'RAS-DOMAIN'}), (u2:User{enabled:true, domain:'RAS-DOMAIN'}) 10 | where not u1.pwdlastset = -1 and not u1.name = u2.name and u1.pwdlastset = u2.pwdlastset 11 | return distinct u1.name as Name, split(toString(datetime({epochSeconds:toInteger(u1.pwdlastset)})), '.')[0] as Datetime, u1.pwdlastset as Timestamp 12 | order by Timestamp desc, Name asc 13 | count: 14 | - return .* order/return split(toString(datetime({epochSeconds:toInteger(u1.pwdlastset)})), '.')[0] as Datetime, count(distinct u1.name) as Count order 15 | - order .*/order by Count desc, Datetime desc 16 | reference: 17 | - https://posts.specterops.io/case-study-password-analysis-with-bloodhound-a3d264736c7 18 | - https://porterhau5.com/blog/representing-password-reuse-in-bloodhound/ 19 | nextsteps: 20 | rt: 21 | - Compromise them. 22 | - Check if those users share the same password or hash. 23 | bt: 24 | - Discover why passwords are set at the same time. 25 | - Check if passwords have the same hash. 26 | -------------------------------------------------------------------------------- /queries/tactics/credential_access/computers_same_timestamp_password.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: computers_same_timestamp_password 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: Get computers with password set at the same time 7 | statement: 8 | main: >- 9 | match (c1:Computer{enabled:true, domain:'RAS-DOMAIN'}), (c2:Computer{enabled:true, domain:'RAS-DOMAIN'}) 10 | where not c1.pwdlastset = -1 and not c1.name = c2.name and c1.pwdlastset = c2.pwdlastset 11 | return distinct c1.name as Name, split(toString(datetime({epochSeconds:toInteger(c1.pwdlastset)})), '.')[0] as Datetime, c1.pwdlastset as Timestamp 12 | order by Timestamp desc, Name asc 13 | count: 14 | - return .* order/return split(toString(datetime({epochSeconds:toInteger(c1.pwdlastset)})), '.')[0] as Datetime, count(distinct c1.name) as Count order 15 | - order .*/order by Count desc, Datetime desc 16 | reference: 17 | - https://posts.specterops.io/case-study-password-analysis-with-bloodhound-a3d264736c7 18 | - https://porterhau5.com/blog/representing-password-reuse-in-bloodhound/ 19 | nextsteps: 20 | rt: 21 | - Compromise them. 22 | - Check if those computers share the same password or hash. 23 | bt: 24 | - Discover why passwords are set at the same time. 25 | - Check if passwords have the same hash. 26 | -------------------------------------------------------------------------------- /queries/permissions/add_key-credential-link.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: add_key-credential-link 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Grants you the ability to write to the Key-Credential-Link attribute on the target object. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:AddKeyCredentialLink]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:AddKeyCredentialLink]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#addkeycredentiallink 20 | - https://www.thehacker.recipes/ad-ds/movement/kerberos/shadow-credentials 21 | nextsteps: 22 | rt: 23 | - If end node is a User or Computer, and AD CS is enabled, add 'key credentials' to the attribute msDS-KeyCredentialLink. 24 | -------------------------------------------------------------------------------- /queries/permissions/write_spn.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: write_spn 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Grants you the ability to write to the SPN attribute on a target object. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:WriteSPN]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:WriteSPN]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#writespn 20 | - https://www.thehacker.recipes/ad/movement/kerberos/spn-jacking 21 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/targeted-kerberoasting 22 | nextsteps: 23 | rt: 24 | - If end node is a Computer, perform an SPN-jacking attack. 25 | - If end node is a User, perform a targeted kerberoasting attack. 26 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/unconstrained_delegation.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: unconstrained_delegation 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Get accounts with unconstrained delegation. 7 | statement: 8 | main: >- 9 | match (n{enabled:true, domain:'RAS-DOMAIN'})-[:MemberOf*1..]->(:Group{name:'RAS-DC@RAS-DOMAIN'}) 10 | with collect(distinct n.name) as cdn 11 | match (n{enabled:true, domain:'RAS-DOMAIN'}) 12 | where n.unconstraineddelegation = true and not n.name in cdn 13 | unwind labels(n) as type 14 | unwind (case n.serviceprincipalnames when [] then [''] else n.serviceprincipalnames end) as spn 15 | return n.name, type, spn 16 | order by type, n.name, spn asc 17 | reference: 18 | - https://shenaniganslabs.io/media/Constructing%20Kerberos%20Attacks%20with%20Delegation%20Primitives.pdf 19 | - https://ired.team/offensive-security-experiments/active-directory-kerberos-abuse/domain-compromise-via-unrestricted-kerberos-delegation 20 | - https://www.tarlogic.com/es/blog/kerberos-iii-como-funciona-la-delegacion/ 21 | nextsteps: 22 | rt: 23 | - Compromise them. 24 | - Use the 'Printer bug' (MS-RPRN), 'PetitPotam' (MS-EFSR), 'ShadowCoerce' (MS-FSRVP) or 'DFSCoerce' (MS-DFSNM) to coerce machine authentication. 25 | - Dump TGTs from memory. 26 | bt: 27 | - Evaluate the use of resource-based constrained delegation. 28 | -------------------------------------------------------------------------------- /queries/permissions/allowed_to_act.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: allowed_to_act 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Execute a modified S4U2self/S4U2proxy abuse chain to impersonate any domain user to the target computer system and receive a valid service ticket as this user. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:AddAllowedToAct|AllowedToAct]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:AddAllowedToAct|AllowedToAct]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#allowedtoact 20 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#addallowedtoact 21 | - https://blog.netspi.com/cve-2020-17049-kerberos-bronze-bit-overview/ 22 | nextsteps: 23 | rt: 24 | - If end node is a Computer, impersonate any domain user on it (RBCD attack). 25 | -------------------------------------------------------------------------------- /queries/permissions/allowed_to_delegate.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: allowed_to_delegate 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Impersonate any domain principal (except Protected Users) to the specific service on the target host. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:AllowedToDelegate]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:AllowedToDelegate]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#allowedtodelegate 20 | - https://blog.netspi.com/cve-2020-17049-kerberos-bronze-bit-overview/ 21 | nextsteps: 22 | rt: 23 | - Check if userAccountControl has TRUSTED_TO_AUTH_FOR_DELEGATION value to directly request TGS on behalf of any user (S4U2self). 24 | - Check the SPN server name/port and try to change the service name to abuse other services on the same server. 25 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/constrained_delegation.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: constrained delegation 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Get accounts with constrained delegation. 7 | statement: 8 | main: >- 9 | match (n{enabled:true, domain:'RAS-DOMAIN'}) 10 | where n.allowedtodelegate and not (n)-[:MemberOf]->(:Group{name:'RAS-DC@RAS-DOMAIN'}) 11 | unwind labels(n) as type 12 | unwind n.allowedtodelegate as atd 13 | return type, n.name, atd 14 | order by type, n.name, atd 15 | reference: 16 | - https://shenaniganslabs.io/media/Constructing%20Kerberos%20Attacks%20with%20Delegation%20Primitives.pdf 17 | - https://ired.team/offensive-security-experiments/active-directory-kerberos-abuse/abusing-kerberos-constrained-delegation 18 | - https://blog.netspi.com/cve-2020-17049-kerberos-bronze-bit-overview/ 19 | - https://www.tarlogic.com/es/blog/kerberos-iii-como-funciona-la-delegacion/ 20 | nextsteps: 21 | rt: 22 | - Compromise them. 23 | - Check if the user has the NOT_DELEGATED flag or belongs to the Protected Users group. 24 | - Check if userAccountControl has TRUSTED_TO_AUTH_FOR_DELEGATION value to directly request forwardable TGS on behalf of any user (S4U2self). 25 | - Check the SPN server name/port and try to change the service name to abuse other services on the same server. 26 | - Dump TGSs from memory. 27 | - Perform impersonation attacks (bronze bit). 28 | bt: 29 | - Evaluate the use of resource-based constrained delegation. 30 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/resource-based_constrained_delegation.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: resource-based_constrained_delegation 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Get accounts with resource-based constrained delegation. 7 | statement: 8 | main: >- 9 | match (d:Domain{domain:'RAS-DOMAIN'}) 10 | where d.functionallevel =~ '(?i).*(2012|2016|2019).*' 11 | match (n{enabled:true, domain:'RAS-DOMAIN'})-[r:AddAllowedToAct|AllowedToAct]->(c:Computer{enabled:true, domain:'RAS-DOMAIN'}) 12 | unwind labels(n) as type 13 | return distinct type, n.name, type(r), c.name 14 | order by type, n.name, type(r), c.name 15 | reference: 16 | - https://shenaniganslabs.io/media/Constructing%20Kerberos%20Attacks%20with%20Delegation%20Primitives.pdf 17 | - https://ired.team/offensive-security-experiments/active-directory-kerberos-abuse/resource-based-constrained-delegation-ad-computer-object-take-over-and-privilged-code-execution 18 | - https://blog.netspi.com/cve-2020-17049-kerberos-bronze-bit-overview/ 19 | - https://www.tarlogic.com/es/blog/kerberos-iii-como-funciona-la-delegacion/ 20 | nextsteps: 21 | rt: 22 | - Compromise them. 23 | - Check if the user has the NOT_DELEGATED flag or belongs to the Protected Users group. 24 | - Use S4U2self to get a non-forwardable TGS for any user, to exchange it with the KDC for a forwardable TGS. 25 | - Dump TGSs from memory. 26 | - Perform impersonation attacks (bronze bit). 27 | bt: 28 | - Check security descriptors. 29 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/adcs/find_misconfigured_certificate_templates_esc2.yaml: -------------------------------------------------------------------------------- 1 | author: e1abrador (Eric Labrador) 2 | name: find_misconfigured_certificate_templates_esc2 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: ESC2 occurs when a certificate template can be used for any purpose. 7 | statement: 8 | main: >- 9 | match (ct:CertificateTemplate{domain:'RAS-DOMAIN', Enabled:true}) 10 | where ct.`Any Purpose` = true 11 | return distinct ct.`Template Name` as `Template Name`, ct.`Any Purpose` as `Any Purpose` 12 | order by `Any Purpose` desc, `Template Name` asc 13 | reference: 14 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=65 15 | - https://github.com/ly4k/Certipy#esc2 16 | - https://research.ifcr.dk/certipy-2-0-bloodhound-new-escalations-shadow-credentials-golden-certificates-and-more-34d1c26f0dc6 17 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=106 18 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=114 19 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=119 20 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=125 21 | nextsteps: 22 | rt: 23 | - Misconfigured Certificate Templates - ESC2 24 | bt: 25 | - Harden Certificate Template Settings - PREVENT4 26 | - Enforce Strict User Mappings - PREVENT7 27 | - Monitor User/Machine Certificate Enrollments - DETECT1 28 | - Monitor Certificate Authentication Events - DETECT2 29 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/adcs/find_misconfigured_enrollment_agent_templates_esc3.yaml: -------------------------------------------------------------------------------- 1 | author: e1abrador (Eric Labrador) 2 | name: find_misconfigured_enrollment_agent_templates_esc3 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: ESC3 occurs when a certificate template specifies the Certificate Request Agent EKU (Enrollment Agent). This EKU can be used to request certificates on behalf of other users. 7 | statement: 8 | main: >- 9 | match (ct:CertificateTemplate{domain:'RAS-DOMAIN', Enabled:true}) 10 | where ct.`Enrollment Agent` = true 11 | return distinct ct.`Template Name` as `Template Name`, ct.`Enrollment Agent` as `Enrollment Agent` 12 | order by `Enrollment Agent` desc, `Template Name` asc 13 | reference: 14 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=66 15 | - https://github.com/ly4k/Certipy#esc3 16 | - https://research.ifcr.dk/certipy-2-0-bloodhound-new-escalations-shadow-credentials-golden-certificates-and-more-34d1c26f0dc6 17 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=101 18 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=106 19 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=119 20 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=125 21 | nextsteps: 22 | rt: 23 | - Misconfigured Enrollment Agent Templates - ESC3 24 | bt: 25 | - Harden CA Settings - PREVENT2 26 | - Harden Certificate Template Settings - PREVENT4 27 | - Monitor User/Machine Certificate Enrollments - DETECT1 28 | - Monitor Certificate Authentication Events - DETECT2 29 | -------------------------------------------------------------------------------- /queries/permissions/all_extended_rights.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: all_extended_rights 3 | state: enabled 4 | tactic: credential access 5 | tag: attack 6 | description: Extended rights are special rights granted on objects which allow reading of privileged attributes, as well as performing special actions. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:AllExtendedRights]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:AllExtendedRights]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#allextendedrights 20 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/readlapspassword 21 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/readgmsapassword 22 | - https://www.thehacker.recipes/ad-ds/movement/credentials/dumping/dcsync 23 | nextsteps: 24 | rt: 25 | - If end node is a Group, add a user. 26 | - If end node is a Computer, read the password of the local administrator. 27 | - If end node is a User, read the password of the Group Managed Service Account. 28 | - If end node is a User, reset its password. 29 | - If end node is a Domain, get any password hash on the domain. 30 | -------------------------------------------------------------------------------- /queries/tactics/privilege_escalation/adcs/find_misconfigured_certificate_templates_esc1.yaml: -------------------------------------------------------------------------------- 1 | author: e1abrador (Eric Labrador) 2 | name: find_misconfigured_certificate_templates_esc1 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: ESC1 occurs when a certificate template permits Client Authentication and allows the enrollee to supply an arbitrary Subject Alternative Name (SAN). 7 | statement: 8 | main: >- 9 | match (ct:CertificateTemplate{domain:'RAS-DOMAIN', Enabled:true}) 10 | where ct.`Enrollee Supplies Subject` = true and ct.`Client Authentication` = true 11 | return distinct ct.`Template Name` as `Template Name`, ct.`Client Authentication` as `Client Authentication`, ct.`Enrollee Supplies Subject` as `Enrollee Supplies Subject` 12 | order by `Client Authentication` desc, `Enrollee Supplies Subject` asc, `Template Name` asc 13 | reference: 14 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=58 15 | - https://github.com/ly4k/Certipy#esc1 16 | - https://research.ifcr.dk/certipy-2-0-bloodhound-new-escalations-shadow-credentials-golden-certificates-and-more-34d1c26f0dc6 17 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=106 18 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=114 19 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=119 20 | - https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf#page=125 21 | nextsteps: 22 | rt: 23 | - Misconfigured Certificate Templates - ESC1 24 | bt: 25 | - Harden Certificate Template Settings - PREVENT4 26 | - Enforce Strict User Mappings - PREVENT7 27 | - Monitor User/Machine Certificate Enrollments - DETECT1 28 | - Monitor Certificate Authentication Events - DETECT2 29 | -------------------------------------------------------------------------------- /queries/permissions/generic_write.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: generic_write 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Grants you the ability to write to any non-protected attribute on the target object. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:GenericWrite]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:GenericWrite]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#genericwrite 20 | - https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab 21 | - https://www.thehacker.recipes/ad-ds/movement/kerberos/shadow-credentials 22 | - https://www.thehacker.recipes/ad-ds/movement/kerberos/delegations#resource-based-constrained-delegations-rbcd 23 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/logon-script 24 | - https://www.thehacker.recipes/ad/movement/kerberos/spn-jacking 25 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/targeted-kerberoasting 26 | - https://www.thehacker.recipes/ad-ds/movement/group-policy-objects 27 | nextsteps: 28 | rt: 29 | - If end node is a Computer, impersonate any domain user on it (RBCD attack). 30 | - If end node is a Computer, perform an SPN-jacking attack. 31 | - If end node is a GPO, perform an immediate scheduled task. 32 | - If end node is a Group, add a user. 33 | - If end node is a User or Computer, and AD CS is enabled, add 'key credentials' to the attribute msDS-KeyCredentialLink. 34 | - If end node is a User, execute a custom script at user logon. 35 | - If end node is a User, perform a targeted kerberoasting attack. 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 0 * * *' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['python'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v1 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /queries/permissions/generic_all.yaml: -------------------------------------------------------------------------------- 1 | author: rastreator 2 | name: generic_all 3 | state: enabled 4 | tactic: privilege escalation 5 | tag: attack 6 | description: Also known as full control. This privilege allows the trustee to manipulate the target object however they wish. 7 | statement: 8 | main: >- 9 | match (startNode:RAS-START_NODE_TYPE{RAS-START_NODE_NAME})-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode) 10 | with distinct startNode as ds, middleNode as dm 11 | match (dm)-[:GenericAll]->(endNode:RAS-END_NODE_TYPE{RAS-END_NODE_NAME}) 12 | with distinct ds, endNode as de 13 | match (ds)-[:AddMember|AddSelf|Contains|GpLink|HasSIDHistory|MemberOf|Owns|TrustedBy|RAS-HAS_SESSION*0..6]->(middleNode)-[r:GenericAll]->(de) 14 | unwind labels(ds) as stype 15 | unwind labels(de) as etype 16 | return distinct stype, ds.name as sname, type(r) as right, etype, de.name as ename 17 | order by stype, sname, etype, ename 18 | reference: 19 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#genericall 20 | - https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html#forcechangepassword 21 | - https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab 22 | - https://www.thehacker.recipes/ad-ds/movement/kerberos/shadow-credentials 23 | - https://www.thehacker.recipes/ad-ds/movement/kerberos/delegations#resource-based-constrained-delegations-rbcd 24 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/logon-script 25 | - https://www.thehacker.recipes/ad/movement/kerberos/spn-jacking 26 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/targeted-kerberoasting 27 | - https://www.thehacker.recipes/ad-ds/movement/group-policy-objects 28 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/readlapspassword 29 | - https://www.thehacker.recipes/ad-ds/movement/access-control-entries/readgmsapassword 30 | - https://www.thehacker.recipes/ad-ds/movement/credentials/dumping/dcsync 31 | nextsteps: 32 | rt: 33 | - If end node is a Computer, impersonate any domain user on it (RBCD attack). 34 | - If end node is a Computer, read the password of the local administrator. 35 | - If end node is a Computer, perform an SPN-jacking attack. 36 | - If end node is a Domain, get any password hash on the domain. 37 | - If end node is a GPO, perform an immediate scheduled task. 38 | - If end node is a Group, add a user. 39 | - If end node is a User or Computer, and AD CS is enabled, add 'key credentials' to the attribute msDS-KeyCredentialLink. 40 | - If end node is a User, execute a custom script at user logon. 41 | - If end node is a User, perform a targeted kerberoasting attack. 42 | - If end node is a User, read the password of the Group Managed Service Account. 43 | - If end node is a User, reset its password. 44 | -------------------------------------------------------------------------------- /conf/constants.py: -------------------------------------------------------------------------------- 1 | banner = f'Rastreator\n > Tool with a collection of query files to explore Microsoft Active Directory\n > Developed by @interh4ck and @t0-n1' 2 | 3 | 4 | error = { 5 | 'compile': { 6 | 'pattern': 'Compiling {type} query: Wrong regex "{pattern}" - {e}' 7 | }, 8 | 'csv': { 9 | 'dump': 'Dumping CSV to: {e}', 10 | 'save': 'Saving CSV to {filename}: {e}' 11 | }, 12 | 'file': { 13 | 'open': 'Opening {filename} in {mode} mode: {e}', 14 | 'read': 'Reading {filename}: {e}' 15 | }, 16 | 'json': { 17 | 'dump': 'Dumping JSON: {e}', 18 | 'save': 'Saving JSON to {filename}: {e}' 19 | }, 20 | 'neo4j': { 21 | 'connect': 'Connecting to Neo4j: {e}', 22 | 'execute': 'Executing cstatement: {statement} - {e}', 23 | 'interrupted': 'Interrupted (ctrl+c) cstatement: {statement}' 24 | }, 25 | 'parse': { 26 | 'key': 'Parsing query format: "{key}" key is not set', 27 | 'object': 'Parsing query format: "{bad}" is not a good object value for "{key}" key. Expected a "{good}" object', 28 | 'value': 'Parsing query format: "{value}" value is not valid for "{key}" key' 29 | }, 30 | 'yaml': { 31 | 'dump': 'Dumping YAML: {e}', 32 | 'read': 'Reading YAML {filename}: {e}', 33 | 'save': 'Saving YAML to {filename}: {e}' 34 | } 35 | } 36 | 37 | 38 | parser = { 39 | 'choices': { 40 | 'ad': { 41 | 'lang': ['en', 'es'] 42 | }, 43 | 'audit': { 44 | 'persistence': { 45 | 'format': ['csv', 'json', 'none', 'yaml'] 46 | } 47 | }, 48 | 'check': { 49 | 'persistence': { 50 | 'format': ['none', 'yaml'] 51 | } 52 | }, 53 | 'neo4j': { 54 | 'encrypted': ['false', 'true'] 55 | }, 56 | 'output': { 57 | 'format': ['csv', 'json', 'table', 'yaml'] 58 | }, 59 | 'path': { 60 | 'has_session': ['false', 'true'], 61 | 'persistence': { 62 | 'format': ['csv', 'json', 'none', 'yaml'] 63 | } 64 | }, 65 | 'query': { 66 | 'mode': ['raw', 'test', 'default'] 67 | }, 68 | 'verbose': { 69 | 'mode': ['quiet', 'default', 'debug'] 70 | } 71 | }, 72 | 'help': { 73 | 'ad': { 74 | 'domain': 'Active Directory domain name', 75 | 'lang': 'Active Directory language' 76 | }, 77 | 'execute': { 78 | 'command': 'Semicolon separated commands inside single/double quotes' 79 | }, 80 | 'input': { 81 | 'directory_or_file': 'Input directory or specific query file' 82 | }, 83 | 'neo4j': { 84 | 'encrypted': 'Neo4j encrypted communication', 85 | 'host': 'Neo4j host to connect', 86 | 'password': 'Neo4j password', 87 | 'port': 'Neo4j port to connect', 88 | 'username': 'Neo4j username' 89 | }, 90 | 'output': { 91 | 'format': 'Output format to show executed query results on screen', 92 | 'directory': 'Output directory to save results' 93 | }, 94 | 'path': { 95 | 'has_session': 'Accept paths with HasSession', 96 | 'start_node': 'Start node of the path', 97 | 'end_node': 'End node of the path' 98 | }, 99 | 'query': { 100 | 'mode': 'Query submode' 101 | }, 102 | 'persistence': { 103 | 'format': 'File format to save executed query results' 104 | }, 105 | 'verbose': { 106 | 'mode': 'Verbose mode' 107 | } 108 | } 109 | } 110 | 111 | 112 | shell = { 113 | 'help': { 114 | 'clean': 'base nodes', 115 | 'exit': 'this program (ctrl+d)', 116 | 'help': 'shows this help', 117 | 'set': 'the environment variables', 118 | 'query': 'Example: > match (u:User{enabled:true}) return u.name limit 10' 119 | }, 120 | 'history': '.shell_history', 121 | 'menu': { 122 | 'clean': None, 123 | 'exit': None, 124 | 'help': None, 125 | 'set': { 126 | 'domain': None, 127 | 'lang': {}, 128 | 'multiline': { 129 | 'false': None, 130 | 'true': None 131 | }, 132 | 'output': {}, 133 | } 134 | } 135 | } 136 | 137 | 138 | for e in parser['choices']['ad']['lang']: 139 | shell['menu']['set']['lang'][e] = None 140 | 141 | for e in parser['choices']['output']['format']: 142 | shell['menu']['set']['output'][e] = None 143 | -------------------------------------------------------------------------------- /rastreator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | from argparse import ArgumentParser, RawTextHelpFormatter 5 | from conf.constants import banner, parser as cparser 6 | from conf.defaults import parser as dparser 7 | from modules.controller import Terminal 8 | 9 | 10 | if __name__ == '__main__': 11 | 12 | ap = ArgumentParser( 13 | description = f'{banner}', 14 | formatter_class=RawTextHelpFormatter 15 | ) 16 | 17 | subparsers = ap.add_subparsers(required = True, dest = 'op_mode') 18 | sp = {} 19 | 20 | for op_mode in ['audit', 'check', 'execute', 'path', 'shell']: 21 | sp[op_mode] = subparsers.add_parser( 22 | op_mode, 23 | help = f'{op_mode.capitalize()} mode' 24 | ) 25 | sp[op_mode].add_argument( 26 | '-v', 27 | choices = cparser['choices']['verbose']['mode'], 28 | dest = 'verbose_mode', 29 | default = dparser['verbose']['mode'], 30 | help = cparser['help']['verbose']['mode'] 31 | ) 32 | 33 | for op_mode in ['audit', 'execute', 'path', 'shell']: 34 | sp[op_mode].add_argument( 35 | '-H', 36 | dest = 'neo4j_host', 37 | default = dparser['neo4j']['host'], 38 | help = cparser['help']['neo4j']['host'] 39 | ) 40 | sp[op_mode].add_argument( 41 | '-P', 42 | dest = 'neo4j_port', 43 | default = dparser['neo4j']['port'], 44 | help = cparser['help']['neo4j']['port'] 45 | ) 46 | sp[op_mode].add_argument( 47 | '-u', 48 | dest = 'neo4j_username', 49 | default = dparser['neo4j']['username'], 50 | help = cparser['help']['neo4j']['username'] 51 | ) 52 | sp[op_mode].add_argument( 53 | '-p', 54 | dest = 'neo4j_password', 55 | default = dparser['neo4j']['password'], 56 | help = cparser['help']['neo4j']['password'] 57 | ) 58 | sp[op_mode].add_argument( 59 | '-e', 60 | choices = cparser['choices']['neo4j']['encrypted'], 61 | dest = 'neo4j_encrypted', 62 | default = dparser['neo4j']['encrypted'], 63 | help = cparser['help']['neo4j']['encrypted'] 64 | ) 65 | 66 | for op_mode in ['audit', 'check', 'path']: 67 | sp[op_mode].add_argument( 68 | '-I', 69 | dest = 'input_directory_or_file', 70 | required = True, 71 | help = cparser['help']['input']['directory_or_file'] 72 | ) 73 | sp[op_mode].add_argument( 74 | '-O', 75 | dest = 'output_directory', 76 | default = dparser['output']['directory'], 77 | help = cparser['help']['output']['directory'] 78 | ) 79 | sp[op_mode].add_argument( 80 | '-o', 81 | choices = cparser['choices'][op_mode]['persistence']['format'], 82 | dest = 'persistence_format', 83 | default = dparser[op_mode]['persistence']['format'], 84 | help = cparser['help']['persistence']['format'] 85 | ) 86 | 87 | for op_mode in ['audit', 'path']: 88 | sp[op_mode].add_argument( 89 | '-f', 90 | choices = cparser['choices']['output']['format'], 91 | dest = 'output_format', 92 | default = dparser['output']['format'], 93 | help = cparser['help']['output']['format'] 94 | ) 95 | sp[op_mode].add_argument( 96 | '-l', 97 | choices = cparser['choices']['ad']['lang'], 98 | dest = 'ad_lang', 99 | default = dparser['ad']['lang'], 100 | help = cparser['help']['ad']['lang'] 101 | ) 102 | sp[op_mode].add_argument( 103 | '-d', 104 | dest = 'ad_domain', 105 | required = True, 106 | help = cparser['help']['ad']['domain'] 107 | ) 108 | sp[op_mode].add_argument( 109 | '-m', 110 | choices = cparser['choices']['query']['mode'], 111 | dest = 'query_mode', 112 | default = dparser['query']['mode'], 113 | help = cparser['help']['query']['mode'] 114 | ) 115 | 116 | for op_mode in ['path']: 117 | sp[op_mode].add_argument( 118 | '-S', 119 | dest = 'start_node', 120 | default = dparser[op_mode]['start_node'], 121 | help = cparser['help'][op_mode]['start_node'] 122 | ) 123 | sp[op_mode].add_argument( 124 | '-E', 125 | dest = 'end_node', 126 | default = dparser[op_mode]['end_node'], 127 | help = cparser['help'][op_mode]['end_node'] 128 | ) 129 | sp[op_mode].add_argument( 130 | '-s', 131 | choices = cparser['choices'][op_mode]['has_session'], 132 | dest = 'has_session', 133 | default = dparser[op_mode]['has_session'], 134 | help = cparser['help'][op_mode]['has_session'] 135 | ) 136 | 137 | for op_mode in ['execute']: 138 | sp[op_mode].add_argument( 139 | '-c', 140 | dest = 'command', 141 | help = cparser['help'][op_mode]['command'] 142 | ) 143 | 144 | Terminal(ap.parse_args()) 145 | -------------------------------------------------------------------------------- /modules/io.py: -------------------------------------------------------------------------------- 1 | from conf.constants import error 2 | from csv import DictWriter, QUOTE_ALL 3 | from datetime import datetime 4 | from json import dump as jdump, dumps as jdumps 5 | from modules.error import Error 6 | from neo4j import GraphDatabase 7 | from os import makedirs, path 8 | from yaml import dump as ydump, load, SafeLoader 9 | 10 | 11 | class File: 12 | 13 | 14 | error = Error() 15 | fd = None 16 | name = None 17 | 18 | 19 | def __init__(self, directory = None): 20 | if directory: 21 | timestamp = datetime.now().strftime('%Y.%m.%d-%H.%M.%S') 22 | self.directory = f'{directory}/{timestamp}' 23 | if not path.exists(self.directory): 24 | makedirs(self.directory) 25 | 26 | 27 | def __del__(self): 28 | if File.fd: 29 | File.fd.close() 30 | File.name = None 31 | 32 | 33 | def read(self, filename): 34 | self.open(filename) 35 | if not self.error: 36 | try: 37 | return File.fd.read().splitlines() 38 | except Exception as e: 39 | File.error.add(error['file']['read'].format( 40 | e = e, 41 | filename = filename 42 | )) 43 | return None 44 | 45 | 46 | def open(self, filename, mode = 'read'): 47 | try: 48 | if mode == 'write': 49 | m = 'w' 50 | File.name = f'{self.directory}/{filename}' 51 | else: 52 | m = 'r' 53 | File.name = filename 54 | File.fd = open(File.name, mode = m, encoding = 'utf-8') 55 | except Exception as e: 56 | File.error.add(error['file']['open'].format( 57 | e = e, 58 | filename = File.name, 59 | mode = mode 60 | )) 61 | 62 | 63 | class CSV(File): 64 | 65 | 66 | def __init__(self, directory = None): 67 | File.__init__(self, directory) 68 | 69 | 70 | def dump(self, data, filename = None): 71 | if data: 72 | fieldnames = data[0].keys() 73 | 74 | # Dump to file 75 | if filename: 76 | filename = f'{filename}.csv' 77 | self.open(filename, 'write') 78 | if not self.error: 79 | try: 80 | writer = DictWriter( 81 | self.fd, 82 | delimiter = ';', 83 | quoting = QUOTE_ALL, 84 | fieldnames = fieldnames 85 | ) 86 | writer.writeheader() 87 | writer.writerows(data) 88 | except Exception as e: 89 | CSV.error.add(error['csv']['dump'].format( 90 | e = e, 91 | filename = self.name 92 | )) 93 | data = None 94 | 95 | # Return dump 96 | else: 97 | output = f'{";".join(fieldnames)}\n' 98 | for row in data: 99 | output += f'{";".join(list(row.values()))}\n' 100 | data = output[:-1] 101 | 102 | return data 103 | 104 | 105 | class JSON(File): 106 | 107 | 108 | def __init__(self, directory = None): 109 | File.__init__(self, directory) 110 | 111 | 112 | def dump(self, data, filename = None): 113 | 114 | # Dump to file 115 | if filename: 116 | filename = f'{filename}.json' 117 | self.open(filename, 'write') 118 | if not self.error: 119 | try: 120 | jdump(data, self.fd) 121 | self.fd.write('\n') 122 | except Exception as e: 123 | JSON.error.add(error['json']['save'].format( 124 | e = e, 125 | filename = self.name 126 | )) 127 | data = None 128 | 129 | # Return dump 130 | else: 131 | try: 132 | data = jdumps(data) 133 | except Exception as e: 134 | YAML.error.add(error['json']['dump'].format(e = e)) 135 | 136 | return data 137 | 138 | 139 | class YAML(File): 140 | 141 | 142 | def __init__(self, directory = None): 143 | File.__init__(self, directory) 144 | 145 | 146 | def dump(self, data, filename = None): 147 | 148 | # Dump to file 149 | if filename: 150 | filename = f'{filename}.yaml' 151 | self.open(filename, 'write') 152 | if not self.error: 153 | try: 154 | ydump( 155 | data, 156 | default_flow_style = False, 157 | sort_keys = False, 158 | stream = self.fd 159 | ) 160 | except Exception as e: 161 | YAML.error.add(error['yaml']['save'].format( 162 | e = e, 163 | filename = self.name 164 | )) 165 | data = None 166 | 167 | # Return dump 168 | else: 169 | try: 170 | data = ydump( 171 | data, 172 | default_flow_style = False, 173 | sort_keys = False 174 | ) 175 | data = data[:-1] 176 | except Exception as e: 177 | YAML.error.add(error['yaml']['dump'].format(e = e)) 178 | 179 | return data 180 | 181 | 182 | def read(self, filename): 183 | data = None 184 | self.open(filename) 185 | if not self.error: 186 | try: 187 | data = load(self.fd, Loader = SafeLoader) 188 | except Exception as e: 189 | YAML.error.add(error['yaml']['read'].format( 190 | e = e, 191 | filename = filename 192 | )) 193 | return data 194 | 195 | 196 | class Neo4j: 197 | 198 | 199 | def __init__(self, host, port, username, password, encrypted): 200 | self.error = Error() 201 | self.uri = f'bolt://{host}:{port}' 202 | self.user = username 203 | self.password = password 204 | self.encrypted = bool(encrypted == 'true') 205 | try: 206 | self.driver = GraphDatabase.driver( 207 | self.uri, 208 | auth = (self.user, self.password), 209 | encrypted = self.encrypted 210 | ) 211 | except Exception as e: 212 | self.error.add(error['neo4j']['connect'].format(e = e)) 213 | self.clean() 214 | 215 | 216 | def __del__(self): 217 | if 'driver' in self.__dict__: 218 | self.driver.close() 219 | 220 | 221 | def clean(self): 222 | self.result = { 223 | 'key': None, 224 | 'record': None 225 | } 226 | 227 | 228 | def execute(self, statement): 229 | self.clean() 230 | with self.driver.session() as session: 231 | try: 232 | result = session.run(statement) 233 | self.result = self.normalize(result.data()) 234 | except Exception as e: 235 | self.error.add(error['neo4j']['execute'].format( 236 | e = e, 237 | statement = statement 238 | )) 239 | except KeyboardInterrupt: 240 | # ctrl+c 241 | self.error.add( 242 | error['neo4j']['interrupted'].format(statement = statement) 243 | ) 244 | self.driver.close() 245 | 246 | 247 | def normalize(self, results): 248 | for row in results: 249 | for k, v in row.items(): 250 | row[k] = str(v) 251 | return results 252 | -------------------------------------------------------------------------------- /modules/presenter.py: -------------------------------------------------------------------------------- 1 | from modules.error import Error 2 | from modules.io import CSV, JSON, YAML 3 | from modules.model import Result 4 | from prettytable import PrettyTable 5 | 6 | 7 | class Presenter: 8 | 9 | 10 | def __init__(self, verbose, op_mode): 11 | self.verbose = verbose 12 | self.op_mode = op_mode 13 | 14 | 15 | def filter(self, data): 16 | 17 | verbose = self.verbose 18 | 19 | if 'Query' in type(data).__name__: 20 | 21 | exclude = True 22 | 23 | # If there is an error 24 | if data.error: 25 | attribute = { 26 | 'filename': None, 27 | 'error': None 28 | } 29 | exclude = False 30 | else: 31 | # If verbose is quiet 32 | if verbose == 'quiet': 33 | if self.op_mode == 'execute': 34 | # Remove execution time 35 | data.result['main'].time = None 36 | return data.result['main'] 37 | elif self.op_mode == 'shell': 38 | verbose = 'default' 39 | else: 40 | # Remove query 41 | return None 42 | # If verbose is default 43 | if verbose == 'default': 44 | # Check mode 45 | if self.op_mode == 'check': 46 | attribute = { 47 | 'filename': None, 48 | 'result': { 49 | 'built_query': None, 50 | 'diff': None 51 | } 52 | } 53 | if not data.result['diff']: 54 | return None 55 | exclude = False 56 | # Execute/Shell mode 57 | elif self.op_mode in ['execute', 'shell']: 58 | # With query results 59 | if 'main' in data.result: 60 | attribute = { 61 | 'result': { 62 | 'main': None 63 | } 64 | } 65 | exclude = False 66 | # Remove execution time 67 | data.result['main'].time = None 68 | # No query results 69 | else: 70 | # Remove query 71 | return None 72 | # Audit mode 73 | else: 74 | # With query results 75 | if data.result['main'].data: 76 | attribute = { 77 | 'author': None, 78 | 'cstatement': None, 79 | 'error': None, 80 | 'name': None, 81 | 'reference': None, 82 | 'result': { 83 | 'main': None 84 | }, 85 | 'statement': None, 86 | 'state': None 87 | } 88 | # Remove empty attributes 89 | self.filter_empty( 90 | data.result, 91 | attribute, 92 | 'result', 93 | ['main'] 94 | ) 95 | if 'nextsteps' in data.__dict__: 96 | self.filter_empty( 97 | data.nextsteps, 98 | attribute, 99 | 'nextsteps' 100 | ) 101 | # Remove execution time 102 | for stype in ['count', 'main']: 103 | if stype in data.result: 104 | data.result[stype].time = None 105 | # No query results 106 | else: 107 | # Remove query 108 | return None 109 | # If verbose is debug 110 | elif verbose == 'debug': 111 | # Check mode 112 | if self.op_mode == 'check': 113 | attribute = { 114 | 'filename': None, 115 | 'result': { 116 | 'built_query': None, 117 | 'diff': None 118 | } 119 | } 120 | exclude = False 121 | # Execute/Shell mode 122 | elif self.op_mode in ['execute', 'shell']: 123 | attribute = { 124 | 'cstatement': { 125 | 'main': None 126 | }, 127 | 'result': { 128 | 'main': None 129 | } 130 | } 131 | exclude = False 132 | # Audit mode 133 | else: 134 | attribute = { 135 | 'error': None 136 | } 137 | 138 | qa = data.get(attribute, exclude) 139 | 140 | # Return the filtered query 141 | return qa 142 | 143 | # Format a string 144 | elif isinstance(data, str): 145 | if verbose != 'quiet': 146 | return data 147 | return None 148 | 149 | 150 | def filter_empty(self, data, attribute, key, exceptions = []): 151 | d = {} 152 | e = {} 153 | if data: 154 | for k, v in data.items(): 155 | if k not in exceptions: 156 | if v: 157 | d[k] = None 158 | else: 159 | e[k] = None 160 | if d: 161 | if e: 162 | if key in attribute: 163 | attribute[key].update(e) 164 | else: 165 | attribute[key] = e 166 | else: 167 | attribute[key] = None 168 | 169 | 170 | class Terminal(Presenter): 171 | 172 | 173 | def __init__(self, verbose, op_mode): 174 | Presenter.__init__(self, verbose, op_mode) 175 | 176 | self.built_query_sep = '·' * 64 177 | self.diff_sep = '-' * 8 178 | 179 | 180 | def diff(self, result): 181 | lines = '' 182 | for e in result: 183 | lines += f'{e["line"]}\n' 184 | lines += f'< {e["<"]}\n' 185 | lines += f'> {e[">"]}\n' 186 | lines += f'{self.diff_sep}\n' 187 | 188 | return lines 189 | 190 | 191 | def format(self, data, indent = ''): 192 | output = '' 193 | 194 | # Format an error 195 | if isinstance(data, Error): 196 | for error in data: 197 | output += f'{indent}- {error}\n' 198 | output += '\n' 199 | 200 | # Format a dict 201 | elif isinstance(data, dict): 202 | for key, value in data.items(): 203 | output += f'{indent}- {key}:\n' 204 | if isinstance(value, str): 205 | output = f'{output[:-1]} {self.format(value)}' 206 | elif key == 'result': 207 | for k, v in value.items(): 208 | output += f'{indent} - {k}:\n' 209 | if k == 'built_query': 210 | output += self.built_query_sep + '\n' 211 | output += YAML().dump(v) + '\n' 212 | output += self.built_query_sep + '\n' 213 | elif k == 'diff': 214 | output += self.diff(v) 215 | elif k in ['main', 'count']: 216 | output += self.fresult(v, f'{indent} ') 217 | elif k == 'graph': 218 | output = f'{output[:-1]} {self.format(v)}' 219 | else: 220 | output += f'{self.format(value, f"{indent} ")}' 221 | 222 | # Format a Result 223 | elif isinstance(data, Result): 224 | output = self.fresult(data) 225 | 226 | # Format a list 227 | elif isinstance(data, list): 228 | for e in data: 229 | output += f'{indent}- {e}\n' 230 | 231 | # Format a string 232 | elif isinstance(data, str): 233 | if indent: 234 | indent = f'{indent} ' 235 | output = f'{indent}{data}\n' 236 | 237 | return output 238 | 239 | 240 | def fresult(self, result, indent = ''): 241 | if self.output_format == 'csv': 242 | output = CSV().dump(result.data) 243 | elif self.output_format == 'json': 244 | output = JSON().dump(result.data) 245 | elif self.output_format == 'yaml': 246 | output = YAML().dump(result.data) 247 | else: 248 | output = self.tablify(result.data, indent) 249 | output += '\n' 250 | if result.time: 251 | output += f'{indent}- execution time: {result.time}\n' 252 | return output 253 | 254 | 255 | def tablify(self, result, indent = ''): 256 | table = '' 257 | if result: 258 | key = result[0].keys() 259 | record = [list(row.values()) for row in result] 260 | tlp = PrettyTable() 261 | tlp.field_names = key 262 | for row in record: 263 | tlp.add_row(row) 264 | tlp.align = 'l' 265 | for line in tlp.get_string().split('\n'): 266 | table += f'{indent}{line}\n' 267 | return table[:-1] 268 | -------------------------------------------------------------------------------- /modules/controller.py: -------------------------------------------------------------------------------- 1 | from conf.constants import shell 2 | from modules.error import Error 3 | from modules.io import CSV, File, JSON, Neo4j, YAML 4 | from modules.model import ActiveDirectory, Path, Query, Result 5 | from modules.view import Terminal as vTerminal 6 | from os import path, walk 7 | from prompt_toolkit import PromptSession 8 | from prompt_toolkit.auto_suggest import AutoSuggestFromHistory 9 | from prompt_toolkit.completion import NestedCompleter 10 | from prompt_toolkit.history import FileHistory 11 | from prompt_toolkit.lexers import PygmentsLexer 12 | from prompt_toolkit.styles import Style 13 | from pygments.lexers.graph import CypherLexer 14 | from sys import exit as sexit, stdin 15 | 16 | 17 | class Controller: 18 | 19 | 20 | def __init__(self, args): 21 | 22 | self.ql = [] 23 | self.error = Error() 24 | 25 | if args.op_mode in ['audit', 'path']: 26 | self.op_mode = args.query_mode 27 | else: 28 | self.op_mode = args.op_mode 29 | 30 | self.persistence = None 31 | 32 | 33 | def add(self, data, op_mode): 34 | q = Query(op_mode, data) 35 | if q.error: 36 | self.viewer.print(q) 37 | else: 38 | self.ql.append(q) 39 | 40 | 41 | def check(self): 42 | for query in self.ql: 43 | query.diff() 44 | 45 | self.viewer.print(query) 46 | 47 | if not query.error and self.persistence: 48 | self.dump(query) 49 | self.error_exists(self.persistence.error) 50 | 51 | 52 | def clean(self): 53 | self.ql.clear() 54 | 55 | 56 | def connect(self, args): 57 | 58 | # Neo4j 59 | self.neo = Neo4j( 60 | args.neo4j_host, 61 | args.neo4j_port, 62 | args.neo4j_username, 63 | args.neo4j_password, 64 | args.neo4j_encrypted 65 | ) 66 | self.error_exists(self.neo.error) 67 | 68 | 69 | def dump(self, query = None): 70 | if query: 71 | for rtype in query.result: 72 | if rtype != 'graph': 73 | result = query.result[rtype] 74 | name, _ = path.splitext(path.basename(query.filename)) 75 | filename = f'{name}-{rtype}' 76 | if rtype in ['main', 'count']: 77 | result = result.data 78 | elif rtype in ['built_query', 'diff']: 79 | if rtype == 'built_query': 80 | if not query.result['diff']: 81 | break 82 | elif not query.result[rtype]: 83 | break 84 | self.persistence.dump(result, filename) 85 | 86 | else: 87 | for q in self.ql: 88 | self.dump(q) 89 | 90 | 91 | def error_exists(self, error, _exit = True): 92 | if error: 93 | self.viewer.print(error) 94 | if _exit: 95 | sexit() 96 | return True 97 | return False 98 | 99 | 100 | def execute(self, query = None): 101 | if query: 102 | # Execute query 103 | query.compile(self.ad, self.path) 104 | 105 | if not query.error: 106 | for stype, cstatement in query.cstatement.items(): 107 | if stype != 'graph': 108 | result = Result() 109 | self.neo.execute(cstatement) 110 | if self.neo.error: 111 | query.error.add(self.neo.error.get()) 112 | break 113 | result.add(self.neo.result) 114 | query.result[stype] = result 115 | # Iteration order: main, graph and count 116 | if stype == 'main' and not self.neo.result: 117 | break 118 | else: 119 | query.result[stype] = cstatement 120 | else: 121 | # Execute each query 122 | for q in self.ql: 123 | if q.state == 'enabled': 124 | try: 125 | self.execute(q) 126 | self.viewer.print(q) 127 | if not q.error and self.persistence: 128 | self.dump(q) 129 | self.error_exists(self.persistence.error) 130 | except KeyboardInterrupt: 131 | # ctrl+c 132 | continue 133 | except EOFError: 134 | # ctrl+d 135 | self.viewer.print() 136 | break 137 | 138 | 139 | def load(self, data, op_mode): 140 | # Dictionary 141 | if isinstance(data, dict): 142 | self.add(data, op_mode) 143 | 144 | # Is a file 145 | elif path.isfile(data): 146 | filename = data 147 | _, extension = path.splitext(filename) 148 | if op_mode == 'raw': 149 | statements = File().read(filename) 150 | if File.error: 151 | self.error.add(File.error.get()) 152 | else: 153 | for statement in statements: 154 | data = { 155 | 'filename': filename, 156 | 'statement': { 157 | 'main': statement 158 | } 159 | } 160 | self.add(data, op_mode) 161 | elif extension == '.yaml': 162 | data = YAML().read(filename) 163 | if YAML.error: 164 | self.error.add(YAML.error.get()) 165 | else: 166 | data['filename'] = filename 167 | self.add(data, op_mode) 168 | 169 | # Is a directory 170 | else: 171 | directory = data 172 | for root, _, files in sorted(walk(directory)): 173 | if root[-1] == '/': 174 | root = root[:-1] 175 | for filename in sorted(files): 176 | self.load(f'{root}/{filename}', self.op_mode) 177 | 178 | 179 | class Terminal(Controller): 180 | 181 | 182 | def __init__(self, args): 183 | Controller.__init__(self, args) 184 | 185 | # Terminal viewer 186 | self.viewer = vTerminal(args.verbose_mode, self.op_mode) 187 | 188 | # Connect to Neo4j database 189 | if self.op_mode != 'check': 190 | self.connect(args) 191 | 192 | # Execute/Shell mode 193 | if self.op_mode in ['execute', 'shell']: 194 | self.ad = ActiveDirectory() 195 | self.error_exists(self.ad.error) 196 | self.path = Path() 197 | self.shell = Shell(self) 198 | # Execute mode 199 | if self.op_mode == 'execute': 200 | self.viewer.presenter.output_format = 'json' 201 | if args.command == '-': args.command = stdin.read() 202 | self.shell.execute(args.command) 203 | # Shell mode 204 | elif self.op_mode == 'shell': 205 | self.viewer.presenter.output_format = 'table' 206 | self.shell.interactive() 207 | 208 | # Audit/Check/Path mode (input from files) 209 | else: 210 | 211 | # Persistence 212 | if args.persistence_format == 'none': 213 | self.persistence = None 214 | else: 215 | if args.persistence_format == 'csv': 216 | persistence = CSV 217 | elif args.persistence_format == 'json': 218 | persistence = JSON 219 | elif args.persistence_format == 'yaml': 220 | persistence = YAML 221 | self.persistence = persistence(args.output_directory) 222 | self.error_exists(self.persistence.error) 223 | 224 | # Load query list 225 | self.load(args.input_directory_or_file, self.op_mode) 226 | self.error_exists(self.error, False) 227 | 228 | # Check mode 229 | if self.op_mode == 'check': 230 | self.check() 231 | 232 | # Audit/Path mode: test, raw or default (complete) 233 | else: 234 | self.ad = ActiveDirectory(args.ad_domain, args.ad_lang) 235 | self.error_exists(self.ad.error) 236 | if 'start_node' in vars(args): 237 | self.path = Path(args.start_node, args.end_node, args.has_session) 238 | else: 239 | self.path = Path() 240 | self.viewer.presenter.output_format = args.output_format 241 | 242 | self.execute() 243 | 244 | 245 | class Shell: 246 | 247 | 248 | def __init__(self, terminal): 249 | 250 | self.terminal = terminal 251 | 252 | self.commands = { 253 | 'clean': { 254 | 'func': self.clean, 255 | 'help': shell['help']['clean'] 256 | }, 257 | 'exit': { 258 | 'func': self.exit, 259 | 'help': shell['help']['exit'] 260 | }, 261 | 'help': { 262 | 'func': self.help, 263 | 'help': shell['help']['help'] 264 | }, 265 | 'match': { 266 | 'func': self.match 267 | }, 268 | 'set': { 269 | 'func': self.set, 270 | 'help': shell['help']['set'] 271 | } 272 | } 273 | 274 | self.query = { 275 | 'filename': 'Interactive', 276 | 'statement': { 277 | 'main': None 278 | } 279 | } 280 | 281 | 282 | def clean(self, _): 283 | self.match("match (n) remove n:Base".split()) 284 | 285 | 286 | def execute(self, command): 287 | if self.terminal.op_mode != 'shell': 288 | self.commands.pop('clean') 289 | self.commands.pop('exit') 290 | self.commands.pop('help') 291 | 292 | for c in command.split(';'): 293 | c = c.strip() 294 | if c: 295 | c = c.split() 296 | c[0] = c[0].lower() 297 | if c[0] in self.commands: 298 | self.commands[c[0]]['func'](c) 299 | 300 | def exit(self, _): 301 | pass 302 | 303 | 304 | def get_input(self): 305 | 306 | multiline = bool(self.multiline == 'true') 307 | 308 | completer = NestedCompleter.from_nested_dict(self.menu) 309 | 310 | # ff0000 = red color 311 | style = Style.from_dict({'prompt': '#ff0000'}) 312 | prompt = [('class:prompt', '> ')] 313 | 314 | session = PromptSession(history = FileHistory(self.history_file)) 315 | return session.prompt( 316 | prompt, 317 | auto_suggest = AutoSuggestFromHistory(), 318 | complete_in_thread = True, 319 | completer = completer, 320 | lexer = PygmentsLexer(CypherLexer), 321 | multiline = multiline, 322 | style = style, 323 | vi_mode = True 324 | ) 325 | 326 | 327 | def help(self, _): 328 | self.terminal.viewer.print('Commands:') 329 | for command in self.commands: 330 | if 'help' in self.commands[command]: 331 | h = self.commands[command]['help'] 332 | self.terminal.viewer.print(f'- {command}: {h}') 333 | 334 | self.terminal.viewer.print('\nMatch Query:') 335 | self.terminal.viewer.print(f' {shell["help"]["query"]}') 336 | self.terminal.viewer.print() 337 | 338 | 339 | def interactive(self): 340 | 341 | self.load() 342 | 343 | command = '' 344 | 345 | while command not in ['e', 'exit', 'q', 'quit']: 346 | try: 347 | self.execute(command) 348 | command = self.get_input().strip() 349 | except KeyboardInterrupt: 350 | # ctrl+c 351 | command = '' 352 | continue 353 | except EOFError: 354 | # ctrl+d 355 | break 356 | 357 | 358 | def load(self): 359 | 360 | self.menu = shell['menu'] 361 | self.history_file = shell['history'] 362 | self.multiline = 'false' 363 | self.terminal.viewer.presenter.output_format = 'table' 364 | 365 | 366 | def match(self, command): 367 | statement = ' '.join(command) 368 | self.query['statement']['main'] = statement 369 | self.terminal.load(self.query, 'raw') 370 | self.terminal.error_exists(self.terminal.error, False) 371 | self.terminal.execute() 372 | self.terminal.clean() 373 | self.terminal.viewer.print() 374 | 375 | 376 | def set(self, command): 377 | 378 | if len(command) == 1: 379 | for k, v in self.terminal.ad.get().items(): 380 | self.terminal.viewer.print(f'{k} = {v}') 381 | self.terminal.viewer.print(f'multiline = {self.multiline}') 382 | output_format = self.terminal.viewer.presenter.output_format 383 | self.terminal.viewer.print(f'output = {output_format}') 384 | self.terminal.viewer.print() 385 | 386 | elif len(command) == 3: 387 | 388 | if command[1] in ['domain', 'lang']: 389 | self.terminal.ad.set(command[1], command[2]) 390 | 391 | elif command[1] == 'multiline': 392 | if command[2] in shell['menu']['set']['multiline']: 393 | self.multiline = command[2] 394 | 395 | elif command[1] == 'output': 396 | if command[2] in shell['menu']['set']['output']: 397 | self.terminal.viewer.presenter.output_format = command[2] 398 | -------------------------------------------------------------------------------- /modules/model.py: -------------------------------------------------------------------------------- 1 | from conf.constants import error 2 | from copy import deepcopy 3 | from datetime import timedelta 4 | from modules.error import Error 5 | from modules.io import YAML 6 | from re import sub 7 | from time import time 8 | 9 | 10 | class ActiveDirectory(): 11 | 12 | 13 | def __init__(self, domain = 'RASTREATOR.LOCAL', lang = 'en'): 14 | self.error = Error() 15 | self.load() 16 | self.set('domain', domain) 17 | self.set('lang', lang) 18 | 19 | 20 | def get(self, attribute = None): 21 | if attribute: 22 | if attribute == 'lang_vars': 23 | return self.languages[self.lang] 24 | elif attribute in self.__dict__: 25 | return self.__dict__[attribute] 26 | else: 27 | return None 28 | else: 29 | attributes = deepcopy(self.__dict__) 30 | attributes.pop('error') 31 | attributes.pop('languages') 32 | return attributes 33 | 34 | 35 | def load(self): 36 | self.languages = YAML().read('conf/languages.yaml') 37 | if YAML.error: 38 | self.error.add(YAML.error.get()) 39 | 40 | 41 | def set(self, key, value): 42 | if key in ['domain', 'lang']: 43 | self.__dict__[key] = value 44 | 45 | 46 | class Path(): 47 | 48 | 49 | def __init__(self, start = '', end = '', has_session = 'false'): 50 | self.error = Error() 51 | self.set('start', start) 52 | self.set('end', end) 53 | self.set('has_session', has_session) 54 | 55 | 56 | def get(self, attribute = None): 57 | if attribute: 58 | if attribute in self.__dict__: 59 | return self.__dict__[attribute] 60 | else: 61 | return None 62 | else: 63 | attributes = deepcopy(self.__dict__) 64 | attributes.pop('error') 65 | return attributes 66 | 67 | 68 | def set(self, key, value): 69 | if key in ['start', 'end']: 70 | node_type = node_name = '' 71 | if value: 72 | kv = value.split(':') 73 | if len(kv) > 1: 74 | node_type = f':{kv[0]}' 75 | node_name = kv[1] 76 | else: 77 | node_name = kv[0] 78 | node_name = f'{{name:"{node_name}"}}' 79 | self.__dict__[f'{key}_type'] = node_type 80 | self.__dict__[f'{key}_name'] = node_name 81 | elif key in ['has_session']: 82 | if value == 'false': 83 | self.__dict__['has_session'] = '' 84 | else: 85 | self.__dict__['has_session'] = '|HasSession' 86 | 87 | 88 | class BaseQuery(): 89 | 90 | 91 | def __init__(self, data): 92 | 93 | self.attribute = { 94 | 'core': {}, 95 | 'optional': {} 96 | } 97 | self.error = Error() 98 | self.state = 'enabled' 99 | 100 | if data: 101 | self.normalize(data) 102 | self.check_data(self.__dict__, data, self.attribute) 103 | 104 | 105 | def check_data(self, internal, data, check): 106 | if 'core' in check: 107 | for ckey, cvalue in check['core'].items(): 108 | if ckey in data: 109 | self.check_key_value(internal, data, ckey, cvalue) 110 | else: 111 | self.error.add(error['parse']['key'].format(key = ckey)) 112 | if 'optional' in check: 113 | for key, _ in data.items(): 114 | if not ('core' in check and key in check['core']): 115 | if key in check['optional']: 116 | ovalue = check['optional'][key] 117 | self.check_key_value(internal, data, key, ovalue) 118 | 119 | 120 | def check_key_value(self, internal, data, key, value): 121 | if isinstance(value, dict): 122 | if key in data: 123 | if isinstance(data[key], dict): 124 | internal[key] = {} 125 | self.check_data(internal[key], data[key], value) 126 | if not internal[key]: 127 | internal.pop(key) 128 | else: 129 | self.error.add(error['parse']['value'].format( 130 | value = data[key], 131 | key = key 132 | )) 133 | else: 134 | self.error.add(error['parse']['key'].format(key = key)) 135 | else: 136 | if isinstance(value, list): 137 | if key in data: 138 | if data[key]: 139 | if value: 140 | if data[key] not in value: 141 | self.error.add(error['parse']['value'].format( 142 | value = data[key], 143 | key = key 144 | )) 145 | return 146 | else: 147 | if not isinstance(data[key], list): 148 | self.error.add(error['parse']['object'].format( 149 | bad = type(data[key]).__name__, 150 | good = 'list', 151 | key = key 152 | )) 153 | return 154 | 155 | else: 156 | self.error.add(error['parse']['value'].format( 157 | value = data[key], 158 | key = key 159 | )) 160 | return 161 | else: 162 | self.error.add(error['parse']['key'].format(key = key)) 163 | return 164 | internal[key] = data[key] 165 | 166 | 167 | def get(self, attribute, exclude = True): 168 | sd = self.__dict__ 169 | qa = deepcopy(sd) 170 | 171 | # Remove internal attributes 172 | qa.pop('attribute', None) 173 | 174 | # Get attributes 175 | for a in sd: 176 | if a in attribute: 177 | # Value != None 178 | if attribute[a]: 179 | for sa in sd[a]: 180 | if sa in attribute[a]: 181 | if exclude: 182 | qa[a].pop(sa, None) 183 | else: 184 | if not exclude: 185 | qa[a].pop(sa, None) 186 | elif exclude: 187 | qa.pop(a, None) 188 | elif not exclude: 189 | qa.pop(a, None) 190 | 191 | # Sort attributes 192 | attributes = \ 193 | list(self.attribute['core'].keys()) + \ 194 | list(self.attribute['optional'].keys()) + \ 195 | ['cstatement', 'result', 'error'] 196 | 197 | return dict(sorted(qa.items(), key = lambda x: attributes.index(x[0]))) 198 | 199 | 200 | def normalize(self, data): 201 | for key in list(data): 202 | value = data[key] 203 | if isinstance(value, dict): 204 | value = data.pop(key) 205 | key = key.lower() 206 | data[key] = value 207 | self.normalize(value) 208 | else: 209 | value = data.pop(key) 210 | key = key.lower() 211 | if key in ['tactic', 'tag']: 212 | value = value.lower() 213 | data[key] = value 214 | 215 | 216 | def update(self, key, value): 217 | value.update(self.attribute[key]) 218 | self.attribute[key] = value 219 | 220 | 221 | class RawQuery(BaseQuery): 222 | 223 | 224 | def __init__(self, data): 225 | 226 | # Parent init 227 | BaseQuery.__init__(self, data) 228 | 229 | if not self.error: 230 | # Core attributes definition 231 | self.update('core', { 232 | 'filename': None, 233 | 'statement': { 234 | 'core': { 235 | 'main': None 236 | }, 237 | 'optional': { 238 | 'count': [], 239 | 'graph': [] 240 | } 241 | } 242 | }) 243 | 244 | # Set data 245 | if data: 246 | self.check_data(self.__dict__, data, self.attribute) 247 | 248 | # Execution result 249 | self.result = {} 250 | 251 | 252 | def compile(self, ad, path): 253 | 254 | self.cstatement = {} 255 | 256 | define = self.preprocess(ad, path) 257 | 258 | if 'main' in self.statement: 259 | self.cstatement['main'] = self.statement['main'] 260 | for e in define: 261 | pattern = e['pattern'] 262 | replace = e['replace'] 263 | try: 264 | self.cstatement['main'] = sub( 265 | pattern, 266 | replace, 267 | self.cstatement['main'] 268 | ) 269 | except Exception as e: 270 | self.error.add(error['compile']['pattern'].format( 271 | e = e, 272 | pattern = pattern, 273 | type = 'main' 274 | )) 275 | 276 | for stype in ['graph', 'count']: 277 | if stype in self.statement: 278 | self.cstatement[stype] = self.cstatement['main'] 279 | for regex in self.statement[stype]: 280 | for e in define: 281 | pattern = e['pattern'] 282 | replace = e['replace'] 283 | try: 284 | regex = sub(pattern, replace, regex) 285 | except Exception as e: 286 | self.error.add( 287 | error['compile']['pattern'].format( 288 | e = e, 289 | pattern = pattern, 290 | type = stype 291 | ) 292 | ) 293 | field = regex.split('/') 294 | try: 295 | self.cstatement[stype] = sub( 296 | field[0], 297 | field[1], 298 | self.cstatement[stype] 299 | ) 300 | except Exception as e: 301 | self.error.add(error['compile']['pattern'].format( 302 | e = e, 303 | pattern = field[0], 304 | type = stype 305 | )) 306 | 307 | 308 | def preprocess(self, ad, path): 309 | define = [] 310 | 311 | # AD domain 312 | define.append({ 313 | 'pattern': 'RAS-DOMAIN', 314 | 'replace': ad.get('domain') 315 | }) 316 | 317 | # AD language 318 | for key, value in ad.get('lang_vars').items(): 319 | define.append({ 320 | 'pattern': key, 321 | 'replace': value 322 | }) 323 | 324 | # Path from type 325 | define.append({ 326 | 'pattern': ':RAS-START_NODE_TYPE', 327 | 'replace': path.get('start_type') 328 | }) 329 | 330 | # Path from name 331 | define.append({ 332 | 'pattern': '{RAS-START_NODE_NAME}', 333 | 'replace': path.get('start_name') 334 | }) 335 | 336 | # Path to type 337 | define.append({ 338 | 'pattern': ':RAS-END_NODE_TYPE', 339 | 'replace': path.get('end_type') 340 | }) 341 | 342 | # Path to name 343 | define.append({ 344 | 'pattern': '{RAS-END_NODE_NAME}', 345 | 'replace': path.get('end_name') 346 | }) 347 | 348 | # Path has session 349 | define.append({ 350 | 'pattern': '\|RAS-HAS_SESSION', 351 | 'replace': path.get('has_session') 352 | }) 353 | 354 | return define 355 | 356 | 357 | class TestQuery(RawQuery): 358 | 359 | 360 | def __init__(self, data): 361 | 362 | # Parent init 363 | RawQuery.__init__(self, data) 364 | 365 | if not self.error: 366 | # Core attributes definition 367 | self.update('core', { 368 | 'filename': None, 369 | 'name': None 370 | }) 371 | 372 | # Set data 373 | if data: 374 | self.check_data(self.__dict__, data, self.attribute) 375 | 376 | 377 | class DefaultQuery(TestQuery): 378 | 379 | 380 | def __init__(self, data): 381 | 382 | # Parent init 383 | TestQuery.__init__(self, data) 384 | 385 | if not self.error: 386 | # Core/optional attributes definition 387 | self.update('core', { 388 | 'filename': None, 389 | 'author': None, 390 | 'name': None, 391 | 'state': None, 392 | 'tactic': [ 393 | 'collection', 394 | 'command and control', 395 | 'credential access', 396 | 'defense evasion', 397 | 'discovery', 398 | 'execution', 399 | 'exfiltration', 400 | 'impact', 401 | 'initial access', 402 | 'lateral movement', 403 | 'persistence', 404 | 'privilege escalation' 405 | ], 406 | 'tag': ['analysis', 'attack', 'issue'], 407 | 'description': None 408 | }) 409 | 410 | # Optional attributes definition 411 | self.update('optional', { 412 | 'reference': [], 413 | 'nextsteps': { 414 | 'optional': { 415 | 'rt': [], 416 | 'bt': [] 417 | } 418 | } 419 | }) 420 | 421 | # Set data 422 | if data: 423 | self.check_data(self.__dict__, data, self.attribute) 424 | 425 | 426 | class CheckQuery(DefaultQuery): 427 | 428 | 429 | def __init__(self, data): 430 | 431 | # Parent init 432 | DefaultQuery.__init__(self, deepcopy(data)) 433 | 434 | if not self.error: 435 | data.pop('filename', None) 436 | self.disk_query = data 437 | 438 | 439 | def diff(self): 440 | # Transform Dump to string to compare. 441 | 442 | ex = { 443 | 'filename': None, 444 | 'cstatement': None, 445 | 'result': None, 446 | 'disk_query': None 447 | } 448 | 449 | 450 | query = self.get(ex) 451 | 452 | query.pop('error') 453 | 454 | diff = [] 455 | 456 | count_lines = 1 457 | 458 | disk_query_lines = YAML().dump(self.disk_query).splitlines() 459 | query_lines = YAML().dump(query).splitlines() 460 | 461 | 462 | for disk_lines, q_lines in zip(disk_query_lines, query_lines): 463 | if disk_lines != q_lines: 464 | diff.append({ 465 | 'line': count_lines, 466 | '<': disk_lines, 467 | '>': q_lines 468 | }) 469 | count_lines += 1 470 | 471 | if len(disk_query_lines) > len(query_lines): 472 | for lines in disk_query_lines[len(query_lines):]: 473 | diff.append({ 474 | 'line': count_lines, 475 | '<': lines, 476 | '>': '' 477 | }) 478 | count_lines += 1 479 | else: 480 | for lines in query_lines[len(disk_query_lines):]: 481 | diff.append({ 482 | 'line': count_lines, 483 | '<': '', 484 | '>': lines 485 | }) 486 | count_lines += 1 487 | 488 | self.result = { 489 | 'built_query': query, 490 | 'diff': diff 491 | } 492 | 493 | 494 | class Query: 495 | 496 | 497 | def __new__(cls, op_mode, data): 498 | if op_mode == 'raw': 499 | return RawQuery(data) 500 | if op_mode == 'test': 501 | return TestQuery(data) 502 | if op_mode == 'check': 503 | return CheckQuery(data) 504 | # Default Query 505 | return DefaultQuery(data) 506 | 507 | 508 | class Result(): 509 | 510 | 511 | def __init__(self): 512 | self.data = None 513 | self.time = time() 514 | 515 | 516 | def add(self, data): 517 | self.data = data 518 | self.time = timedelta(seconds = time() - self.time) 519 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](images/logo.png) 2 | 3 | 4 | #### > Tool with a collection of query files to explore Microsoft Active Directory 5 | 6 | 7 | ## Contents 8 | 9 | 1. [What is rastreator?](#1-what-is-rastreator) 10 | 2. [What does rastreator solve?](#2-what-does-rastreator-solve) 11 | 3. [Interesting features](#3-interesting-features) 12 | 4. [Goals](#4-goals) 13 | 5. [Collection of query files](#5-collection-of-query-files) 14 | 6. [Query file](#6-query-file) 15 | 7. [Tool](#7-tool) 16 | 1. [Audit mode](#71-audit-mode) 17 | 2. [Check mode](#72-check-mode) 18 | 3. [Execute mode](#73-execute-mode) 19 | 4. [Path mode](#74-path-mode) 20 | 5. [Shell mode](#75-shell-mode) 21 | 8. [Installation](#8-installation) 22 | 1. [Dependencies](#81-dependencies) 23 | 2. [Using Git](#82-using-git) 24 | 3. [Using Docker](#83-using-docker) 25 | 9. [FAQ](#9-faq) 26 | 10. [Similar projects](#10-similar-projects) 27 | 28 | 29 | ## 1. What is rastreator? 30 | 31 | Rastreator is a tool with a collection of query files to obtain information, suggest potential attacks and discover issues in a Microsoft Active Directory domain.
32 | Rastreator requires that: 33 | - SharpHound gather Active Directory domain information, 34 | - BloodHound parse the gathered information and fill a Neo4j database. 35 | 36 | After that, rastreator depends on: 37 | - Neo4j to store the information and execute Cypher queries to obtain interesting information or issues. 38 | 39 | The collection of query files, the core of this project, is grouped by tactics (Mitre ATT&CK) and permissions.
40 | We encourage everyone to share with us their Cypher statements or query files to improve the collection and the community knowledge. 41 | 42 | The tool is a python script (rastreator.py) that executes queries and obtains results.
43 | It provides different: 44 | - Operation modes to work in background (audit, path), interactively (shell) or programmatically (execute). 45 | - Output formats to analyse the results on screen or save them to disk. 46 | 47 | 48 | ## 2. What does rastreator solve? 49 | 50 | [BloodHound](https://github.com/BloodHoundAD/BloodHound) is a great exploration tool and has some awesome queries, like "Shortest path to Domain Admins", but in general, it has three main drawbacks: 51 | - It is not easy to develop and test new queries with it and you end up directly using Neo4j's browser interface for this goal, 52 | - It provides a Linkurious GUI to interact manually but sometimes you need raw data instead of cool graphs and a way to automate a bunch of queries. That's why projects like [CypherDog](https://github.com/SadProcessor/CypherDog) arose, 53 | - It does not have a great collection of queries. 54 | 55 | Rastreator solves all of them: 56 | - It has a rich shell mode, to develop and test new queries, 57 | - It provides raw data in different formats (CSV, JSON or YAML), 58 | - It has a good collection of queries to assist Red/Blue Teamers, Pentesters and Auditors, 59 | - It has three operation modes (audit, path and execute) to automate the discovery process in a bunch of queries. 60 | 61 | 62 | ## 3. Interesting features 63 | 64 | - Different operation modes: audit, check, execute, path, and shell. 65 | - Different query sub-modes (raw, test and default) that require different internal structure and metadata fields in query files, for those of you more interested in executing Cypher statements than documenting them. 66 | - Metadata for query files, beyond name and description, like for example: author, state, tactic, tag, external references and next steps for Red/Blue Teams. 67 | - Cypher statements in query files that allow placeholder variables to support different domain names, Active Directory languages and starting/ending nodes. 68 | - Different screen output formats: CSV, JSON, table and YAML. 69 | - Different persistence formats: CSV, JSON and YAML. 70 | 71 | 72 | ## 4. Goals 73 | 74 | The main goal is to improve the collection of query files. To achieve it we set the following sub-goals: 75 | - Share and centralize query files. 76 | - Research and create new query files. 77 | - Recollect and format, dispersed Cypher statements on the Internet, in query files. 78 | - Promote community collaboration. 79 | 80 | Other goals are: 81 | - Remove dependency on BlooHound to parse and fill the Neo4j database. 82 | - Develop a custom ingestor. 83 | - Add Azure query files. 84 | 85 | 86 | ## 5. Collection of query files 87 | 88 | The core of this project is a collection of query files under the queries/ directory: 89 | - queries/ 90 | - tactics/ 91 | - collection/ 92 | - credential_access/ 93 | - discovery/ 94 | - execution/ 95 | - lateral\_movement/ 96 | - persistence/ 97 | - privilege\_escalation/ 98 | - permissions/ 99 | 100 | The tactics/ directory contains query files categorized by the tactics defined in the Mitre ATT&CK Framework. Executing statements in one of these categories provides results to achieve or detect that tactical goal in a domain.
101 | The permissions/ directory contains query files to detect interesting control permissions of a given start/end node.
102 | We encourage everyone to participate and share their Cypher statements or query files with us to improve the collection of query files. 103 | 104 | 105 | ## 6. Query file 106 | 107 | A query file has a different internal structure depending on the targeted query sub-mode (raw, test, default): 108 | 109 | - raw: the query file is a regular text file with one or more Cypher statements, one per line. 110 | - test: the query file is a YAML file that contains required (name and statement-main) and optional (statement-count, statement-graph) metadata. 111 | - default: the query file is a YAML file that contains required and optional metadata. 112 | 113 | Next, we will describe the required and optional metadata for a valid query file, required by default in audit/pat modes and candidate to be added to the collection of query files. 114 | 115 | Metadata fields: 116 | 117 | - author (required): Query author name. 118 | - name (required): Query name using underscores instead of spaces. 119 | - state (required): Only enabled queries will be executed. Valid values (choose one): 120 | - enabled 121 | - disabled 122 | - tactic (required): Query tactical goal. Also needed to compute statistics. Valid values (choose one): 123 | - collection 124 | - command and control 125 | - credential access 126 | - defense evasion 127 | - discovery 128 | - execution 129 | - exfiltration 130 | - impact 131 | - initial access 132 | - lateral movement 133 | - persistence 134 | - privilege escalation 135 | - tag (required): Another way to classify queries. Also needed to compute statistics. Valid values (choose one): 136 | - analysis: queries provide results to analyse. 137 | - attack: queries provide results with information to perform an attack as the next step. 138 | - issue: queries provide results pointing to a vulnerability or an incorrect configuration, but also can be used to perform an attack as the next step. 139 | - description (required): Summary of the query purpose. 140 | - reference (optional): List of external URLs with information related to the query. 141 | - nextsteps (optional): List of recommended next steps or tasks for a Red and Blue Team. 142 | - statement (required): 143 | - main (required): The main Cypher statement of this query file. Results are given as text and can be printed on screen or saved to disk. 144 | - count (optional): A regular expression that converts the previous main statement into a new one that provides a summary or statistics. Results are given as text and can be printed on screen or saved to disk. 145 | - graph (optional): A regular expression that converts the previous main statement into a new one that provides a graphical representation. Currently, this query is not executed and the result is a Cypher statement to be copy-pasted and executed in the Neo4j's browser interface. 146 | 147 | 148 | ## 7. Tool 149 | 150 | The tool is a python script (rastreator.py) that executes queries and obtains results. 151 | It provides different: 152 | - Operation modes to work in background (audit, path), interactively (shell) or programmatically (execute). 153 | - Output formats to analyse the results on screen or save them to disk. 154 | 155 | ``` 156 | RastreatorTeam@localhost$ python3 rastreator.py -h 157 | usage: rastreator.py [-h] {audit,check,execute,path,shell} ... 158 | 159 | Rastreator 160 | > Tool with a collection of query files to explore Microsoft Active Directory 161 | > Developed by @interh4ck and @t0-n1 162 | 163 | positional arguments: 164 | {audit,check,execute,path,shell} 165 | audit Audit mode 166 | check Check mode 167 | execute Execute mode 168 | path Path mode 169 | shell Shell mode 170 | 171 | optional arguments: 172 | -h, --help show this help message and exit 173 | ``` 174 | 175 | Positional arguments: 176 | - audit: This mode executes in batch mode one or more query files. 177 | - check: This mode checks the correctness of one or more query files. 178 | - execute: This mode executes one Cypher statement passed as a one-liner. 179 | - path: Same as audit mode, but also allows you to specify the source and end nodes. 180 | - shell: This mode provides a REPL shell with autocompletion support from where you can execute multiple Cypher statements in a single session. 181 | 182 | 183 | ### 7.1. Audit mode 184 | 185 | This mode finds issues and general information. It runs one or more query files in batch mode. It's possible to execute query files without all the required metadata fields using sub-modes (raw or test). 186 | 187 | ``` 188 | RastreatorTeam@localhost$ python3 rastreator.py audit -h 189 | usage: rastreator.py audit [-h] [-v {quiet,default,debug}] [-H NEO4J_HOST] [-P NEO4J_PORT] 190 | [-u NEO4J_USERNAME] [-p NEO4J_PASSWORD] [-e {false,true}] -I 191 | INPUT_DIRECTORY_OR_FILE [-O OUTPUT_DIRECTORY] 192 | [-o {csv,json,none,yaml}] [-f {csv,json,table,yaml}] [-l {en,es}] 193 | -d AD_DOMAIN [-m {raw,test,default}] 194 | 195 | optional arguments: 196 | -h, --help show this help message and exit 197 | -v {quiet,default,debug} 198 | Verbose mode 199 | -H NEO4J_HOST Neo4j host to connect 200 | -P NEO4J_PORT Neo4j port to connect 201 | -u NEO4J_USERNAME Neo4j username 202 | -p NEO4J_PASSWORD Neo4j password 203 | -e {false,true} Neo4j encrypted communication 204 | -I INPUT_DIRECTORY_OR_FILE 205 | Input directory or specific query file 206 | -O OUTPUT_DIRECTORY Output directory to save results 207 | -o {csv,json,none,yaml} 208 | File format to save executed query results 209 | -f {csv,json,table,yaml} 210 | Output format to show executed query results on screen 211 | -l {en,es} Active Directory language 212 | -d AD_DOMAIN Active Directory domain name 213 | -m {raw,test,default} 214 | Query submode 215 | ``` 216 | 217 | Optional arguments: 218 | 219 | - -v {quiet,default,debug}: Verbosity level for screen output. Default: default. 220 | - -H NEO4J\_HOST: IP address or hostname of your Neo4j database. Default: localhost. 221 | - -P NEO4J\_PORT: Port number of your Neo4j database. Default: 7687. 222 | - -u NEO4J\_USERNAME: The username to login in your Neo4j database. Default: neo4j. 223 | - -p NEO4J\_PASSWORD: The password to login in your Neo4j database. Default: neo4j. 224 | - -e {false,true}: Select 'true' if communication to your Neo4j database is encrypted, elsewhere select 'false'. Default: true. 225 | - -I INPUT\_DIRECTORY\_OR\_FILE: Input directory with query files or a specific query file to execute. 226 | - -O OUTPUT\_DIRECTORY: Output directory to save the new generated query files. Default: output. 227 | - -o {csv,json,none,yaml}: Select 'csv', 'json' or 'yaml' to save to disk the query results in CSV, JSON or YAML format. Select 'none' to do not save results to disk. Default: csv. 228 | - -m {raw,test,default}: Select 'raw' to use query files without metadata, only Cypher statements one per line. Select 'test' to use query files with a minimal metadata (name and statement-main are required). Finally, select 'default' to use query files with a complete format. Default: default. 229 | - -f {csv,json,table,yaml}: Select 'csv', 'json', 'table' or 'yaml' to output the query results to screen in CSV, JSON or YAML format. Select 'none' to do not output results to screen. Default: table. 230 | - -l {en,es}: Select 'en' or 'es' to use English or Spanish as the Active Directory language. To add more languages, please refer to the [FAQ](#9-faq) section. Default: en. 231 | - -d AD_DOMAIN: Active Directory domain name. 232 | 233 | 234 | ### 7.2. Check mode 235 | 236 | This mode checks the correctness of one or more query files. We suggest to execute this mode before doing a pull request to share your query files with us. 237 | 238 | ``` 239 | RastreatorTeam@localhost$ python3 rastreator.py check -h 240 | usage: rastreator.py check [-h] [-v {quiet,default,debug}] [-I INPUT_DIRECTORY_OR_FILE] 241 | [-O OUTPUT_DIRECTORY] [-o {none,yaml}] 242 | 243 | optional arguments: 244 | -h, --help show this help message and exit 245 | -v {quiet,default,debug} 246 | Verbose mode 247 | -I INPUT_DIRECTORY_OR_FILE 248 | Input directory or specific query file 249 | -O OUTPUT_DIRECTORY Output directory to save results 250 | -o {none,yaml} File format to save executed query results 251 | ``` 252 | 253 | Optional arguments: 254 | 255 | - -v {quiet,default,debug}: Verbosity level for screen output. Default: default. 256 | - -I INPUT\_DIRECTORY\_OR\_FILE: Input directory with query files or a specific query file to check. Default: queries. 257 | - -O OUTPUT\_DIRECTORY: Output directory to save the new generated query files. Default: output. 258 | - -o {none,yaml}: Select 'yaml' to save to disk the new generated query files in YAML format. Select 'none' to do not save anything. Default: yaml. 259 | 260 | 261 | ### 7.3. Execute mode 262 | 263 | This mode executes a Cypher statement passed as a one-liner. It facilitates programmatic integration with other tools. 264 | 265 | ``` 266 | RastreatorTeam@localhost$ python3 rastreator.py execute -h 267 | usage: rastreator.py execute [-h] [-v {quiet,default,debug}] [-H NEO4J_HOST] 268 | [-P NEO4J_PORT] [-u NEO4J_USERNAME] [-p NEO4J_PASSWORD] 269 | [-e {false,true}] [-c COMMAND] 270 | 271 | optional arguments: 272 | -h, --help show this help message and exit 273 | -v {quiet,default,debug} 274 | Verbose mode 275 | -H NEO4J_HOST Neo4j host to connect 276 | -P NEO4J_PORT Neo4j port to connect 277 | -u NEO4J_USERNAME Neo4j username 278 | -p NEO4J_PASSWORD Neo4j password 279 | -e {false,true} Neo4j encrypted communication 280 | -c COMMAND Semicolon separated commands inside single/double quotes 281 | ``` 282 | 283 | Optional arguments: 284 | 285 | - -v {quiet,default,debug}: Verbosity level for screen output. Default: default. 286 | - -H NEO4J\_HOST: IP address or hostname of your Neo4j database. Default: localhost. 287 | - -P NEO4J\_PORT: Port number of your Neo4j database. Default: 7687. 288 | - -u NEO4J\_USERNAME: The username to login in your Neo4j database. Default: neo4j. 289 | - -p NEO4J\_PASSWORD: The password to login in your Neo4j database. Default: neo4j. 290 | - -e {false,true}: Select 'true' if communication to your Neo4j database is encrypted, elsewhere select 'false'. Default: true. 291 | - -c COMMAND: List of internal shell commands to execute separated by semicolons. 292 | 293 | 294 | ### 7.4. Path mode 295 | 296 | This mode finds permissions between start and end nodes. Useful when you want to know what the new compromised user can do. It runs one or more query files in batch mode. It's possible to execute query files without all the required metadata fields using sub-modes (raw or test). 297 | 298 | ``` 299 | RastreatorTeam@localhost$ python3 rastreator.py path -h 300 | usage: rastreator.py path [-h] [-v {quiet,default,debug}] [-H NEO4J_HOST] [-P NEO4J_PORT] 301 | [-u NEO4J_USERNAME] [-p NEO4J_PASSWORD] [-e {false,true}] -I 302 | INPUT_DIRECTORY_OR_FILE [-O OUTPUT_DIRECTORY] 303 | [-o {csv,json,none,yaml}] [-f {csv,json,table,yaml}] [-l {en,es}] 304 | -d AD_DOMAIN [-m {raw,test,default}] [-S START_NODE] [-E END_NODE] 305 | [-s {false,true}] 306 | 307 | optional arguments: 308 | -h, --help show this help message and exit 309 | -v {quiet,default,debug} 310 | Verbose mode 311 | -H NEO4J_HOST Neo4j host to connect 312 | -P NEO4J_PORT Neo4j port to connect 313 | -u NEO4J_USERNAME Neo4j username 314 | -p NEO4J_PASSWORD Neo4j password 315 | -e {false,true} Neo4j encrypted communication 316 | -I INPUT_DIRECTORY_OR_FILE 317 | Input directory or specific query file 318 | -O OUTPUT_DIRECTORY Output directory to save results 319 | -o {csv,json,none,yaml} 320 | File format to save executed query results 321 | -f {csv,json,table,yaml} 322 | Output format to show executed query results on screen 323 | -l {en,es} Active Directory language 324 | -d AD_DOMAIN Active Directory domain name 325 | -m {raw,test,default} 326 | Query submode 327 | -S START_NODE Start node of the path 328 | -E END_NODE End node of the path 329 | -s {false,true} Accept paths with HasSession 330 | ``` 331 | 332 | Optional arguments: 333 | 334 | - -v {quiet,default,debug}: Verbosity level for screen output. Default: default. 335 | - -H NEO4J\_HOST: IP address or hostname of your Neo4j database. Default: localhost. 336 | - -P NEO4J\_PORT: Port number of your Neo4j database. Default: 7687. 337 | - -u NEO4J\_USERNAME: The username to login in your Neo4j database. Default: neo4j. 338 | - -p NEO4J\_PASSWORD: The password to login in your Neo4j database. Default: neo4j. 339 | - -e {false,true}: Select 'true' if communication to your Neo4j database is encrypted, elsewhere select 'false'. Default: true. 340 | - -I INPUT\_DIRECTORY\_OR\_FILE: Input directory with query files or a specific query file to execute. 341 | - -O OUTPUT\_DIRECTORY: Output directory to save the new generated query files. Default: output. 342 | - -o {csv,json,none,yaml}: Select 'csv', 'json' or 'yaml' to save to disk the query results in CSV, JSON or YAML format. Select 'none' to do not save results to disk. Default: csv. 343 | - -m {raw,test,default}: Select 'raw' to use query files without metadata, only Cypher statements one per line. Select 'test' to use query files with a minimal metadata (name and statement-main are required). Finally, select 'default' to use query files with a complete format. Default: default. 344 | - -f {csv,json,table,yaml}: Select 'csv', 'json', 'table' or 'yaml' to output the query results to screen in CSV, JSON or YAML format. Select 'none' to do not output results to screen. Default: table. 345 | - -l {en,es}: Select 'en' or 'es' to use English or Spanish as the Active Directory language. To add more languages, please refer to the [FAQ](#9-faq) section. Default: en. 346 | - -d AD_DOMAIN: Active Directory domain name. 347 | - -S START_NODE: Specify the start node (NODE_TYPE:NODE_NAME). Default: ''. 348 | - -E END_NODE: Specify the end node (NODE_TYPE:NODE_NAME). Default: ''. 349 | - -s {false,true}: Select 'true' if results may contain HasSession edges, elsewhere select 'false'. Default: false. 350 | 351 | 352 | ### 7.5. Shell mode 353 | 354 | This mode provides a REPL shell with autocomplete support and allows the execution of multiple Cypher statements in a single session. The best way to develop and test new Cypher statements. 355 | 356 | ``` 357 | RastreatorTeam@localhost$ python3 rastreator.py shell -h 358 | usage: rastreator.py shell [-h] [-v {quiet,default,debug}] [-H NEO4J_HOST] 359 | [-P NEO4J_PORT] [-u NEO4J_USERNAME] [-p NEO4J_PASSWORD] 360 | [-e {false,true}] 361 | 362 | optional arguments: 363 | -h, --help show this help message and exit 364 | -v {quiet,default,debug} 365 | Verbose mode 366 | -H NEO4J_HOST Neo4j host to connect 367 | -P NEO4J_PORT Neo4j port to connect 368 | -u NEO4J_USERNAME Neo4j username 369 | -p NEO4J_PASSWORD Neo4j password 370 | -e {false,true} Neo4j encrypted communication 371 | ``` 372 | 373 | Optional arguments: 374 | 375 | - -v {quiet,default,debug}: Verbosity level for screen output. Default: default. 376 | - -H NEO4J\_HOST: IP address or hostname of your Neo4j database. Default: localhost. 377 | - -P NEO4J\_PORT: Port number of your Neo4j database. Default: 7687. 378 | - -u NEO4J\_USERNAME: The username to login in your Neo4j database. Default: neo4j. 379 | - -p NEO4J\_PASSWORD: The password to login in your Neo4j database. Default: neo4j. 380 | - -e {false,true}: Select 'true' if communication to your Neo4j database is encrypted, elsewhere select 'false'. Default: true. 381 | 382 | ``` 383 | RastreatorTeam@localhost$ python3 rastreator.py shell 384 | Rastreator 385 | > Tool with a collection of query files to explore Microsoft Active Directory 386 | > Developed by @interh4ck and @t0-n1 387 | 388 | > help 389 | Commands: 390 | - clean: base nodes 391 | - exit: this program (ctrl+d) 392 | - help: shows this help 393 | - set: the environment variables 394 | 395 | Match Query: 396 | Example: > match (u:User{enabled:true}) return u.name limit 10 397 | 398 | > set 399 | domain = RASTREATOR.LOCAL 400 | lang = en 401 | multiline = false 402 | output = table 403 | 404 | > set output csv 405 | json 406 | table 407 | yaml 408 | ``` 409 | 410 | Commands: 411 | 412 | - clean: Remove 'Base' nodes from Neo4j database. 413 | - set: Shows the environment variables. 414 | - set domain AD_DOMAIN: Set the Active Directory domain name. 415 | - set lang {en,es}: Select 'en' or 'es' to use English or Spanish as the Active Directory language. To add more languages, please refer to the [FAQ](#9-faq) section. Default: en. 416 | - set multiline {false,true}: Select 'true' to write and edit a Cypher statement in multiple lines, elsewhere select 'false'. Default: false. 417 | - set output {csv,json,table,yaml}: Select 'csv', 'json', 'table' or 'yaml' to output the Cypher statement results to screen in CSV, JSON or YAML format. Default: table. 418 | 419 | 420 | ## 8. Installation 421 | 422 | 423 | ### 8.1. Dependencies 424 | 425 | You need to install first: 426 | 427 | - [BloodHound](https://bloodhound.readthedocs.io/en/latest/index.html) 428 | 429 | After that continue with the installation [Using Git](#82-using-git) or [Using Docker](#83-using-docker). 430 | 431 | 432 | ### 8.2. Using Git 433 | 434 | ``` 435 | $ git clone https://github.com/RastreatorTeam/rastreator.git 436 | $ python3 -m venv rastreator 437 | $ source rastreator/bin/activate 438 | (rastreator) $ cd rastreator 439 | (rastreator) $ pip3 install -r requirements.txt 440 | (rastreator) $ python3 rastreator.py -h 441 | ``` 442 | 443 | 444 | ### 8.3. Using Docker 445 | 446 | ``` 447 | $ git clone https://github.com/RastreatorTeam/rastreator.git 448 | $ sudo docker build -t rastreatorteam/rastreator . 449 | $ sudo docker run --rm -it rastreatorteam/rastreator -h 450 | ``` 451 | 452 | Use the -v option to mount another query directory into the container: 453 | 454 | ``` 455 | $ sudo docker run --rm -it -v {host_directory}:{container_mount_point} rastreatorteam/rastreator -h 456 | ``` 457 | 458 | 459 | ## 9. FAQ 460 | 461 | 462 | #### How can I share my query file with you? 463 | 464 | Check you query file using the check mode.
465 | Get the new generated file and make a pull request. 466 | 467 | 468 | #### Could you support more Active Directory languages? 469 | 470 | Sure, check the conf/languages.yaml file and update it with the variables for you language.
471 | After that, please make a pull request. 472 | 473 | 474 | #### How can I persistently set my defaults? 475 | 476 | Edit the conf/defaults.yaml file. 477 | 478 | 479 | #### How can I remove 'Base' nodes from Neo4j database? 480 | 481 | Base nodes were added for deduplication purposes (https://github.com/BloodHoundAD/BloodHound/issues/352), but you can remove them by running the following Cypher statement: 482 | ``` 483 | match (n) remove n:Base 484 | ``` 485 | or by entering the shell mode to run the clean command: 486 | ``` 487 | > clean 488 | ``` 489 | 490 | 491 | ## 10. Similar projects 492 | 493 | During our private development, we observed the emergence of the following similar projects: 494 | 495 | - [PlumHound](https://github.com/DefensiveOrigins/PlumHound) 496 | - [BloodHound Notebook](https://github.com/OTRF/bloodhound-notebook) 497 | --------------------------------------------------------------------------------