def _graph_shodan(self): """Convert the Shodan tables with ports added as Neo4j graph nodes linked to hosts.""" self.c.execute("SELECT ip_address,port,banner_data,os,organization FROM shodan_host_lookup") all_shodan_lookup = self.c.fetchall() with click.progressbar(all_shodan_lookup, label="Creating Port nodes", length=len(all_shodan_lookup)) as bar: for row in bar: query = """ MATCH (a:IP {Address:'%s'}) CREATE UNIQUE (b:Port {Number:'%s', OS:'%s', Organization:"%s", Hostname:''})<-[r:HAS_PORT]-(a) SET a.Organization = "%s" RETURN a,b """ % (row[0],row[1],row[3],row[4],row[4]) helpers.execute_query(self.neo4j_driver,query) self.c.execute("SELECT domain,ip_address,port,banner_data,os,hostname FROM shodan_search") all_shodan_search = self.c.fetchall() with click.progressbar(all_shodan_search, label="Creating Port and IP relationships", length=len(all_shodan_search)) as bar: for row in bar: query = """ MATCH (a:Port)<-[:HAS_PORT]-(b:IP {Address:'%s'}) SET a.Hostname = "%s" RETURN a """ % (row[1],row[5]) helpers.execute_query(self.neo4j_driver,query)
def _update_dns(self): """Update domain nodes with DNS information.""" self.c.execute( "SELECT domain,ns_record,a_record,mx_record,txt_record,soa_record,dmarc,vulnerable_cache_snooping FROM dns" ) dns_data = self.c.fetchall() with click.progressbar(dns_data, label="Updating Domain nodes with DNS info", length=len(dns_data)) as bar: for row in bar: query = """ MATCH (a:Domain {Name:"%s"}) SET a += {NameServers:"%s", Address:"%s", MXRecords:'%s', TXTRecords:'%s', SOARecords:'%s', DMARC:'%s'} RETURN a """ % (row[0], row[1], row[2], row[3], row[4], row[5], row[6]) helpers.execute_query(self.neo4j_driver, query) for address in row[2].split(","): query = """ MATCH (a:Domain {Name:'%s'}) MATCH (b:IP {Address:'%s'}) CREATE UNIQUE (a)-[r:RESOLVES_TO]->(b) RETURN a,r,b """ % (row[0], address) helpers.execute_query(self.neo4j_driver, query)
def _graph_shodan(self): """Convert the Shodan tables with ports added as Neo4j graph nodes linked to hosts.""" self.c.execute( "SELECT ip_address,port,banner_data,os,organization FROM shodan_host_lookup" ) all_shodan_lookup = self.c.fetchall() for row in all_shodan_lookup: query = """ MATCH (a:IP {Address:'%s'}) CREATE UNIQUE (b:Port {Number:'%s', OS:'%s', Organization:"%s", Hostname:''})<-[r:HAS_PORT]-(a) SET a.Organization = "%s" RETURN a,b """ % (row[0], row[1], row[3], row[4], row[4]) helpers.execute_query(self.neo4j_driver, query) self.c.execute( "SELECT domain,ip_address,port,banner_data,os,hostname FROM shodan_search" ) all_shodan_search = self.c.fetchall() for row in all_shodan_search: query = """ MATCH (a:Port)<-[:HAS_PORT]-(b:IP {Address:'%s'}) SET a.Hostname = "%s" RETURN a """ % (row[1], row[5]) helpers.execute_query(self.neo4j_driver, query)
def _update_rdap(self): """Update host nodes with RDAP information.""" self.c.execute("SELECT ip_address,rdap_source,organization,network_cidr,asn,country_code,robtex_related_domains FROM rdap_data") all_rdap = self.c.fetchall() with click.progressbar(all_rdap, label="Updating IP nodes with RDAP info", length=len(all_rdap)) as bar: for row in bar: query = """ MATCH (a:IP {Address:'%s'}) SET a += {RDAPSource:'%s', Organization:"%s", CIDR:'%s', ASN:'%s', CountryCode:'%s', RelatedDomains:'%s'} RETURN a """ % (row[0],row[1],row[2],row[3],row[4],row[5],row[6]) helpers.execute_query(self.neo4j_driver,query)
def _update_whois(self): """Update domain nodes with WHOIS information.""" self.c.execute("SELECT domain,registrar,expiration,organization,registrant,admin_contact,tech_contact,address,dns_sec FROM whois_data") all_whois = self.c.fetchall() with click.progressbar(all_whois, label="Updating Domain nodes with WHOIS info", length=len(all_whois)) as bar: for row in bar: query = """ MATCH (a:Domain {Name:'%s'}) SET a += {Registrar:"%s", Expiration:'%s', Organization:"%s", Registrant:"%s", Admin:"%s", Tech:"%s", ContactAddress:"%s", DNSSEC:'%s'} RETURN a """ % (row[0],row[1],row[2],row[3],row[4],row[5],row[6],row[7],row[8]) helpers.execute_query(self.neo4j_driver,query)
def _update_rdap(self): """Update host nodes with RDAP information.""" self.c.execute( "SELECT ip_address,rdap_source,organization,network_cidr,asn,country_code,robtex_related_domains FROM rdap_data" ) all_rdap = self.c.fetchall() for row in all_rdap: query = """ MATCH (a:IP {Address:'%s'}) SET a += {RDAPSource:'%s', Organization:"%s", CIDR:'%s', ASN:'%s', CountryCode:'%s', RelatedDomains:'%s'} RETURN a """ % (row[0], row[1], row[2], row[3], row[4], row[5], row[6]) helpers.execute_query(self.neo4j_driver, query)
def _update_whois(self): """Update domain nodes with whois information.""" self.c.execute( "SELECT domain,registrar,expiration,organization,registrant,admin_contact,tech_contact,address,dns_sec FROM whois_data" ) all_whois = self.c.fetchall() for row in all_whois: query = """ MATCH (a:Domain {Name:'%s'}) SET a += {Registrar:"%s", Expiration:'%s', Organization:"%s", Registrant:"%s", Admin:"%s", Tech:"%s", ContactAddress:"%s", DNSSEC:'%s'} RETURN a """ % (row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8]) helpers.execute_query(self.neo4j_driver, query)
def get_all_domains(self, inclusive=False): """Fetch and return distinct domains from the BloodHound data set for which there is data. If a True value is provided for inclusive then all domains are pulled from the dataset for comparison purposes. """ # We fetch groups and then get the domain property for the group. We do this instead # of MATCHing on domains, or even users, because pulling domains from those objects # may lead to failed queries later due to not enough data about the additional domains. # BloodHound may have users from additional domains via foreign group membership, which # adds those domains to Domains while no other data about that domain is available. # Include ALL domains regardless of info available -- useful for comparisons if inclusive: query = """ MATCH (d:Domain) RETURN DISTINCT d.name """ # Get only domains for which we have data else: query = """ MATCH (g:Group) RETURN DISTINCT g.domain """ results = helpers.execute_query(self.neo4j_driver, query) domains = [] for record in results: domains.append(record[0]) return domains
def _graph_subdomains(self): """Convert the subdomains table into Neo4j graph nodes with relationships to the domain and other subdomain nodes. """ self.c.execute( "SELECT domain,subdomain,ip_address,domain_frontable FROM subdomains" ) all_subdomains = self.c.fetchall() with click.progressbar(all_subdomains, label="Creating Subdomain nodes", length=len(all_subdomains)) as bar: # Enforce unique nodes for subdomains query = """ CREATE CONSTRAINT ON (a:Subdomain) ASSERT a.Name IS UNIQUE """ helpers.execute_query(self.neo4j_driver, query) # Loop over each subdomain to create nodes for row in bar: # Start with the full domain info and then split it apart # If we have a subdomain of a subdomain we want to create that relationship base_domain = row[0] subdomain = row[1] partial_subdomain = '.'.join(subdomain.split('.')[1:]) ip_address = row[2] domain_frontable = row[3] # Create the subdomain node query = """ MERGE (x:Subdomain {Name:'%s'}) ON CREATE SET x.Address = "%s", x.DomainFrontable = '%s' ON MATCH SET x.Address = "%s", x.DomainFrontable = '%s' """ % (subdomain, ip_address, domain_frontable, ip_address, domain_frontable) helpers.execute_query(self.neo4j_driver, query) # Check if the partial subdomain is the base domain if partial_subdomain == base_domain: query = """ MATCH (b:Domain {Name:'%s'}) MERGE (a:Subdomain {Name:'%s'}) ON CREATE SET a.Address = "%s", a.DomainFrontable = '%s' MERGE (c:IP {Address:"%s"}) MERGE (c)<-[r1:RESOLVES_TO]-(a)<-[r2:HAS_SUBDOMAIN]-(b) RETURN a,b,c """ % (base_domain, subdomain, ip_address, domain_frontable, ip_address) helpers.execute_query(self.neo4j_driver, query) # If not, the subdomain is a subdomain of another subdomain, so create that relationship else: query = """ MERGE (a:Subdomain {Name:'%s'}) ON CREATE SET a.Address = "%s", a.DomainFrontable = '%s' MERGE (b:Subdomain {Name:'%s'}) MERGE (c:IP {Address:"%s"}) MERGE (c)<-[r1:RESOLVES_TO]-(a)<-[r2:HAS_SUBDOMAIN]-(b) RETURN a,b,c """ % (subdomain, ip_address, domain_frontable, partial_subdomain, ip_address) helpers.execute_query(self.neo4j_driver, query)
def _graph_subdomains(self): """Convert the subdomains table into Neo4j graph nodes with relationships to the domain and other subdomain nodes. """ self.c.execute("SELECT domain,subdomain,ip_address,domain_frontable FROM subdomains") all_subdomains = self.c.fetchall() with click.progressbar(all_subdomains, label="Creating Subdomain nodes", length=len(all_subdomains)) as bar: # Enforce unique nodes for subdomains query = """ CREATE CONSTRAINT ON (a:Subdomain) ASSERT a.Name IS UNIQUE """ helpers.execute_query(self.neo4j_driver,query) # Loop over each subdomain to create nodes for row in bar: # Start with the full domain info and then split it apart # If we have a subdomain of a subdomain we want to create that relationship base_domain = row[0] subdomain = row[1] partial_subdomain = '.'.join(subdomain.split('.')[1:]) ip_address = row[2] domain_frontable = row[3] # Create the subdomain node query = """ MERGE (x:Subdomain {Name:'%s'}) ON CREATE SET x.Address = "%s", x.DomainFrontable = '%s' ON MATCH SET x.Address = "%s", x.DomainFrontable = '%s' """ % (subdomain,ip_address,domain_frontable,ip_address,domain_frontable) helpers.execute_query(self.neo4j_driver,query) # Check if the partial subdomain is the base domain if partial_subdomain == base_domain: query = """ MATCH (b:Domain {Name:'%s'}) MERGE (a:Subdomain {Name:'%s'}) ON CREATE SET a.Address = "%s", a.DomainFrontable = '%s' MERGE (c:IP {Address:"%s"}) MERGE (c)<-[r1:RESOLVES_TO]-(a)<-[r2:HAS_SUBDOMAIN]-(b) RETURN a,b,c """ % (base_domain,subdomain,ip_address,domain_frontable,ip_address) helpers.execute_query(self.neo4j_driver,query) # If not, the subdomain is a subdomain of another subdomain, so create that relationship else: query = """ MERGE (a:Subdomain {Name:'%s'}) ON CREATE SET a.Address = "%s", a.DomainFrontable = '%s' MERGE (b:Subdomain {Name:'%s'}) MERGE (c:IP {Address:"%s"}) MERGE (c)<-[r1:RESOLVES_TO]-(a)<-[r2:HAS_SUBDOMAIN]-(b) RETURN a,b,c """ % (subdomain,ip_address,domain_frontable,partial_subdomain,ip_address) helpers.execute_query(self.neo4j_driver,query)
def avg_path_length(self, domain): """Returns the average number of hops in a path to a Domain Admin in the given domain.""" query = """ MATCH p = shortestPath((n {domain:UPPER('%s')})-[r*1..]->(g:Group {name:'DOMAIN ADMINS@%s'})) RETURN toInt(AVG(LENGTH(p))) as avgPathLength """ % (domain, domain) results = helpers.execute_query(self.neo4j_driver, query) for record in results: return record[0]
def get_total_computers(self, domain): """Returns the total number of computers in the given domain.""" query = """ MATCH (totalComputers:Computer {domain:UPPER('%s')}) RETURN COUNT(DISTINCT(totalComputers)) """ % domain results = helpers.execute_query(self.neo4j_driver, query) for record in results: return record[0]
def get_all_da_paths(self, domain): """Returns the number of paths to a Domain Admin that exist for the given domain.""" query = """ MATCH p = shortestPath((pathToDAUsers:User {domain:UPPER('%s')})-[r*1..]-> (g:Group {name:UPPER('DOMAIN ADMINS@%s')})) RETURN COUNT(DISTINCT(pathToDAUsers)) """ % (domain, domain) results = helpers.execute_query(self.neo4j_driver, query) for record in results: return record[0]
def _graph_certificates(self): """Convert the certificates table into Neo4j graph nodes with relationships to the domain nodes. """ self.c.execute( "SELECT host,subject,issuer,start_date,expiration_date,self_signed,signature_algo,censys_fingerprint,alternate_names FROM certificates" ) all_certificates = self.c.fetchall() for row in all_certificates: if row[5]: self_signed = False else: self_signed = True query = """ CREATE (a:Certificate {Subject:"%s", Issuer:"%s", StartDate:"%s", ExpirationDate:"%s", SelfSigned:"%s", SignatureAlgo:"%s", CensysFingerprint:"%s"}) RETURN a """ % (row[1], row[2], row[3], row[4], self_signed, row[6], row[7]) helpers.execute_query(self.neo4j_driver, query) alt_names = row[8].split(",") for name in alt_names: query = """ MATCH (a:Subdomain {Name:"%s"}) MATCH (b:Certificate {CensysFingerprint:"%s"}) MERGE (a)<-[r:ISSUED_FOR]-(b) """ % (name.strip(), row[7]) helpers.execute_query(self.neo4j_driver, query) query = """ MATCH (a:Domain {Name:"%s"}) MATCH (b:Certificate {CensysFingerprint:"%s"}) MERGE (a)<-[r:ISSUED_FOR]-(b) """ % (name.strip(), row[7]) helpers.execute_query(self.neo4j_driver, query)
def _graph_certificates(self): """Convert the certificates table into Neo4j graph nodes with relationships to the domain nodes. """ self.c.execute("SELECT host,subject,issuer,start_date,expiration_date,self_signed,signature_algo,censys_fingerprint,alternate_names FROM certificates") all_certificates = self.c.fetchall() with click.progressbar(all_certificates, label="Creating Certificate nodes", length=len(all_certificates)) as bar: for row in bar: if row[5]: self_signed = False else: self_signed = True query = """ CREATE (a:Certificate {Subject:"%s", Issuer:"%s", StartDate:"%s", ExpirationDate:"%s", SelfSigned:"%s", SignatureAlgo:"%s", CensysFingerprint:"%s"}) RETURN a """ % (row[1],row[2],row[3],row[4],self_signed,row[6],row[7]) helpers.execute_query(self.neo4j_driver,query) alt_names = row[8].split(",") for name in alt_names: query = """ MERGE (a:Subdomain {Name:"%s"}) MERGE (b:Certificate {CensysFingerprint:"%s"}) MERGE (a)<-[r:ISSUED_FOR]-(b) """ % (name.strip(),row[7]) helpers.execute_query(self.neo4j_driver,query) query = """ MERGE (a:Domain {Name:"%s"}) MERGE (b:Certificate {CensysFingerprint:"%s"}) MERGE (a)<-[r:ISSUED_FOR]-(b) """ % (name.strip(),row[7]) helpers.execute_query(self.neo4j_driver,query)
def _graph_hosts(self): """Convert the hosts table into Neo4j graph nodes.""" self.c.execute("SELECT host_address,in_scope_file,source FROM hosts") all_hosts = self.c.fetchall() for row in all_hosts: if row[1] == 0: scoped = False else: scoped = True if helpers.is_ip(row[0]): query = """ MERGE (x:IP {Address:'%s', Scoped:'%s', Source:'%s'}) RETURN x """ % (row[0], scoped, row[2]) helpers.execute_query(self.neo4j_driver, query) else: query = """ MERGE (x:Domain {Name:'%s', Scoped:'%s', Source:'%s'}) RETURN x """ % (row[0], scoped, row[2]) helpers.execute_query(self.neo4j_driver, query)
def get_admin_groups(self, domain): """Get the Domain Admins, Enterprise Admins, and Administrator group members for the given domain. """ da_query = """ MATCH (n:Group) WHERE n.name =~ 'DOMAIN ADMINS@%s' WITH n MATCH (n)<-[r:MemberOf*1..]-(m) RETURN m.name,r """ % domain ea_query = """ MATCH (n:Group) WHERE n.name =~ 'ENTERPRISE ADMINS@%s' WITH n MATCH (n)<-[r:MemberOf*1..]-(m) RETURN m.name,r """ % domain admin_query = """ MATCH (n:Group) WHERE n.name =~ 'ADMINISTRATORS@%s' WITH n MATCH (n)<-[r:MemberOf*1..]-(m) RETURN m.name,r """ % domain da_results = helpers.execute_query(self.neo4j_driver, da_query) ea_results = helpers.execute_query(self.neo4j_driver, ea_query) admin_results = helpers.execute_query(self.neo4j_driver, admin_query) domain_admins = [] for record in da_results: domain_admins.append(record[0]) enterprise_admins = [] for record in ea_results: enterprise_admins.append(record[0]) admins = [] for record in admin_results: admins.append(record[0]) return domain_admins, enterprise_admins, admins
def _update_dns(self): """Update domain nodes with DNS information.""" self.c.execute("SELECT domain,ns_record,a_record,mx_record,txt_record,soa_record,dmarc,vulnerable_cache_snooping FROM dns") dns_data = self.c.fetchall() with click.progressbar(dns_data, label="Updating Domain nodes with DNS info", length=len(dns_data)) as bar: for row in bar: query = """ MATCH (a:Domain {Name:"%s"}) SET a += {NameServers:"%s", Address:"%s", MXRecords:'%s', TXTRecords:'%s', SOARecords:'%s', DMARC:'%s'} RETURN a """ % (row[0],row[1],row[2],row[3],row[4],row[5],row[6]) helpers.execute_query(self.neo4j_driver,query) for address in row[2].split(","): query = """ MATCH (a:Domain {Name:'%s'}) MATCH (b:IP {Address:'%s'}) CREATE UNIQUE (a)-[r:RESOLVES_TO]->(b) RETURN a,r,b """ % (row[0],address) helpers.execute_query(self.neo4j_driver,query)
def _graph_subdomains(self): """Convert the subdomains table into Neo4j graph nodes with relationships to the domain nodes. """ self.c.execute( "SELECT domain,subdomain,ip_address,domain_frontable FROM subdomains" ) all_subdomains = self.c.fetchall() for row in all_subdomains: query = """ MERGE (x:Subdomain {Name:'%s', Address:"%s", DomainFrontable:'%s'}) """ % (row[1], row[2], row[3]) helpers.execute_query(self.neo4j_driver, query) query = """ MATCH (a:Subdomain {Name:'%s'}) MATCH (b:Domain {Name:'%s'}) MATCH (c:IP {Address:"%s"}) CREATE UNIQUE (c)<-[r1:RESOLVES_TO]-(a)-[r2:SUBDOMAIN_OF]->(b) RETURN a,b,c """ % (row[1], row[0], row[2]) helpers.execute_query(self.neo4j_driver, query)
def find_blocked_inheritance(self, domain): """Finds Active Directory OUs that block inheritance of group policies.""" query = """ MATCH (o:OU {domain:'%s'}) WHERE o.blocksinheritance = True RETURN o.name """ % domain results = helpers.execute_query(self.neo4j_driver, query) blocker_ous = [] for record in results: blocker_ous.append(record[0]) return blocker_ous
def find_unconstrained_delegation(self, domain): """Identifies computers with unconstrained delegation enabled on the given domain.""" query = """ MATCH (c:Computer {domain:'%s'}) WHERE c.unconstraineddelegation = True RETURN c.name """ % domain results = helpers.execute_query(self.neo4j_driver, query) computers = [] for record in results: computers.append(record[0]) return computers
def count_local_admins(self, domain): """Discover the number of local admins for each computer in the domain.""" query = """ MATCH p = (u1:User)-[r:MemberOf|AdminTo*1..]->(c:Computer) RETURN c.name as computerName,COUNT(DISTINCT(u1)) AS adminCount ORDER BY adminCount DESC """ results = helpers.execute_query(self.neo4j_driver, query) admin_count = {} for record in results: admin_count[record[0]] = record[1] return admin_count
def _graph_hosts(self): """Convert the hosts table into Neo4j graph nodes.""" self.c.execute("SELECT host_address,in_scope_file,source FROM hosts") all_hosts = self.c.fetchall() with click.progressbar(all_hosts, label="Creating Domain and IP nodes", length=len(all_hosts)) as bar: for row in bar: if row[1] == 0: scoped = False else: scoped = True if helpers.is_ip(row[0]): query = """ MERGE (x:IP {Address:'%s', Scoped:'%s', Source:'%s'}) RETURN x """ % (row[0], scoped, row[2]) helpers.execute_query(self.neo4j_driver, query) else: query = """ MERGE (x:Domain {Name:'%s', Scoped:'%s', Source:'%s'}) RETURN x """ % (row[0], scoped, row[2]) helpers.execute_query(self.neo4j_driver, query)
def find_da_spn(self, domain): """Identify Domain Admins linked to SPNs.""" query = """ MATCH (u:User {domain:'%s'})-[:MemberOf*1..]->(g:Group {name:'DOMAIN ADMINS@%s'}) WHERE u.hasspn = True RETURN u.name """ % (domain, domain) results = helpers.execute_query(self.neo4j_driver, query) has_spn = [] for record in results: has_spn.append(record[0]) return has_spn
def get_all_gpos(self, domain): """Get the names of all GPOs for the given domain.""" query = """ MATCH (g:GPO {domain:'%s'}) WHERE NOT (g.name is Null or g.name = "") RETURN g.name """ % domain results = helpers.execute_query(self.neo4j_driver, query) gpos = [] for record in results: gpos.append(record[0]) return gpos
def find_remote_desktop_users(self, domain): """Identify members of the Remote Desktop Users.""" query = """ MATCH (n:Group) WHERE n.name = 'REMOTE DESKTOP USERS@%s' WITH n MATCH (n)<-[r:MemberOf*1..]-(m) RETURN m.name """ % domain results = helpers.execute_query(self.neo4j_driver, query) members = [] for member in results: members.append(member[0]) return members
def _graph_hosts(self): """Convert the hosts table into Neo4j graph nodes.""" self.c.execute("SELECT host_address,in_scope_file,source FROM hosts") all_hosts = self.c.fetchall() with click.progressbar(all_hosts, label="Creating Domain and IP nodes", length=len(all_hosts)) as bar: for row in bar: if row[1] == 0: scoped = False else: scoped = True if helpers.is_ip(row[0]): query = """ MERGE (x:IP {Address:'%s', Scoped:'%s', Source:'%s'}) RETURN x """% (row[0],scoped,row[2]) helpers.execute_query(self.neo4j_driver,query) else: query = """ MERGE (x:Domain {Name:'%s', Scoped:'%s', Source:'%s'}) RETURN x """ % (row[0],scoped,row[2]) helpers.execute_query(self.neo4j_driver,query)
def get_operating_systems(self, domain): """Get a list of the opreating systems reported for the given domain's computers.""" query = """ MATCH (c:Computer {domain:'%s'}) WHERE NOT (c.operatingsystem = "" or c.operatingsystem is Null) RETURN DISTINCT(c.operatingsystem) as OperartingSystems,COUNT(c.operatingsystem) as Total ORDER BY Total DESC """ % domain results = helpers.execute_query(self.neo4j_driver, query) operating_systems = {} for record in results: operating_systems[record[0]] = record[1] return operating_systems
def find_foreign_group_membership(self, domain): """Identify users with foregin group memberships.""" query = """ MATCH (n:User) WHERE n.name ENDS WITH ('@' + '%s') WITH n MATCH (n)-[r:MemberOf]->(m:Group) WHERE NOT m.name ENDS WITH ('@' + '%s') RETURN n.name,m.name """ % (domain, domain) results = helpers.execute_query(self.neo4j_driver, query) users = {} for record in results: users[record[0]] = record[1] return users
def find_local_admin_groups(self, domain): """Identify groups that are not built-in Admin groups and have Local Administrator privileges. """ query = """ MATCH (g:Group {domain:'%s'})-[:AdminTo*1..]->(c:Computer) WHERE NOT ('DOMAIN ADMINS@%s' in g.name) AND NOT ('ENTERPRISE ADMINS@%s' in g.name) AND NOT ('ADMINISTRATORS@%s' in g.name) RETURN DISTINCT(g.name) """ % (domain, domain, domain, domain) results = helpers.execute_query(self.neo4j_driver, query) groups = [] for record in results: groups.append(record[0]) return groups
def find_old_pwdlastset(self, domain, months=6): """Find active users with PwdLastSet dates older than the specified number of months.""" months_ago = datetime.today() - timedelta(months*365/12) query = """ MATCH (u:User {domain:'%s'}) RETURN u.name,u.pwdlastset """ % domain results = helpers.execute_query(self.neo4j_driver, query) old_passwords = {} for record in results: timestamp = record[1] if timestamp: pwdlastset = datetime.fromtimestamp(timestamp) if pwdlastset < months_ago: old_passwords[record[0]] = ctime(timestamp) return old_passwords
def get_systems_with_da(self, domain): """Returns a list of computers that are not Domain Controllers and have at least one active session for a Domain Admin user. """ query = """ MATCH (c2:Computer)-[r3:MemberOf*1..]->(g2:Group {name:UPPER('DOMAIN CONTROLLERS@%s')}) WITH COLLECT(c2.name) as domainControllers MATCH (c1:Computer)-[r1:HasSession]->(u1:User)-[r2:MemberOf*1..]->(g1:Group {name:UPPER('DOMAIN ADMINS@%s')}) WHERE NOT (c1.name IN domainControllers) RETURN DISTINCT(c1.name) ORDER BY c1.name ASC """ % (domain, domain) results = helpers.execute_query(self.neo4j_driver, query) computers = [] for record in results: computers.append(record[0]) return computers
def find_admin_groups(self, domain): """Attempt to find interesting groups with ADMIN in their names. The built-in Domain Admins, Enterprise Admins, and Administrator accounts are ignored. """ query = """ MATCH (g:Group {domain:'%s'}) WHERE g.name =~ '(?i).*ADMIN.*' AND NOT ('DOMAIN ADMINS@%s' in g.name) AND NOT ('ENTERPRISE ADMINS@%s' in g.name) AND NOT ('ADMINISTRATORS@%s' in g.name) RETURN g.name """ % (domain, domain, domain, domain) results = helpers.execute_query(self.neo4j_driver, query) groups = [] for record in results: groups.append(record[0]) return groups
def get_total_users(self, domain, enabled=False): """Returns the total number of users in the given domain. All user accounts are returned unless the Enabled flag is set, in which case only accounts with the "Enabled" attribute are returned. """ if enabled: query = """ MATCH (totalUsers:User {domain:UPPER('%s')}) WHERE (totalUsers.enabled = True) RETURN COUNT(DISTINCT(totalUsers)) """ % domain else: query = """ MATCH (totalUsers:User {domain:UPPER('%s')}) RETURN COUNT(DISTINCT(totalUsers)) """ % domain results = helpers.execute_query(self.neo4j_driver, query) for record in results: return record[0]
def _graph_company(self): """Create nodes for the organization names and link them to domains based on whois records and Full Contact API results. """ org_names = [] try: self.c.execute("SELECT organization FROM whois_data") whois_orgs = self.c.fetchall() for org in whois_orgs: org_names.append(org[0]) except: pass try: self.c.execute( "SELECT company_name,website,website_overview,employees,year_founded FROM company_info" ) company_info = self.c.fetchone() org_names.append(company_info[0]) org_names = set(org_names) except: pass if len(org_names) > 0: for org in org_names: query = """ MERGE (x:Organization {Name:"%s"}) RETURN x """ % (org) helpers.execute_query(self.neo4j_driver, query) if company_info: query = """ MATCH (x:Organization {Name:'%s'}) SET x += {Website:'%s', WebsiteOverview:"%s", Employees:'%s', YearFounded:'%s'} RETURN x """ % (company_info[0], company_info[1], company_info[2], company_info[3], company_info[4]) helpers.execute_query(self.neo4j_driver, query) for org in org_names: query = """ MATCH (o:Organization {Name:"%s"}) MATCH (d:Domain) WHERE d.Organization="%s" MERGE (o)-[r:OWNS]->(d) RETURN o,r,d """ % (org, org) helpers.execute_query(self.neo4j_driver, query)
def get_avg_group_membership(self, domain, recursive=False): """Calculate the average number of groups memberships for each user. If the recursive flag is set, this function will unroll group memberships to get the total number of groups. """ if recursive: query = """ MATCH (u:User {domain: UPPER('%s')})-[r:MemberOf*1..]->(g:Group) WITH u.name as userName,COUNT(r) as relCount RETURN AVG(relCount) """ % domain else: query = """ MATCH (u:User {domain: UPPER('%s')})-[r:MemberOf*1]->(g:Group) WITH u.name as userName,COUNT(r) as relCount RETURN AVG(relCount) """ % domain results = helpers.execute_query(self.neo4j_driver, query) for record in results: return record[0]
def find_special_users(self, domain): """Attempt to find user accounts containing common prefixes or suffixes that often denote accounts with administrator privileges. """ # TODO: This seems like it could be more efficient query = """ MATCH (u:User {domain:'%s'}) WHERE u.name STARTS WITH '_' or u.name STARTS WITH '$' or u.name =~ '(?i).*ADMIN_.*' or u.name =~ '(?i).*ADMIN-.*' or u.name =~ '(?i).*_ADMIN.*' or u.name =~ '(?i).*-ADMIN.*' or u.name =~ '(?i).*ADM_.*' or u.name =~ '(?i).*ADM-.*' or u.name =~ '(?i).*_ADM.*' or u.name =~ '(?i).*-ADM.*' or u.name =~ '(?i).*_A.*' or u.name =~ '(?i).*-A.*' or u.name =~ '(?i).*A_.*' or u.name =~ '(?i).*A-.*' RETURN u.name """ % domain results = helpers.execute_query(self.neo4j_driver, query) users = [] for record in results: users.append(record[0]) return users
def clear_neo4j_database(self): """Clear the current Neo4j database by detaching and deleting all nodes.""" query = "MATCH (n) DETACH DELETE n" helpers.execute_query(self.neo4j_driver,query)
def _graph_company(self): """Create nodes for the organization names and link them to domains based on WHOIS records and Full Contact API results. """ org_names = [] try: self.c.execute("SELECT company_name,website,website_overview,employees,year_founded FROM company_info") company_info = self.c.fetchone() org_names.append(company_info[0]) except: pass org_names = set(org_names) if len(org_names) > 0: for org in org_names: query = """ MERGE (x:Organization {Name:"%s"}) RETURN x """ % (org) helpers.execute_query(self.neo4j_driver,query) else: query = """ MERGE (x:Organization {Name:"Target"}) RETURN x """ helpers.execute_query(self.neo4j_driver,query) if company_info: query = """ MATCH (x:Organization {Name:'%s'}) SET x += {Website:'%s', WebsiteOverview:"%s", Employees:'%s', YearFounded:'%s'} RETURN x """% (company_info[0],company_info[1],company_info[2],company_info[3],company_info[4]) helpers.execute_query(self.neo4j_driver,query) for org in org_names: if len(org_names) == 1: # Associate the domain nodes with the organization query = """ MATCH (d:Domain) SET d += {Organization:'%s'} RETURN d """% (org) helpers.execute_query(self.neo4j_driver,query) # Create the relationships between the organization node and the domain nodes query = """ MATCH (o:Organization {Name:"%s"}) MATCH (d:Domain) WHERE d.Organization="%s" MERGE (o)-[r:OWNS]->(d) RETURN o,r,d """% (org,org) helpers.execute_query(self.neo4j_driver,query) else: query = """ MATCH (o:Organization {Name:"%s"}) MATCH (d:Domain) WHERE d.Organization="%s" MERGE (o)-[r:OWNS]->(d) RETURN o,r,d """% (org,org) helpers.execute_query(self.neo4j_driver,query)