├── .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 | 
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 |
--------------------------------------------------------------------------------