diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..c240a53 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/python-3/.devcontainer/base.Dockerfile + +# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster +FROM mcr.microsoft.com/vscode/devcontainers/python:3.10-bullseye + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 + +USER vscode \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..568e2ec --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,59 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/docker-existing-docker-compose +// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml. +{ + "name": "bloodhound-tools", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": "docker-compose.yml", + + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "bh-tools", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/workspace", + + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "bungcip.better-toml" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + + // Uncomment the next line to run commands after the container is created - for example installing curl. + "postCreateCommand": "curl -sSL https://install.python-poetry.org | python3 - && poetry config virtualenvs.create false && poetry install", + + // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "git": "latest" + } +} \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..5a609f3 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.9' + +services: + neo4j: + image: neo4j:latest + environment: + - NEO4J_AUTH=neo4j/bloodhound + expose: + - "7474" + - "7687" + ports: + - "127.0.0.1:7474:7474" + - "127.0.0.1:7687:7687" + volumes: + - neo4j_data:/data + networks: + - default + + bh-tools: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + user: vscode + command: sleep infinity + volumes: + # Update this to wherever you want VS Code to mount the folder of your project + - ..:/workspace + depends_on: + - neo4j + networks: + - default + +volumes: + neo4j_data: + +networks: + default: \ No newline at end of file diff --git a/DBCreator/DBCreator.py.bak b/DBCreator/DBCreator.py.bak deleted file mode 100644 index 6407e7c..0000000 --- a/DBCreator/DBCreator.py.bak +++ /dev/null @@ -1,751 +0,0 @@ -# Requirements - pip install neo4j-driver -# This script is used to create randomized sample databases. -# Commands -# dbconfig - Set the credentials and URL for the database you're connecting too -# connect - Connects to the database using supplied credentials -# setnodes - Set the number of nodes to generate (defaults to 500, this is a safe number!) -# setdomain - Set the domain name -# cleardb - Clears the database and sets the schema properly -# generate - Generates random data in the database -# clear_and_generate - Connects to the database, clears the DB, sets the schema, and generates random data - -from neo4j.v1 import GraphDatabase -import cmd -import os -import sys -import random -import cPickle -import math -import itertools -from collections import defaultdict -import uuid -import time - -class Messages(): - def title(self): - print "================================================================" - print "BloodHound Sample Database Creator" - print "================================================================" - - def input_default(self, prompt, default): - return raw_input("%s [%s] " % (prompt, default)) or default - -class MainMenu(cmd.Cmd): - def __init__(self): - self.m = Messages() - self.url = "bolt://localhost:7687" - self.username = "neo4j" - self.password = "neo4jj" - self.driver = None - self.connected = False - self.num_nodes = 500 - self.domain = "TESTLAB.LOCAL" - self.current_time = int(time.time()) - self.base_sid = "S-1-5-21-883232822-274137685-4173207997" - with open('first.pkl', 'rb') as f: - self.first_names = cPickle.load(f) - - with open('last.pkl', 'rb') as f: - self.last_names = cPickle.load(f) - - cmd.Cmd.__init__(self) - - def cmdloop(self): - while True: - self.m.title() - self.do_help("") - try: - cmd.Cmd.cmdloop(self) - except KeyboardInterrupt: - if self.driver is not None: - self.driver.close() - raise KeyboardInterrupt - - def help_dbconfig(self): - print "Configure database connection parameters" - - def help_connect(self): - print "Test connection to the database and verify credentials" - - def help_setnodes(self): - print "Set base number of nodes to generate (default 500)" - - def help_setdomain(self): - print "Set domain name (default 'TESTLAB.LOCAL')" - - def help_cleardb(self): - print "Clear the database and set constraints" - - def help_generate(self): - print "Generate random data" - - def help_clear_and_generate(self): - print "Connect to the database, clear the db, set the schema, and generate random data" - - def help_exit(self): - print "Exits the database creator" - - def do_dbconfig(self, args): - print "Current Settings:" - print "DB Url: {}".format(self.url) - print "DB Username: {}".format(self.username) - print "DB Password: {}".format(self.password) - print "" - self.url = self.m.input_default("Enter DB URL", self.url) - self.username = self.m.input_default("Enter DB Username", self.username) - self.password = self.m.input_default("Enter DB Password", self.password) - print "" - print "New Settings:" - print "DB Url: {}".format(self.url) - print "DB Username: {}".format(self.username) - print "DB Password: {}".format(self.password) - print "" - print "Testing DB Connection" - self.test_db_conn() - - def do_setnodes(self, args): - passed = args - if passed != "": - try: - self.num_nodes = int(passed) - return - except ValueError: - pass - - self.num_nodes = int(self.m.input_default("Number of nodes of each type to generate", self.num_nodes)) - - def do_setdomain(self, args): - passed = args - if passed != "": - try: - self.domain = passed.upper() - return - except ValueError: - pass - - self.domain = self.m.input_default("Domain", self.domain).upper() - print "" - print "New Settings:" - print "Domain: {}".format(self.domain) - - def do_exit(self, args): - raise KeyboardInterrupt - - def do_connect(self, args): - self.test_db_conn() - - def do_cleardb(self, args): - if not self.connected: - print "Not connected to database. Use connect first" - return - - print "Clearing Database" - d = self.driver - session = d.session() - num = 1 - while num > 0: - result = session.run("MATCH (n) WITH n LIMIT 10000 DETACH DELETE n RETURN count(n)") - for r in result: - num = int(r['count(n)']) - - print "Resetting Schema" - for constraint in session.run("CALL db.constraints"): - session.run("DROP {}".format(constraint['description'])) - - for index in session.run("CALL db.indexes"): - session.run("DROP {}".format(index['description'])) - - session.run("CREATE CONSTRAINT ON (c:User) ASSERT c.name IS UNIQUE") - session.run("CREATE CONSTRAINT ON (c:Group) ASSERT c.name IS UNIQUE") - session.run("CREATE CONSTRAINT ON (c:Computer) ASSERT c.name IS UNIQUE") - session.run("CREATE CONSTRAINT ON (c:Domain) ASSERT c.name IS UNIQUE") - session.run("CREATE CONSTRAINT ON (c:OU) ASSERT c.guid IS UNIQUE") - session.run("CREATE CONSTRAINT ON (c:GPO) ASSERT c.name IS UNIQUE") - - session.close() - - print "DB Cleared and Schema Set" - - def test_db_conn(self): - self.connected = False - if self.driver is not None: - self.driver.close() - try: - self.driver = GraphDatabase.driver(self.url, auth=(self.username,self.password)) - self.connected = True - print "Database Connection Successful!" - except: - self.connected = False - print "Database Connection Failed. Check your settings." - - def do_generate(self, args): - self.generate_data() - - def do_clear_and_generate(self, args): - self.test_db_conn() - self.do_cleardb("a") - self.generate_data() - - def split_seq(self, iterable, size): - it = iter(iterable) - item = list(itertools.islice(it, size)) - while item: - yield item - item = list(itertools.islice(it, size)) - - def generate_timestamp(self): - choice = random.randint(-1,1) - if choice == 1: - variation = random.randint(0,31536000) - return self.current_time - variation - else: - return choice - - - def generate_data(self): - if not self.connected: - print "Not connected to database. Use connect first" - return - - computers = [] - groups = [] - users = [] - gpos = [] - ou_guid_map = {} - - used_states = [] - - states = ["WA","MD","AL","IN","NV","VA","CA","NY","TX","FL"] - partitions = ["IT","HR","MARKETING","OPERATIONS","BIDNESS"] - os_list = ["Windows Server 2003"] * 1 + ["Windows Server 2008"] * 15 + ["Windows 7"] * 35 + ["Windows 10"] * 28 + ["Windows XP"] * 1 + ["Windows Server 2012"] * 8 + ["Windows Server 2008"] * 12 - session = self.driver.session() - - def cn(name): - return f"{name}@{self.domain}" - - def cs(rid): - return f"{self.base_sid}-{sid}" - - def cws(sid): - return f"{self.domain}-{sid}" - - print "Starting data generation with nodes={}".format(self.num_nodes) - - print "Populating Standard Nodes" - base_statement = "MERGE (n:Base {name: $gname}) SET n:Group, n.objectid=$sid" - session.run(f"{base_statement},n.highvalue=true", sid=cs(512), gname=cn("DOMAIN ADMINS")) - session.run(base_statement, sid=cs(515), gname=cn("DOMAIN COMPUTERS")) - session.run(base_statement, gname=cn("DOMAIN USERS"), sid=cs(513)) - session.run(f"{base_statement},n.highvalue=true", gname=cn("DOMAIN CONTROLLERS"), sid=cs(516)) - session.run(f"{base_statement},n.highvalue=true", gname=cn("ENTERPRISE DOMAIN CONTROLLERS"), sid=cws("S-1-5-9")) - session.run(base_statement, gname=cn("ENTERPRISE READ-ONLY DOMAIN CONTROLLERS"), sid=cs(498)) - session.run(f"{base_statement},n.highvalue=true", gname=cn("ADMINISTRATORS"), sid=cs(544)) - session.run(f"{base_statement},n.highvalue=true", gname=cn("ENTERPRISE ADMINS"), sid=cs(519)) - session.run("MERGE (n:Base {name:$domain}) SET n:Domain, n.highvalue=true", domain=self.domain) - ddp = str(uuid.uuid4()) - ddcp = str(uuid.uuid4()) - dcou = str(uuid.uuid4()) - base_statement = "MERGE (n:Base {name:$gpo, objectid:$guid}) SET n:GPO" - session.run(base_statement, gpo=cn("DEFAULT DOMAIN POLICY"), guid=ddp) - session.run(base_statement, gpo=cn("DEFAULT DOMAIN CONTROLLERS POLICY"), guid=ddp) - session.run("MERGE (n:Base {name:$ou, objectid:$guid, blocksInheritance: false}) SET n:OU", ou=cn("DOMAIN CONTROLLERS"), guid=dcou) - - - print "Adding Standard Edges" - - #Default GPOs - gpo_name = "DEFAULT DOMAIN POLICY@{}".format(self.domain) - session.run('MERGE (n:GPO {name:$gpo}) MERGE (m:Domain {name:$domain}) MERGE (n)-[:GpLink {isacl:false, enforced:toBoolean(false)}]->(m)', gpo=gpo_name, domain=self.domain) - session.run('MERGE (n:Domain {name:$domain}) MERGE (m:OU {guid:$guid}) MERGE (n)-[:Contains {isacl:false}]->(m)', domain=self.domain, guid=dcou) - gpo_name = "DEFAULT DOMAIN CONTROLLERS POLICY@{}".format(self.domain) - session.run('MERGE (n:GPO {name:"DEFAULT DOMAIN CONTROLLERS POLICY@$domain"}) MERGE (m:OU {guid:$guid}) MERGE (n)-[:GpLink {isacl:false, enforced:toBoolean(false)}]->(m)', domain=self.domain, guid=dcou) - - #Ent Admins -> Domain Node - group_name = "ENTERPRISE ADMINS@{}".format(self.domain) - session.run( - 'MERGE (n:Domain {name:$domain}) MERGE (m:Group {name:$gname}) MERGE (m)-[:GenericAll {isacl:true}]->(n)', domain=self.domain, gname=group_name) - - #Administrators -> Domain Node - group_name = "ADMINISTRATORS@{}".format(self.domain) - session.run( - 'MERGE (n:Domain {name:$domain}) MERGE (m:Group {name:$gname}) MERGE (m)-[:Owns {isacl:true}]->(n)', domain=self.domain, gname=group_name) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:WriteOwner {isacl:true}]->(n)', domain=self.domain, gname=group_name) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:WriteDacl {isacl:true}]->(n)', domain=self.domain, gname=group_name) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:DCSync {isacl:true}]->(n)', domain=self.domain, gname=group_name) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:GetChanges {isacl:true}]->(n)', domain=self.domain, gname=group_name) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:GetChangesAll {isacl:true}]->(n)', domain=self.domain, gname=group_name) - - #Domain Admins -> Domain Node - group_name = "DOMAIN ADMINS@{}".format(self.domain) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:WriteOwner {isacl:true}]->(n)', domain=self.domain, gname=group_name) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:WriteDacl {isacl:true}]->(n)', domain=self.domain, gname=group_name) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:DCSync {isacl:true}]->(n)', domain=self.domain, gname=group_name) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:GetChanges {isacl:true}]->(n)', domain=self.domain, gname=group_name) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:GetChangesAll {isacl:true}]->(n)', domain=self.domain, gname=group_name) - - #DC Groups -> Domain Node - group_name = "ENTERPRISE DOMAIN CONTROLLERS@{}".format(self.domain) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:GetChanges {isacl:true}]->(n)', domain=self.domain, gname=group_name) - group_name = "ENTERPRISE READ-ONLY DOMAIN CONTROLLERS@{}".format(self.domain) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:GetChanges {isacl:true}]->(n)', domain=self.domain, gname=group_name) - group_name = "DOMAIN CONTROLLERS@{}".format(self.domain) - session.run( - 'MERGE (n:Domain {name:$domain}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:GetChangesAll {isacl:true}]->(n)', domain=self.domain, gname=group_name) - - print "Generating Computer Nodes" - group_name = "DOMAIN COMPUTERS@{}".format(self.domain) - props = [] - ridcount = 1000 - for i in xrange(1,self.num_nodes+1): - comp_name = "COMP{:05d}.{}".format(i, self.domain) - computers.append(comp_name) - os = random.choice(os_list) - enabled = True - props.append({'name':comp_name, 'id': cs(ridcount), 'props':{ - 'operatingsystem':os, - 'enabled':enabled, - }}) - - if (len(props) > 500): - session.run('UNWIND {props} as prop MERGE (n:Base {objectid: prop.id}) SET n += prop.props WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) - props = [] - session.run('UNWIND {props} as prop MERGE (n:Computer {name:prop.name}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) - - print "Creating Domain Controllers" - for state in states: - comp_name = "{}LABDC." + self.domain - comp_name = comp_name.format(state) - group_name = "DOMAIN CONTROLLERS@{}".format(self.domain) - session.run( - 'MERGE (n:Computer {name:{name}}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', name=comp_name, gname=group_name) - ou_name = "DOMAIN CONTROLLERS@{}".format(self.domain) - session.run( - 'MERGE (n:Computer {name:{name}}) WITH n MATCH (m:OU {name:$ou,guid:$dcou}) WITH n,m MERGE (m)-[:Contains]->(n)', name=comp_name, ou=ou_name, dcou=dcou) - group_name = "ENTERPRISE DOMAIN CONTROLLERS@{}".format(self.domain) - session.run( - 'MERGE (n:Computer {name:{name}}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', name=comp_name, gname=group_name) - group_name = "DOMAIN ADMINS@{}".format(self.domain) - session.run( - 'MERGE (n:Computer {name:{name}}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:AdminTo]->(n)', name=comp_name, gname=group_name) - - - used_states = list(set(used_states)) - - print "Generating User Nodes" - current_time = int(time.time()) - group_name = "DOMAIN USERS@{}".format(self.domain) - props = [] - for i in xrange(1, self.num_nodes+1): - first = random.choice(self.first_names) - last = random.choice(self.last_names) - user_name = "{}{}{:05d}@{}".format(first[0], last,i, self.domain).upper() - user_name = user_name.format(first[0], last,i).upper() - users.append(user_name) - dispname = "{} {}".format(first,last) - enabled = True - pwdlastset = self.generate_timestamp() - lastlogon = self.generate_timestamp() - sidint = i + 1000 - objectsid = self.base_sid + "-" + str(sidint) - - props.append({'name':user_name, 'props': { - 'displayname': dispname, - 'enabled': enabled, - 'pwdlastset': pwdlastset, - 'lastlogon': lastlogon, - 'objectsid': objectsid - }}) - if (len(props) > 500): - session.run( - 'UNWIND {props} as prop MERGE (n:User {name:prop.name}) SET n += prop.props WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) - props = [] - - session.run( - 'UNWIND {props} as prop MERGE (n:User {name:prop.name}) SET n += prop.props WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) - - - print "Generating Group Nodes" - weighted_parts = ["IT"] * 7 + ["HR"] * 13 + ["MARKETING"] * 30 + ["OPERATIONS"] * 20 + ["BIDNESS"] * 30 - props = [] - for i in xrange(1, self.num_nodes + 1): - dept = random.choice(weighted_parts) - group_name = "{}{:05d}@{}".format(dept, i, self.domain) - groups.append(group_name) - props.append({'name':group_name}) - if len(props) > 500: - session.run('UNWIND {props} as prop MERGE (n:Group {name:prop.name})', props=props) - props = [] - - session.run( - 'UNWIND {props} as prop MERGE (n:Group {name:prop.name})', props=props) - - print "Adding Domain Admins to Local Admins of Computers" - props = [] - group_name = "DOMAIN ADMINS@{}".format(self.domain) - for comp in computers: - props.append({'name':comp}) - if len(props) > 500: - session.run('UNWIND {props} as prop MERGE (n:Computer {name:prop.name}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:AdminTo]->(n)', props=props, gname=group_name) - props = [] - - session.run( - 'UNWIND {props} as prop MERGE (n:Computer {name:prop.name}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (m)-[:AdminTo]->(n)', props=props, gname=group_name) - - dapctint = random.randint(3,5) - dapct = float(dapctint) / 100 - danum = int(math.ceil(self.num_nodes * dapct)) - danum = min([danum, 30]) - print "Creating {} Domain Admins ({}% of users capped at 30)".format(danum, dapctint) - das = random.sample(users, danum) - group_name = "DOMAIN ADMINS@{}".format(self.domain) - for da in das: - session.run( - 'MERGE (n:User {name:{name}}) WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', name=da, gname=group_name) - - print "Applying random group nesting" - max_nest = int(round(math.log10(self.num_nodes))) - props = [] - for group in groups: - if (random.randrange(0,100) < 10): - num_nest = random.randrange(1, max_nest) - dept = group[0:-19] - dpt_groups = [x for x in groups if dept in x] - if num_nest > len(dpt_groups): - num_nest = random.randrange(1, len(dpt_groups)) - to_nest = random.sample(dpt_groups, num_nest) - for g in to_nest: - if not g == group: - props.append({'a':group,'b':g}) - - if (len(props) > 500): - session.run('UNWIND {props} AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) - props = [] - - session.run('UNWIND {props} AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) - - print "Adding users to groups" - props = [] - a = math.log10(self.num_nodes) - a = math.pow(a,2) - a = math.floor(a) - a = int(a) - num_groups_base = a - variance = int(math.ceil(math.log10(self.num_nodes))) - it_users = [] - - print "Calculated {} groups per user with a variance of - {}".format(num_groups_base,variance*2) - - for user in users: - dept = random.choice(weighted_parts) - if dept == "IT": - it_users.append(user) - possible_groups = [x for x in groups if dept in x] - - sample = num_groups_base + random.randrange(-(variance*2), 0) - if (sample > len(possible_groups)): - sample = int(math.floor(float(len(possible_groups)) / 4)) - - if (sample == 0): - continue - - to_add = random.sample(possible_groups, sample) - - for group in to_add: - props.append({'a':user,'b':group}) - - if len(props) > 500: - session.run('UNWIND {props} AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) - props = [] - - session.run( - 'UNWIND {props} AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) - - it_users = it_users + das - it_users = list(set(it_users)) - - print "Adding local admin rights" - it_groups = [x for x in groups if "IT" in x] - random.shuffle(it_groups) - super_groups = random.sample(it_groups, 4) - super_group_num = int(math.floor(len(computers) * .85)) - - it_groups = [x for x in it_groups if not x in super_groups] - - total_it_groups = len(it_groups) - - dista = int(math.ceil(total_it_groups * .6)) - distb = int(math.ceil(total_it_groups * .3)) - distc = int(math.ceil(total_it_groups * .07)) - distd = int(math.ceil(total_it_groups * .03)) - - distribution_list = [1] * dista + [2] * distb + [10] * distc + [50] * distd - - props = [] - for x in xrange(0, total_it_groups): - g = it_groups[x] - dist = distribution_list[x] - - to_add = random.sample(computers, dist) - for a in to_add: - props.append({'a': g, 'b': a}) - - if len(props) > 500: - session.run('UNWIND {props} AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (n)-[:AdminTo]->(m)', props=props) - props = [] - - for x in super_groups: - for a in random.sample(computers, super_group_num): - props.append({'a': x, 'b': a}) - - if len(props) > 500: - session.run('UNWIND {props} AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (n)-[:AdminTo]->(m)', props=props) - props = [] - - session.run('UNWIND {props} AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (n)-[:AdminTo]->(m)', props=props) - - print "Adding RDP/ExecuteDCOM/AllowedToDelegateTo" - count = int(math.floor(len(computers) * .1)) - props = [] - for i in xrange(0, count): - comp = random.choice(computers) - user = random.choice(it_users) - props.append({'a': user, 'b': comp}) - - - session.run('UNWIND {props} AS prop MERGE (n:User {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:CanRDP]->(m)', props=props) - - props = [] - for i in xrange(0, count): - comp = random.choice(computers) - user = random.choice(it_users) - props.append({'a': user, 'b': comp}) - - - session.run('UNWIND {props} AS prop MERGE (n:User {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:ExecuteDCOM]->(m)', props=props) - - props = [] - for i in xrange(0, count): - comp = random.choice(computers) - user = random.choice(it_groups) - props.append({'a': user, 'b': comp}) - - - session.run('UNWIND {props} AS prop MERGE (n:Group {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:CanRDP]->(m)', props=props) - - props = [] - for i in xrange(0, count): - comp = random.choice(computers) - user = random.choice(it_groups) - props.append({'a': user, 'b': comp}) - - - session.run('UNWIND {props} AS prop MERGE (n:Group {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:ExecuteDCOM]->(m)', props=props) - - props = [] - for i in xrange(0, count): - comp = random.choice(computers) - user = random.choice(it_users) - props.append({'a': user, 'b': comp}) - - - session.run('UNWIND {props} AS prop MERGE (n:User {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:AllowedToDelegate]->(m)', props=props) - - props = [] - for i in xrange(0, count): - comp = random.choice(computers) - user = random.choice(computers) - if (comp == user): - continue - props.append({'a': user, 'b': comp}) - - session.run('UNWIND {props} AS prop MERGE (n:Computer {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:AllowedToDelegate]->(m)', props=props) - - print "Adding sessions" - max_sessions_per_user = int(math.ceil(math.log10(self.num_nodes))) - - props = [] - for user in users: - num_sessions = random.randrange(0, max_sessions_per_user) - if (user in das): - num_sessions = max(num_sessions, 1) - - if num_sessions == 0: - continue - - for c in random.sample(computers, num_sessions): - props.append({'a':user,'b':c}) - - if (len(props) > 500): - session.run('UNWIND {props} AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (m)-[:HasSession]->(n)', props=props) - props = [] - - session.run('UNWIND {props} AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (m)-[:HasSession]->(n)', props=props) - - print "Adding Domain Admin ACEs" - group_name = "DOMAIN ADMINS@{}".format(self.domain) - props = [] - for x in computers: - props.append({'name':x}) - - if len(props) > 500: - session.run('UNWIND {props} as prop MATCH (n:Computer {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) - props = [] - - session.run('UNWIND {props} as prop MATCH (n:Computer {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) - - for x in users: - props.append({'name':x}) - - if len(props) > 500: - session.run('UNWIND {props} as prop MATCH (n:User {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) - props = [] - - session.run('UNWIND {props} as prop MATCH (n:User {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) - - for x in groups: - props.append({'name':x}) - - if len(props) > 500: - session.run('UNWIND {props} as prop MATCH (n:Group {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) - props = [] - - session.run('UNWIND {props} as prop MATCH (n:Group {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) - - print "Creating OUs" - temp_comps = computers - random.shuffle(temp_comps) - split_num = int(math.ceil(self.num_nodes / 10)) - split_comps = list(self.split_seq(temp_comps,split_num)) - props = [] - for i in xrange(0, 10): - state = states[i] - ou_comps = split_comps[i] - ouname = "{}_COMPUTERS@{}".format(state, self.domain) - guid = str(uuid.uuid4()) - ou_guid_map[ouname] = guid - for c in ou_comps: - props.append({'compname':c,'ouguid':guid,'ouname':ouname}) - if len(props) > 500: - session.run('UNWIND {props} as prop MERGE (n:Computer {name:prop.compname}) WITH n,prop MERGE (m:OU {guid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) - props = [] - - session.run('UNWIND {props} as prop MERGE (n:Computer {name:prop.compname}) WITH n,prop MERGE (m:OU {guid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) - - temp_users = users - random.shuffle(temp_users) - split_users = list(self.split_seq(temp_users,split_num)) - props = [] - - for i in xrange(0, 10): - state = states[i] - ou_users = split_users[i] - ouname = "{}_USERS@{}".format(state, self.domain) - guid = str(uuid.uuid4()) - ou_guid_map[ouname] = guid - for c in ou_users: - props.append({'username':c,'ouguid':guid,'ouname':ouname}) - if len(props) > 500: - session.run( - 'UNWIND {props} as prop MERGE (n:User {name:prop.username}) WITH n,prop MERGE (m:OU {guid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) - props = [] - - session.run('UNWIND {props} as prop MERGE (n:User {name:prop.username}) WITH n,prop MERGE (m:OU {guid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) - - props = [] - for x in ou_guid_map.keys(): - guid = ou_guid_map[x] - props.append({'b':guid}) - - session.run('UNWIND {props} as prop MERGE (n:OU {guid:prop.b}) WITH n MERGE (m:Domain {name:$domain}) WITH n,m MERGE (m)-[:Contains]->(n)', props=props, domain=self.domain) - - print "Creating GPOs" - - for i in xrange(1,20): - gpo_name = "GPO_{}@{}".format(i, self.domain) - guid = str(uuid.uuid4()) - session.run("MERGE (n:GPO {name:{gponame}}) SET n.guid=$guid", gponame=gpo_name, guid=guid) - gpos.append(gpo_name) - - ou_names = ou_guid_map.keys() - for g in gpos: - num_links = random.randint(1,3) - linked_ous = random.sample(ou_names,num_links) - for l in linked_ous: - guid = ou_guid_map[l] - session.run("MERGE (n:GPO {name:{gponame}}) WITH n MERGE (m:OU {guid:$guid}) WITH n,m MERGE (n)-[r:GpLink]->(m)", gponame=g, guid=guid) - - num_links = random.randint(1,3) - linked_ous = random.sample(ou_names,num_links) - for l in linked_ous: - guid = ou_guid_map[l] - session.run("MERGE (n:Domain {name:{gponame}}) WITH n MERGE (m:OU {guid:$guid}) WITH n,m MERGE (n)-[r:GpLink]->(m)", gponame=self.domain, guid=guid) - - gpos.append("DEFAULT DOMAIN POLICY@{}".format(self.domain)) - gpos.append("DEFAULT DOMAIN CONTROLLER POLICY@{}".format(self.domain)) - - acl_list = ["GenericAll"] * 10 + ["GenericWrite"] * 15 + ["WriteOwner"] * 15 + ["WriteDacl"] * 15 + ["AddMember"] * 30 + ["ForceChangePassword"] * 15 + ["ReadLAPSPassword"] * 10 - - num_acl_principals = int(round(len(it_groups) * .1)) - print "Adding outbound ACLs to {} objects".format(num_acl_principals) - - acl_groups = random.sample(it_groups, num_acl_principals) - all_principals = it_users + it_groups - props = [] - for i in acl_groups: - ace = random.choice(acl_list) - ace_string = '[r:' + ace + '{isacl:true}]' - if ace == "GenericAll" or ace == 'GenericWrite' or ace == 'WriteOwner' or ace == 'WriteDacl': - p = random.choice(all_principals) - p2 = random.choice(gpos) - session.run('MERGE (n:Group {name:{group}}) MERGE (m {name:{principal}}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p) - session.run('MERGE (n:Group {name:{group}}) MERGE (m:GPO {name:{principal}}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p2) - elif ace == 'AddMember': - p = random.choice(it_groups) - session.run('MERGE (n:Group {name:{group}}) MERGE (m:Group {name:{principal}}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p) - elif ace == 'ReadLAPSPassword': - p = random.choice(all_principals) - targ = random.choice(computers) - session.run('MERGE (n {name:{principal}}) MERGE (m:Computer {name:{target}}) MERGE (n)-[r:ReadLAPSPassword]->(m)', target=targ, principal=p) - else: - p = random.choice(it_users) - session.run('MERGE (n:Group {name:{group}}) MERGE (m:User {name:{principal}}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p) - - print "Marking some users as Kerberoastable" - i = random.randint(10,20) - i = min(i, len(it_users)) - for user in random.sample(it_users,i): - session.run('MATCH (n:User {name:{user}}) SET n.hasspn=true', user=user) - - print "Adding unconstrained delegation to a few computers" - i = random.randint(10,20) - i = min(i, len(computers)) - session.run('MATCH (n:Computer {name:{user}}) SET n.unconstrainteddelegation=true', user=user) - - session.run('MATCH (n:User) SET n.owned=false') - session.run('MATCH (n:Computer) SET n.owned=false') - session.run('MATCH (n) SET n.domain=$domain', domain=self.domain) - - session.close() - - print "Database Generation Finished!" - - - -if __name__ == '__main__': - try: - MainMenu().cmdloop() - except KeyboardInterrupt: - print "Exiting" - sys.exit() diff --git a/DBCreator/README.md b/DBCreator/README.md deleted file mode 100644 index 89334ce..0000000 --- a/DBCreator/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# BloodHound Database Creator - -This python script will generate a randomized data set for testing BloodHound features and analysis. - -## Requirements - -This script requires Python 3.7+, as well as the neo4j-driver. The script will only work with BloodHound 3.0.0 and above. - -The Neo4j Driver can be installed using pip: - -``` -pip install neo4j-driver -``` - -or - -``` -pip install -r requirements.txt -``` - -## Running - -Ensure that all files in this repo are in the same directory. - -``` -python DBCreator.py -``` - -## Commands - -- dbconfig - Set the credentials and URL for the database you're connecting too -- connect - Connects to the database using supplied credentials -- setnodes - Set the number of nodes to generate (defaults to 500, this is a safe number!) -- setdomain - Set the domain name -- cleardb - Clears the database and sets the schema properly -- generate - Generates random data in the database -- clear_and_generate - Connects to the database, clears the DB, sets the schema, and generates random data -- exit - Exits the script diff --git a/DBCreator/requirements.txt b/DBCreator/requirements.txt deleted file mode 100644 index f4595c2..0000000 Binary files a/DBCreator/requirements.txt and /dev/null differ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..217c30f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.10-slim as build-stage + +WORKDIR /tmp/code + +COPY . . + +RUN pip wheel --wheel-dir ./dist . + +FROM python:3.10-slim + +WORKDIR /app + +COPY --from=build-stage /tmp/code/dist . + +RUN pip install --no-cache-dir --no-index --find-links . bloodhound-tools \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..44e16ca --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +.PHONY: tests + +default: build + +clean: + rm -f -r build/ + rm -f -r bin/ + rm -f -r dist/ + rm -f -r *.egg-info + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -rf {} + + find . -name '.pytest_cache' -exec rm -rf {} + + +tests: + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + python -m pytest + +requirements: + poetry export -f requirements.txt -o requirements.txt + poetry export --dev -f requirements.txt -o requirements-dev.txt \ No newline at end of file diff --git a/README.md b/README.md index 028123f..8e50dfc 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,7 @@ This is a collection of miscellaneous tools released by the BloodHound team. See * DBCreator - Tool to generate randomized Neo4j databases for use with BloodHound * BloodHoundAnalytics.pbix - Proof of concept charting capability * BloodHoundAnalytics.py - Proof of concept audit script + +# Credits + +This package was created with [Cookiecutter](https://github.com/cookiecutter/cookiecutter) and the [byt3bl33d3r/pythoncookie](https://github.com/byt3bl33d3r/pythoncookie) project template. diff --git a/bh_tools/__init__.py b/bh_tools/__init__.py new file mode 100644 index 0000000..1876243 --- /dev/null +++ b/bh_tools/__init__.py @@ -0,0 +1,3 @@ +import importlib.metadata + +__version__ = importlib.metadata.version("bloodhound-tools") diff --git a/bloodhoundanalytics.py b/bh_tools/analytics.py similarity index 90% rename from bloodhoundanalytics.py rename to bh_tools/analytics.py index 893262f..1bbef7d 100644 --- a/bloodhoundanalytics.py +++ b/bh_tools/analytics.py @@ -1,1255 +1,1255 @@ -from openpyxl import Workbook, styles -from openpyxl.utils import get_column_letter -from neo4j.v1 import GraphDatabase -import cmd -import sys -from timeit import default_timer as timer - -# Authors: -# Andy Robbins - @_wald0 -# Rohan Vazarkar - @CptJesus -# https://www.specterops.io - -# License: GPLv3 - -class Messages(object): - def title(self): - print "================================================================" - print " BloodHound Analytics Generator" - print " connect - connect to database" - print " dbconfig - configure database settings" - print " changedomain - change domain for analysis" - print " startanalysis - start analysis for specified domain" - print " changefilename - change output filename" - print "================================================================" - - def input_default(self, prompt, default): - return raw_input("%s [%s] " % (prompt, default)) or default - - -class FrontPage(object): - def __init__(self, driver, domain, workbook): - self.driver = driver - self.domain = domain - self.col_count = 1 - self.workbook = workbook - - def write_single_cell(self, sheet, row, column, text): - sheet.cell(row, column, value=text) - - def do_front_page_analysis(self): - func_list = [self.create_node_statistics, - self.create_edge_statistics, self.create_qa_statistics] - sheet = self.workbook._sheets[0] - self.write_single_cell(sheet, 1, 1, "Node Statistics") - self.write_single_cell(sheet, 1, 2, "Edge Statistics") - self.write_single_cell(sheet, 1, 3, "QA Info") - font = styles.Font(bold=True) - sheet.cell(1, 1).font = font - sheet.cell(1, 2).font = font - sheet.cell(1, 3).font = font - - for f in func_list: - s = timer() - f(sheet) - print "{} completed in {}s".format(f.__name__, timer() - s) - - def create_node_statistics(self, sheet): - session = self.driver.session() - for result in session.run("MATCH (n:User {domain:{domain}}) RETURN count(n)", domain=self.domain): - self.write_single_cell( - sheet, 2, 1, "Users: {:,}".format(result[0])) - - for result in session.run("MATCH (n:Group {domain:{domain}}) RETURN count(n)", domain=self.domain): - self.write_single_cell( - sheet, 3, 1, "Groups: {:,}".format(result[0])) - - for result in session.run("MATCH (n:Computer {domain:{domain}}) RETURN count(n)", domain=self.domain): - self.write_single_cell( - sheet, 4, 1, "Computers: {:,}".format(result[0])) - - for result in session.run("MATCH (n:Domain) RETURN count(n)", domain=self.domain): - self.write_single_cell( - sheet, 5, 1, "Other Domains: {:,}".format(result[0]-1)) - - for result in session.run("MATCH (n:GPO) WHERE n.name =~ '.*"+ self.domain +"$' RETURN count(n)"): - self.write_single_cell( - sheet, 6, 1, "GPOs: {:,}".format(result[0])) - - for result in session.run("MATCH (n:OU) WHERE n.name =~ '.*@"+ self.domain +"$' RETURN count(n)"): - self.write_single_cell( - sheet, 7, 1, "OUs: {:,}".format(result[0])) - - session.close() - - def create_edge_statistics(self, sheet): - session = self.driver.session() - for result in session.run("MATCH ()-[r:MemberOf]->({domain:{domain}}) RETURN count(r)", domain=self.domain): - self.write_single_cell( - sheet, 2, 2, "MemberOf: {:,}".format(result[0])) - - for result in session.run("MATCH ()-[r:AdminTo]->({domain:{domain}}) RETURN count(r)", domain=self.domain): - self.write_single_cell( - sheet, 3, 2, "AdminTo: {:,}".format(result[0])) - - for result in session.run("MATCH ()-[r:HasSession]->({domain:{domain}}) RETURN count(r)", domain=self.domain): - self.write_single_cell( - sheet, 4, 2, "HasSession: {:,}".format(result[0])) - - for result in session.run("MATCH ()-[r:GpLink]-(n) WHERE n.name =~ '.*"+ self.domain +"$' RETURN count(r)"): - self.write_single_cell( - sheet, 5, 2, "GpLinks: {:,}".format(result[0])) - - for result in session.run("MATCH ()-[r {isacl:true}]->({domain:{domain}}) RETURN count(r)", domain=self.domain): - self.write_single_cell( - sheet, 6, 2, "ACLs: {:,}".format(result[0])) - session.close() - - def create_qa_statistics(self, sheet): - session = self.driver.session() - computer_local_admin_pct = 0 - computer_session_pct = 0 - user_session_pct = 0 - - query = """MATCH (n)-[:AdminTo]->(c:Computer {domain:{domain}}) - WITH COUNT(DISTINCT(c)) as computersWithAdminsCount - MATCH (c2:Computer {domain:{domain}}) - RETURN toInt(100 * (toFloat(computersWithAdminsCount) / COUNT(c2))) - """ - for result in session.run(query, domain=self.domain): - computer_local_admin_pct = result[0] - - query = """MATCH (c:Computer {domain:{domain}})-[:HasSession]->() - WITH COUNT(DISTINCT(c)) as computersWithSessions - MATCH (c2:Computer {domain:{domain}}) - RETURN toInt(100 * (toFloat(computersWithSessions) / COUNT(c2))) - """ - - for result in session.run(query, domain=self.domain): - computer_session_pct = result[0] - - query = """MATCH ()-[:HasSession]->(u:User {domain:{domain}}) - WITH COUNT(DISTINCT(u)) as usersWithSessions - MATCH (u2:User {domain:{domain},enabled:true}) - RETURN toInt(100 * (toFloat(usersWithSessions) / COUNT(u2))) - """ - - for result in session.run(query, domain=self.domain): - user_session_pct = result[0] - - session.close() - self.write_single_cell(sheet, 2, 3, "Computers With Local Admin Data: {}%".format(computer_local_admin_pct)) - self.write_single_cell(sheet, 3, 3, "Computers With Session Data: {}%".format(computer_session_pct)) - self.write_single_cell(sheet, 4, 3, "Users With Session Data: {}%".format(user_session_pct)) - - -class LowHangingFruit(object): - def __init__(self, driver, domain, workbook): - self.driver = driver - self.domain = domain - self.col_count = 1 - self.workbook = workbook - - def write_column_data(self, sheet, title, results): - count = len(results) - offset = 6 - font = styles.Font(bold=True) - c = sheet.cell(offset, self.col_count) - c.font = font - sheet.cell(offset, self.col_count, value=title.format(count)) - for i in xrange(0, count): - sheet.cell(i+offset+1, self.col_count, value=results[i]) - self.col_count += 1 - - def write_single_cell(self, sheet, row, column, text): - sheet.cell(row, column, value=text) - - def do_low_hanging_fruit_analysis(self): - func_list = [ - self.domain_user_admin, self.everyone_admin, self.authenticated_users_admin, - self.domain_users_control, self.everyone_control, self.authenticated_users_control, - self.domain_users_rdp, self.everyone_rdp, self.authenticated_users_dcom, - self.domain_users_dcom, self.everyone_dcom, self.authenticated_users_dcom, - self.shortest_acl_path_domain_users, self.shortest_derivative_path_domain_users, self.shortest_hybrid_path_domain_users, - self.shortest_acl_path_everyone, self.shortest_derivative_path_everyone, self.shortest_hybrid_path_everyone, - self.shortest_acl_path_auth_users, self.shortest_derivative_path_auth_users, self.shortest_hybrid_path_auth_users, - self.kerberoastable_path_len, self.asreproastable_path_len, self.high_admin_comps - ] - sheet = self.workbook._sheets[2] - self.write_single_cell(sheet, 1, 1, "Domain Users to Domain Admins") - self.write_single_cell(sheet, 1, 2, "Everyone to Domain Admins") - self.write_single_cell( - sheet, 1, 3, "Authenticated Users to Domain Admins") - font = styles.Font(bold=True) - sheet.cell(1, 1).font = font - sheet.cell(1, 2).font = font - sheet.cell(1, 3).font = font - - for f in func_list: - s = timer() - f(sheet) - print "{} completed in {}s".format(f.__name__, timer() - s) - - def domain_user_admin(self, sheet): - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-513" - OPTIONAL MATCH (g)-[:AdminTo]->(c1) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2) - WITH COLLECT(c1) + COLLECT(c2) as tempVar - UNWIND tempVar AS computers - RETURN DISTINCT(computers.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Domain Users with Local Admin: {}", results) - - def everyone_admin(self, sheet): - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid = "S-1-1-0" - OPTIONAL MATCH (g)-[:AdminTo]->(c1) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2) - WITH COLLECT(c1) + COLLECT(c2) as tempVar - UNWIND tempVar AS computers - RETURN DISTINCT(computers.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data(sheet, "Everyone with Local Admin: {}", results) - - def authenticated_users_admin(self, sheet): - - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid = "S-1-5-11" - OPTIONAL MATCH (g)-[:AdminTo]->(c1) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2) - WITH COLLECT(c1) + COLLECT(c2) as tempVar - UNWIND tempVar AS computers - RETURN DISTINCT(computers.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Authenticated Users with Local Admin: {}", results) - - def domain_users_control(self, sheet): - - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-513" - OPTIONAL MATCH (g)-[{isacl:true}]->(n) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(m) - WITH COLLECT(n) + COLLECT(m) as tempVar - UNWIND tempVar AS objects - RETURN DISTINCT(objects) - ORDER BY objects.name ASC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Objects Controlled by Domain Users: {}", results) - - def everyone_control(self, sheet): - - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid = 'S-1-1-0' - OPTIONAL MATCH (g)-[{isacl:true}]->(n) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(m) - WITH COLLECT(n) + COLLECT(m) as tempVar - UNWIND tempVar AS objects - RETURN DISTINCT(objects) - ORDER BY objects.name ASC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Objects Controlled by Everyone: {}", results) - - def authenticated_users_control(self, sheet): - - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid = 'S-1-5-11' - OPTIONAL MATCH (g)-[{isacl:true}]->(n) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(m) - WITH COLLECT(n) + COLLECT(m) as tempVar - UNWIND tempVar AS objects - RETURN DISTINCT(objects) - ORDER BY objects.name ASC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Objects Controlled by Authenticated Users: {}", results) - - def domain_users_rdp(self, sheet): - - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-513" - OPTIONAL MATCH (g)-[:CanRDP]->(c1) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c2) - WITH COLLECT(c1) + COLLECT(c2) as tempVar - UNWIND tempVar AS computers - RETURN DISTINCT(computers.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Domain Users with RDP Rights: {}", results) - - def everyone_rdp(self, sheet): - - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid = "S-1-1-0" - OPTIONAL MATCH (g)-[:CanRDP]->(c1) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c2) - WITH COLLECT(c1) + COLLECT(c2) as tempVar - UNWIND tempVar AS computers - RETURN DISTINCT(computers.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data(sheet, "Everyone with RDP Rights: {}", results) - - def authenticated_users_rdp(self, sheet): - - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid = "S-1-5-11" - OPTIONAL MATCH (g)-[:CanRDP]->(c1) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c2) - WITH COLLECT(c1) + COLLECT(c2) as tempVar - UNWIND tempVar AS computers - RETURN DISTINCT(computers.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Authenticated Users with RDP Rights: {}", results) - - def domain_users_dcom(self, sheet): - - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-513" - OPTIONAL MATCH (g)-[:ExecuteDCOM]->(c1) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:ExecuteDCOM]->(c2) - WITH COLLECT(c1) + COLLECT(c2) as tempVar - UNWIND tempVar AS computers - RETURN DISTINCT(computers.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Domain Users with DCOM Rights: {}", results) - - def everyone_dcom(self, sheet): - - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid = "S-1-1-0" - OPTIONAL MATCH (g)-[:ExecuteDCOM]->(c1) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:ExecuteDCOM]->(c2) - WITH COLLECT(c1) + COLLECT(c2) as tempVar - UNWIND tempVar AS computers - RETURN DISTINCT(computers.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Domain Users with DCOM Rights: {}", results) - - def authenticated_users_dcom(self, sheet): - - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid = "S-1-5-11" - OPTIONAL MATCH (g)-[:ExecuteDCOM]->(c1) - OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:ExecuteDCOM]->(c2) - WITH COLLECT(c1) + COLLECT(c2) as tempVar - UNWIND tempVar AS computers - RETURN DISTINCT(computers.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Domain Users with DCOM Rights: {}", results) - - def shortest_acl_path_domain_users(self, sheet): - count_query = """MATCH (g1:Group {domain:{domain}}) - WHERE g1.objectsid ENDS WITH "-513" - MATCH (g2:Group {domain:{domain}}) - WHERE g2.objectsid ENDS WITH "-512" - MATCH p = shortestPath((g1)-[:Owns|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner*1..]->(g2)) - RETURN LENGTH(p) - """ - - session = self.driver.session() - count = 0 - for result in session.run(count_query, domain=self.domain): - count = result[0] - - session.close() - self.write_single_cell( - sheet, 2, 1, "Shortest ACL Path Length: {}".format(count)) - - def shortest_derivative_path_domain_users(self, sheet): - count_query = """MATCH (g1:Group {domain:{domain}}) - WHERE g1.objectsid ENDS WITH "-513" - MATCH (g2:Group {domain:{domain}}) - WHERE g2.objectsid ENDS WITH "-512" - MATCH p = shortestPath((g1)-[:AdminTo|HasSession|MemberOf*1..]->(g2)) - RETURN LENGTH(p) - """ - - session = self.driver.session() - count = 0 - for result in session.run(count_query, domain=self.domain): - count = result[0] - - session.close() - self.write_single_cell( - sheet, 3, 1, "Shortest Derivative Path Length: {}".format(count)) - - def shortest_hybrid_path_domain_users(self, sheet): - count_query = """MATCH (g1:Group {domain:{domain}}) - WHERE g1.objectsid ENDS WITH "-513" - MATCH (g2:Group {domain:{domain}}) - WHERE g2.objectsid ENDS WITH "-512" - MATCH p = shortestPath((g1)-[r*1..]->(g2)) - WHERE NONE(rel in r WHERE type(rel)="GetChanges") - WITH * - WHERE NONE(rel in r WHERE type(rel)="GetChangesAll") - RETURN LENGTH(p) - """ - - session = self.driver.session() - count = 0 - for result in session.run(count_query, domain=self.domain): - count = result[0] - - session.close() - self.write_single_cell( - sheet, 4, 1, "Shortest Hybrid Path Length: {}".format(count)) - - def shortest_acl_path_everyone(self, sheet): - count_query = """MATCH (g1:Group {domain:{domain}}) - WHERE g1.objectsid = 'S-1-1-0' - MATCH (g2:Group {domain:{domain}}) - WHERE g2.objectsid ENDS WITH "-512" - MATCH p = shortestPath((g1)-[:Owns|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner*1..]->(g2)) - RETURN LENGTH(p) - """ - - session = self.driver.session() - count = 0 - for result in session.run(count_query, domain=self.domain): - count = result[0] - - session.close() - self.write_single_cell( - sheet, 2, 2, "Shortest ACL Path Length: {}".format(count)) - - def shortest_derivative_path_everyone(self, sheet): - count_query = """MATCH (g1:Group {domain:{domain}}) - WHERE g1.objectsid = 'S-1-1-0' - MATCH (g2:Group {domain:{domain}}) - WHERE g2.objectsid ENDS WITH "-512" - MATCH p = shortestPath((g1)-[:AdminTo|HasSession|MemberOf*1..]->(g2)) - RETURN LENGTH(p) - """ - - session = self.driver.session() - count = 0 - for result in session.run(count_query, domain=self.domain): - count = result[0] - - session.close() - self.write_single_cell( - sheet, 3, 2, "Shortest Derivative Path Length: {}".format(count)) - - def shortest_hybrid_path_everyone(self, sheet): - count_query = """MATCH (g1:Group {domain:{domain}}) - WHERE g1.objectsid = 'S-1-1-0' - MATCH (g2:Group {domain:{domain}}) - WHERE g2.objectsid ENDS WITH "-512" - MATCH p = shortestPath((g1)-[r*1..]->(g2)) - WHERE NONE(rel in r WHERE type(rel)="GetChanges") - WITH * - WHERE NONE(rel in r WHERE type(rel)="GetChangesAll") - RETURN LENGTH(p) - """ - - session = self.driver.session() - count = 0 - for result in session.run(count_query, domain=self.domain): - count = result[0] - - session.close() - self.write_single_cell( - sheet, 4, 2, "Shortest Hybrid Path Length: {}".format(count)) - - def shortest_acl_path_auth_users(self, sheet): - count_query = """MATCH (g1:Group {domain:{domain}}) - WHERE g1.objectsid = 'S-1-5-11' - MATCH (g2:Group {domain:{domain}}) - WHERE g2.objectsid ENDS WITH "-512" - MATCH p = shortestPath((g1)-[:Owns|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner*1..]->(g2)) - RETURN LENGTH(p) - """ - - session = self.driver.session() - count = 0 - for result in session.run(count_query, domain=self.domain): - count = result[0] - - session.close() - self.write_single_cell( - sheet, 2, 3, "Shortest ACL Path Length: {}".format(count)) - - def shortest_derivative_path_auth_users(self, sheet): - count_query = """MATCH (g1:Group {domain:{domain}}) - WHERE g1.objectsid = 'S-1-5-11' - MATCH (g2:Group {domain:{domain}}) - WHERE g2.objectsid ENDS WITH "-512" - MATCH p = shortestPath((g1)-[:AdminTo|HasSession|MemberOf*1..]->(g2)) - RETURN LENGTH(p) - """ - - session = self.driver.session() - count = 0 - for result in session.run(count_query, domain=self.domain): - count = result[0] - - session.close() - self.write_single_cell( - sheet, 3, 3, "Shortest Derivative Path Length: {}".format(count)) - - def shortest_hybrid_path_auth_users(self, sheet): - count_query = """MATCH (g1:Group {domain:{domain}}) - WHERE g1.objectsid = 'S-1-5-11' - MATCH (g2:Group {domain:{domain}}) - WHERE g2.objectsid ENDS WITH "-512" - MATCH p = shortestPath((g1)-[r*1..]->(g2)) - WHERE NONE(rel in r WHERE type(rel)="GetChanges") - WITH * - WHERE NONE(rel in r WHERE type(rel)="GetChangesAll") - RETURN LENGTH(p) - """ - - session = self.driver.session() - count = 0 - for result in session.run(count_query, domain=self.domain): - count = result[0] - - session.close() - self.write_single_cell( - sheet, 4, 3, "Shortest Hybrid Path Length: {}".format(count)) - - def kerberoastable_path_len(self, sheet): - list_query = """MATCH (u:User {domain:{domain},hasspn:true}) - MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-512" AND NOT u.name STARTS WITH "KRBTGT@" - MATCH p = shortestPath((u)-[*1..]->(g)) - RETURN u.name,LENGTH(p) - ORDER BY LENGTH(p) ASC - """ - - session = self.driver.session() - results = [] - for result in session.run(list_query, domain=self.domain): - results.append( - "{} - {}".format(result[0], result[1])) - - session.close() - self.write_column_data( - sheet, "Kerberoastable User to DA Path Length", results) - - def asreproastable_path_len(self, sheet): - list_query = """MATCH (u:User {domain:{domain},dontreqpreauth:True}) - MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-512" AND NOT u.name STARTS WITH "KRBTGT@" - MATCH p = shortestPath((u)-[*1..]->(g)) - RETURN u.name,LENGTH(p) - ORDER BY LENGTH(p) ASC - """ - - session = self.driver.session() - results = [] - for result in session.run(list_query, domain=self.domain): - results.append( - "{} - {}".format(result[0], result[1])) - - session.close() - self.write_column_data( - sheet, "ASReproastable User to DA Path Length", results) - - def high_admin_comps(self, sheet): - list_query = """MATCH (c:Computer {domain:{domain}}) - OPTIONAL MATCH (n)-[:AdminTo]->(c) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) - WITH COLLECT(n) + COLLECT(m) as tempVar,c - UNWIND tempVar as admins - RETURN c.name,COUNT(DISTINCT(admins)) - ORDER BY COUNT(DISTINCT(admins)) DESC - """ - - session = self.driver.session() - results = [] - for result in session.run(list_query, domain=self.domain): - count = result[1] - if (count > 1000): - results.append( - "{} - {}".format(result[0], count)) - - session.close() - self.write_column_data( - sheet, "Computers with > 1000 Admins: {}", results) - - -class CriticalAssets(object): - def __init__(self, driver, domain, workbook): - self.driver = driver - self.domain = domain - self.col_count = 1 - self.workbook = workbook - - def write_column_data(self, sheet, title, results): - count = len(results) - offset = 1 - font = styles.Font(bold=True) - c = sheet.cell(offset, self.col_count) - c.font = font - sheet.cell(offset, self.col_count, value=title.format(count)) - for i in xrange(0, count): - sheet.cell(i+offset+1, self.col_count, value=results[i]) - self.col_count += 1 - - def do_critical_asset_analysis(self): - func_list = [ - self.admins_on_dc, self.rdp_on_dc, self.gpo_on_dc, self.admin_on_exch, - self.rdp_on_exch, self.gpo_on_exch, self.da_controllers, self.da_sessions, - self.gpo_on_da, self.da_equiv_controllers, self.da_equiv_sessions, self.gpo_on_da_equiv] - sheet = self.workbook._sheets[1] - for f in func_list: - s = timer() - f(sheet) - print "{} completed in {}s".format(f.__name__, timer() - s) - - def admins_on_dc(self, sheet): - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-516" - MATCH (c:Computer)-[:MemberOf*1..]->(g) - OPTIONAL MATCH (n)-[:AdminTo]->(c) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) - WHERE (n:User OR n:Computer) AND (m:User OR m:Computer) - WITH COLLECT(n) + COLLECT(m) as tempVar1 - UNWIND tempVar1 as tempVar2 - WITH DISTINCT(tempVar2) as tempVar3 - RETURN tempVar3.name - ORDER BY tempVar3.name ASC""" - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Admins on Domain Controllers: {}", results) - - def rdp_on_dc(self, sheet): - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-516" - MATCH (c:Computer)-[:MemberOf*1..]->(g) - OPTIONAL MATCH (n)-[:CanRDP]->(c) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c) - WHERE (n:User OR n:Computer) AND (m:User OR m:Computer) - - WITH COLLECT(n) + COLLECT(m) as tempVar1 - UNWIND tempVar1 as tempVar2 - WITH DISTINCT(tempVar2) as tempVar3 - RETURN tempVar3.name - ORDER BY tempVar3.name ASC - """ - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "RDPers on Domain Controllers: {}", results) - - def gpo_on_dc(self, sheet): - list_query = """MATCH (g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-516" - MATCH (c:Computer)-[:MemberOf*1..]->(g) - OPTIONAL MATCH p1 = (g1:GPO)-[r1:GpLink {enforced:true}]->(container1)-[r2:Contains*1..]->(c) - OPTIONAL MATCH p2 = (g2:GPO)-[r3:GpLink {enforced:false}]->(container2)-[r4:Contains*1..]->(c) - WHERE NONE (x in NODES(p2) WHERE x.blocksinheritance = true AND x:OU AND NOT (g2)-->(x)) - WITH COLLECT(g1) + COLLECT(g2) AS tempVar1 - UNWIND tempVar1 as tempVar2 - WITH DISTINCT(tempVar2) as GPOs - OPTIONAL MATCH (n)-[{isacl:true}]->(GPOs) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(GPOs) - WITH COLLECT(n) + COLLECT(m) as tempVar1 - UNWIND tempVar1 as tempVar2 - RETURN DISTINCT(tempVar2.name) - ORDER BY tempVar2.name ASC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Domain Controller GPO Controllers: {}", results) - - def admin_on_exch(self, sheet): - list_query = """MATCH (n:Computer) - UNWIND n.serviceprincipalnames AS spn - MATCH (n) WHERE TOUPPER(spn) CONTAINS "EXCHANGEMDB" - WITH n as c - MATCH (c)-[:MemberOf*1..]->(g:Group {domain:{domain}}) - WHERE g.name CONTAINS "EXCHANGE" - OPTIONAL MATCH (n)-[:AdminTo]->(c) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) - WITH COLLECT(n) + COLLECT(m) as tempVar1 - UNWIND tempVar1 AS exchangeAdmins - RETURN DISTINCT(exchangeAdmins.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Admins on Exchange Servers: {}", results) - - def rdp_on_exch(self, sheet): - list_query = """MATCH (n:Computer) - UNWIND n.serviceprincipalnames AS spn - MATCH (n) WHERE TOUPPER(spn) CONTAINS "EXCHANGEMDB" - WITH n as c - MATCH (c)-[:MemberOf*1..]->(g:Group {domain:{domain}}) - WHERE g.name CONTAINS "EXCHANGE" - OPTIONAL MATCH (n)-[:CanRDP]->(c) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c) - WITH COLLECT(n) + COLLECT(m) as tempVar1 - UNWIND tempVar1 AS exchangeAdmins - RETURN DISTINCT(exchangeAdmins.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "RDPers on Exchange Servers: {}", results) - - def gpo_on_exch(self, sheet): - list_query = """MATCH (n:Computer) - UNWIND n.serviceprincipalnames AS spn - MATCH (n) WHERE TOUPPER(spn) CONTAINS "EXCHANGEMDB" - WITH n as c - MATCH (c)-[:MemberOf*1..]->(g:Group {domain:{domain}}) - WHERE g.name CONTAINS "EXCHANGE" - OPTIONAL MATCH p1 = (g1:GPO)-[r1:GpLink {enforced:true}]->(container1)-[r2:Contains*1..]->(c) - OPTIONAL MATCH p2 = (g2:GPO)-[r3:GpLink {enforced:false}]->(container2)-[r4:Contains*1..]->(c) - WHERE NONE (x in NODES(p2) WHERE x.blocksinheritance = true AND x:OU AND NOT (g2)-->(x)) - WITH COLLECT(g1) + COLLECT(g2) AS tempVar1 - UNWIND tempVar1 as tempVar2 - WITH DISTINCT(tempVar2) as GPOs - OPTIONAL MATCH (n)-[{isacl:true}]->(GPOs) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(GPOs) - WITH COLLECT(n) + COLLECT(m) as tempVar1 - UNWIND tempVar1 as tempVar2 - RETURN DISTINCT(tempVar2.name) - ORDER BY tempVar2.name ASC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Exchange Server GPO Controllers: {}", results) - - def da_controllers(self, sheet): - list_query = """MATCH (DAUser)-[:MemberOf*1..]->(g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-512" - OPTIONAL MATCH (n)-[{isacl:true}]->(DAUser) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(DAUser) - WITH COLLECT(n) + COLLECT(m) as tempVar1 - UNWIND tempVar1 AS DAControllers - RETURN DISTINCT(DAControllers.name) - ORDER BY DAControllers.name ASC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Domain Admin Controllers: {}", results) - - def da_sessions(self, sheet): - list_query = """MATCH (c:Computer)-[:HasSession]->()-[:MemberOf*1..]->(g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-512" - RETURN DISTINCT(c.name) - ORDER BY c.name ASC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Computers with DA Sessions: {}", results) - - def gpo_on_da(self, sheet): - list_query = """MATCH (DAUser)-[:MemberOf*1..]->(g:Group {domain:{domain}}) - WHERE g.objectsid ENDS WITH "-512" - OPTIONAL MATCH p1 = (g1:GPO)-[r1:GpLink {enforced:true}]->(container1)-[r2:Contains*1..]->(DAUser) - OPTIONAL MATCH p2 = (g2:GPO)-[r3:GpLink {enforced:false}]->(container2)-[r4:Contains*1..]->(DAUser) - WHERE NONE (x in NODES(p2) WHERE x.blocksinheritance = true AND x:OU AND NOT (g2)-->(x)) - WITH COLLECT(g1) + COLLECT(g2) AS tempVar1 - UNWIND tempVar1 as tempVar2 - WITH DISTINCT(tempVar2) as GPOs - OPTIONAL MATCH (n)-[{isacl:true}]->(GPOs) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(GPOs) - WITH COLLECT(n) + COLLECT(m) as tempVar1 - UNWIND tempVar1 as tempVar2 - RETURN DISTINCT(tempVar2.name) - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "Domain Admin GPO Controllers: {}", results) - - def da_equiv_controllers(self, sheet): - list_query = """MATCH (u:User)-[:MemberOf*1..]->(g:Group {domain:{domain},highvalue:true}) - OPTIONAL MATCH (n)-[{isacl:true}]->(u) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(u) - WITH COLLECT(n) + COLLECT(m) as tempVar - UNWIND tempVar as highValueControllers - RETURN DISTINCT(highValueControllers.name) - ORDER BY highValueControllers.name ASC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "High Value Object Controllers: {}", results) - - def da_equiv_sessions(self, sheet): - list_query = """MATCH (c:Computer)-[:HasSession]->(u:User)-[:MemberOf*1..]->(g:Group {domain:{domain},highvalue:true}) - RETURN DISTINCT(c.name) - ORDER BY c.name ASC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "High Value User Sessions: {}", results) - - def gpo_on_da_equiv(self, sheet): - list_query = """MATCH (u:User)-[:MemberOf*1..]->(g:Group {domain:{domain},highvalue:true}) - OPTIONAL MATCH p1 = (g1:GPO)-[r1:GpLink {enforced:true}]->(container1)-[r2:Contains*1..]->(u) - OPTIONAL MATCH p2 = (g2:GPO)-[r3:GpLink {enforced:false}]->(container2)-[r4:Contains*1..]->(u) - WHERE NONE (x in NODES(p2) WHERE x.blocksinheritance = true AND x:OU AND NOT (g2)-->(x)) - WITH COLLECT(g1) + COLLECT(g2) AS tempVar1 - UNWIND tempVar1 as tempVar2 - WITH DISTINCT(tempVar2) as GPOs - OPTIONAL MATCH (n)-[{isacl:true}]->(GPOs) - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(GPOs) - WITH COLLECT(n) + COLLECT(m) as tempVar1 - UNWIND tempVar1 as tempVar2 - RETURN DISTINCT(tempVar2.name) - ORDER BY tempVar2.name ASC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append(result[0]) - - session.close() - self.write_column_data( - sheet, "High Value User GPO Controllers: {}", results) - -class CrossDomain(object): - def __init__(self, driver, domain, workbook): - self.driver = driver - self.domain = domain - self.col_count = 1 - self.workbook = workbook - - def write_column_data(self, sheet, title, results): - count = len(results) - offset = 1 - font = styles.Font(bold=True) - c = sheet.cell(offset, self.col_count) - c.font = font - sheet.cell(offset, self.col_count, value=title.format(count)) - for i in xrange(0, count): - sheet.cell(i+offset+1, self.col_count, value=results[i]) - self.col_count += 1 - - def write_single_cell(self, sheet, row, column, text): - sheet.cell(row, column, value=text) - - def do_cross_domain_analysis(self): - func_list = [ - self.foreign_admins, self.foreign_gpo_controllers, self.foreign_user_controllers - ] - sheet = self.workbook._sheets[3] - - for f in func_list: - s = timer() - f(sheet) - print "{} completed in {}s".format(f.__name__, timer() - s) - - def foreign_admins(self, sheet): - list_query = """MATCH (c:Computer {domain:{domain}}) - OPTIONAL MATCH (n)-[:AdminTo]->(c) - WHERE (n:User OR n:Computer) AND NOT n.domain = c.domain - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) - WHERE (m:User OR m:Computer) AND NOT m.domain = c.domain - WITH COLLECT(n) + COLLECT(m) AS tempVar,c - UNWIND tempVar AS foreignAdmins - RETURN c.name,COUNT(DISTINCT(foreignAdmins)) - ORDER BY COUNT(DISTINCT(foreignAdmins)) DESC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append("{} - {}".format(result[0], result[1])) - - session.close() - self.write_column_data( - sheet, "Computers with Foreign Admins: {}", results) - - def foreign_gpo_controllers(self, sheet): - list_query = """MATCH (g:GPO) - WHERE SPLIT(g.name,'@')[1] = {domain} - OPTIONAL MATCH (n)-[{isacl:true}]->(g) - WHERE (n:User OR n:Computer) AND NOT n.domain = {domain} - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(g) - WHERE (m:User OR m:Computer) AND NOT m.domain = {domain} - WITH COLLECT(n) + COLLECT(m) AS tempVar,g - UNWIND tempVar AS foreignGPOControllers - RETURN g.name,COUNT(DISTINCT(foreignGPOControllers)) - ORDER BY COUNT(DISTINCT(foreignGPOControllers)) DESC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append("{} - {}".format(result[0], result[1])) - - session.close() - self.write_column_data( - sheet, "GPOs with Foreign Controllers: {}", results) - - def foreign_user_controllers(self, sheet): - list_query = """MATCH (g:Group {domain:{domain}}) - OPTIONAL MATCH (n)-[{isacl:true}]->(g) - WHERE (n:User OR n:Computer) AND NOT n.domain = g.domain - OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(g) - WHERE (m:User OR m:Computer) AND NOT m.domain = g.domain - WITH COLLECT(n) + COLLECT(m) AS tempVar,g - UNWIND tempVar AS foreignGroupControllers - RETURN g.name,COUNT(DISTINCT(foreignGroupControllers)) - ORDER BY COUNT(DISTINCT(foreignGroupControllers)) DESC - """ - - session = self.driver.session() - results = [] - - for result in session.run(list_query, domain=self.domain): - results.append("{} - {}".format(result[0], result[1])) - - session.close() - self.write_column_data( - sheet, "Groups with Foreign Controllers: {}", results) - -class MainMenu(cmd.Cmd): - def __init__(self): - self.m = Messages() - self.url = "bolt://localhost:7687" - self.username = "neo4j" - self.password = "BloodHound" - self.driver = None - self.connected = False - self.num_nodes = 500 - self.filename = "BloodHoundAnalytics.xlsx" - if (len(sys.argv) < 2): - print "No domain specified." - print "Usage: python {} DOMAINNAME".format(sys.argv[0]) - sys.exit() - self.domain = sys.argv[1].upper() - self.domain_validated = False - - cmd.Cmd.__init__(self) - - def do_changefilename(self, args): - if args == "": - print "No filename specified" - return - self.filename = args - print "Change filename to {}".format(self.filename) - - def do_changedomain(self, args): - if args == "": - print "No domain specified" - return - self.domain_validated = False - self.domain = args.upper() - self.validate_domain() - - def cmdloop(self): - while True: - self.m.title() - try: - cmd.Cmd.cmdloop(self) - except KeyboardInterrupt: - if self.driver is not None: - self.driver.close() - raise KeyboardInterrupt - - def do_dbconfig(self, args): - "Configure connection settings to the neo4j database" - print "Current Settings:" - print "DB Url: {}".format(self.url) - print "DB Username: {}".format(self.username) - print "DB Password: {}".format(self.password) - print "" - self.url = self.m.input_default("Enter DB URL", self.url) - self.username = self.m.input_default( - "Enter DB Username", self.username) - self.password = self.m.input_default( - "Enter DB Password", self.password) - print "" - print "New Settings:" - print "DB Url: {}".format(self.url) - print "DB Username: {}".format(self.username) - print "DB Password: {}".format(self.password) - print "" - print "Testing DB Connection" - self.test_db_conn() - - def do_exit(self, args): - raise KeyboardInterrupt - - def do_connect(self, args): - self.test_db_conn() - - def test_db_conn(self): - self.connected = False - if self.driver is not None: - self.driver.close() - try: - self.driver = GraphDatabase.driver( - self.url, auth=(self.username, self.password)) - self.connected = True - print "Database Connection Successful!" - self.validate_domain() - except Exception as e: - print e - self.connected = False - print "Database Connection Failed. Check your settings." - - def validate_domain(self): - if not self.connected: - print "Cant validate domain. Connect using connect first" - return - - print "Validating Selected Domain" - session = self.driver.session() - for result in session.run("MATCH (n {domain:{domain}}) RETURN COUNT(n)", domain=self.domain): - if (int(result[0]) > 0): - print "Domain {domain} validated!".format(domain=self.domain) - self.domain_validated = True - self.create_workbook() - self.create_analytics() - else: - print "Invalid domain specified, use changedomain to pick a new one" - - def do_startanalysis(self, args): - if not self.connected: - print "Not connected to database. Use connect command" - return - - if not self.domain_validated: - print "Invalid domain or not validated. Use changedomain command" - return - - print "----------------------------------" - print "Generating Front Page" - print "----------------------------------" - print "" - self.front.do_front_page_analysis() - print "----------------------------------" - print "Generating Critical Assets Page" - print "----------------------------------" - print "" - self.crit.do_critical_asset_analysis() - print "----------------------------------" - print "Generating Low Hanging Fruit Page" - print "----------------------------------" - print "" - self.low.do_low_hanging_fruit_analysis() - print "----------------------------------" - print "Generating Cross Domain Page" - print "----------------------------------" - print "" - self.cross.do_cross_domain_analysis() - print "Analytics Complete! Saving workbook to {}".format(self.filename) - self.save_workbook() - - def create_analytics(self): - self.crit = CriticalAssets(self.driver, self.domain, self.workbook) - self.low = LowHangingFruit(self.driver, self.domain, self.workbook) - self.front = FrontPage(self.driver, self.domain, self.workbook) - self.cross = CrossDomain(self.driver, self.domain, self.workbook) - self.save_workbook() - - def create_workbook(self): - wb = Workbook() - ws = wb.active - ws.title = '{domain} Overview'.format(domain=self.domain) - wb.create_sheet(title="Critical Assets") - wb.create_sheet(title="Low Hanging Fruit") - wb.create_sheet(title="Cross Domain Attacks") - self.workbook = wb - - def save_workbook(self): - for worksheet in self.workbook._sheets: - for col in worksheet.columns: - max_length = 0 - column = get_column_letter(col[0].column) # Get the column name - for cell in col: - try: # Necessary to avoid error on empty cells - if len(str(cell.value)) > max_length: - max_length = len(cell.value) - except: - pass - adjusted_width = (max_length + 2) * 1.2 - worksheet.column_dimensions[column].width = adjusted_width - self.workbook.save(self.filename) - - -if __name__ == '__main__': - try: - MainMenu().cmdloop() - except KeyboardInterrupt: - print "Exiting" - sys.exit() +from openpyxl import Workbook, styles +from openpyxl.utils import get_column_letter +from neo4j import GraphDatabase +import cmd +import sys +from timeit import default_timer as timer + +# Authors: +# Andy Robbins - @_wald0 +# Rohan Vazarkar - @CptJesus +# https://www.specterops.io + +# License: GPLv3 + +class Messages(object): + def title(self): + print("================================================================") + print(" BloodHound Analytics Generator") + print(" connect - connect to database") + print(" dbconfig - configure database settings") + print(" changedomain - change domain for analysis") + print(" startanalysis - start analysis for specified domain") + print(" changefilename - change output filename") + print("================================================================") + + def input_default(self, prompt, default): + return input("%s [%s] " % (prompt, default)) or default + + +class FrontPage(object): + def __init__(self, driver, domain, workbook): + self.driver = driver + self.domain = domain + self.col_count = 1 + self.workbook = workbook + + def write_single_cell(self, sheet, row, column, text): + sheet.cell(row, column, value=text) + + def do_front_page_analysis(self): + func_list = [self.create_node_statistics, + self.create_edge_statistics, self.create_qa_statistics] + sheet = self.workbook._sheets[0] + self.write_single_cell(sheet, 1, 1, "Node Statistics") + self.write_single_cell(sheet, 1, 2, "Edge Statistics") + self.write_single_cell(sheet, 1, 3, "QA Info") + font = styles.Font(bold=True) + sheet.cell(1, 1).font = font + sheet.cell(1, 2).font = font + sheet.cell(1, 3).font = font + + for f in func_list: + s = timer() + f(sheet) + print("{} completed in {}s".format(f.__name__, timer() - s)) + + def create_node_statistics(self, sheet): + session = self.driver.session() + for result in session.run("MATCH (n:User {domain:{domain}}) RETURN count(n)", domain=self.domain): + self.write_single_cell( + sheet, 2, 1, "Users: {:,}".format(result[0])) + + for result in session.run("MATCH (n:Group {domain:{domain}}) RETURN count(n)", domain=self.domain): + self.write_single_cell( + sheet, 3, 1, "Groups: {:,}".format(result[0])) + + for result in session.run("MATCH (n:Computer {domain:{domain}}) RETURN count(n)", domain=self.domain): + self.write_single_cell( + sheet, 4, 1, "Computers: {:,}".format(result[0])) + + for result in session.run("MATCH (n:Domain) RETURN count(n)", domain=self.domain): + self.write_single_cell( + sheet, 5, 1, "Other Domains: {:,}".format(result[0]-1)) + + for result in session.run("MATCH (n:GPO) WHERE n.name =~ '.*"+ self.domain +"$' RETURN count(n)"): + self.write_single_cell( + sheet, 6, 1, "GPOs: {:,}".format(result[0])) + + for result in session.run("MATCH (n:OU) WHERE n.name =~ '.*@"+ self.domain +"$' RETURN count(n)"): + self.write_single_cell( + sheet, 7, 1, "OUs: {:,}".format(result[0])) + + session.close() + + def create_edge_statistics(self, sheet): + session = self.driver.session() + for result in session.run("MATCH ()-[r:MemberOf]->({domain:{domain}}) RETURN count(r)", domain=self.domain): + self.write_single_cell( + sheet, 2, 2, "MemberOf: {:,}".format(result[0])) + + for result in session.run("MATCH ()-[r:AdminTo]->({domain:{domain}}) RETURN count(r)", domain=self.domain): + self.write_single_cell( + sheet, 3, 2, "AdminTo: {:,}".format(result[0])) + + for result in session.run("MATCH ()-[r:HasSession]->({domain:{domain}}) RETURN count(r)", domain=self.domain): + self.write_single_cell( + sheet, 4, 2, "HasSession: {:,}".format(result[0])) + + for result in session.run("MATCH ()-[r:GpLink]-(n) WHERE n.name =~ '.*"+ self.domain +"$' RETURN count(r)"): + self.write_single_cell( + sheet, 5, 2, "GpLinks: {:,}".format(result[0])) + + for result in session.run("MATCH ()-[r {isacl:true}]->({domain:{domain}}) RETURN count(r)", domain=self.domain): + self.write_single_cell( + sheet, 6, 2, "ACLs: {:,}".format(result[0])) + session.close() + + def create_qa_statistics(self, sheet): + session = self.driver.session() + computer_local_admin_pct = 0 + computer_session_pct = 0 + user_session_pct = 0 + + query = """MATCH (n)-[:AdminTo]->(c:Computer {domain:{domain}}) + WITH COUNT(DISTINCT(c)) as computersWithAdminsCount + MATCH (c2:Computer {domain:{domain}}) + RETURN toInt(100 * (toFloat(computersWithAdminsCount) / COUNT(c2))) + """ + for result in session.run(query, domain=self.domain): + computer_local_admin_pct = result[0] + + query = """MATCH (c:Computer {domain:{domain}})-[:HasSession]->() + WITH COUNT(DISTINCT(c)) as computersWithSessions + MATCH (c2:Computer {domain:{domain}}) + RETURN toInt(100 * (toFloat(computersWithSessions) / COUNT(c2))) + """ + + for result in session.run(query, domain=self.domain): + computer_session_pct = result[0] + + query = """MATCH ()-[:HasSession]->(u:User {domain:{domain}}) + WITH COUNT(DISTINCT(u)) as usersWithSessions + MATCH (u2:User {domain:{domain},enabled:true}) + RETURN toInt(100 * (toFloat(usersWithSessions) / COUNT(u2))) + """ + + for result in session.run(query, domain=self.domain): + user_session_pct = result[0] + + session.close() + self.write_single_cell(sheet, 2, 3, "Computers With Local Admin Data: {}%".format(computer_local_admin_pct)) + self.write_single_cell(sheet, 3, 3, "Computers With Session Data: {}%".format(computer_session_pct)) + self.write_single_cell(sheet, 4, 3, "Users With Session Data: {}%".format(user_session_pct)) + + +class LowHangingFruit(object): + def __init__(self, driver, domain, workbook): + self.driver = driver + self.domain = domain + self.col_count = 1 + self.workbook = workbook + + def write_column_data(self, sheet, title, results): + count = len(results) + offset = 6 + font = styles.Font(bold=True) + c = sheet.cell(offset, self.col_count) + c.font = font + sheet.cell(offset, self.col_count, value=title.format(count)) + for i in range(0, count): + sheet.cell(i+offset+1, self.col_count, value=results[i]) + self.col_count += 1 + + def write_single_cell(self, sheet, row, column, text): + sheet.cell(row, column, value=text) + + def do_low_hanging_fruit_analysis(self): + func_list = [ + self.domain_user_admin, self.everyone_admin, self.authenticated_users_admin, + self.domain_users_control, self.everyone_control, self.authenticated_users_control, + self.domain_users_rdp, self.everyone_rdp, self.authenticated_users_dcom, + self.domain_users_dcom, self.everyone_dcom, self.authenticated_users_dcom, + self.shortest_acl_path_domain_users, self.shortest_derivative_path_domain_users, self.shortest_hybrid_path_domain_users, + self.shortest_acl_path_everyone, self.shortest_derivative_path_everyone, self.shortest_hybrid_path_everyone, + self.shortest_acl_path_auth_users, self.shortest_derivative_path_auth_users, self.shortest_hybrid_path_auth_users, + self.kerberoastable_path_len, self.asreproastable_path_len, self.high_admin_comps + ] + sheet = self.workbook._sheets[2] + self.write_single_cell(sheet, 1, 1, "Domain Users to Domain Admins") + self.write_single_cell(sheet, 1, 2, "Everyone to Domain Admins") + self.write_single_cell( + sheet, 1, 3, "Authenticated Users to Domain Admins") + font = styles.Font(bold=True) + sheet.cell(1, 1).font = font + sheet.cell(1, 2).font = font + sheet.cell(1, 3).font = font + + for f in func_list: + s = timer() + f(sheet) + print("{} completed in {}s".format(f.__name__, timer() - s)) + + def domain_user_admin(self, sheet): + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-513" + OPTIONAL MATCH (g)-[:AdminTo]->(c1) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2) + WITH COLLECT(c1) + COLLECT(c2) as tempVar + UNWIND tempVar AS computers + RETURN DISTINCT(computers.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Domain Users with Local Admin: {}", results) + + def everyone_admin(self, sheet): + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid = "S-1-1-0" + OPTIONAL MATCH (g)-[:AdminTo]->(c1) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2) + WITH COLLECT(c1) + COLLECT(c2) as tempVar + UNWIND tempVar AS computers + RETURN DISTINCT(computers.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data(sheet, "Everyone with Local Admin: {}", results) + + def authenticated_users_admin(self, sheet): + + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid = "S-1-5-11" + OPTIONAL MATCH (g)-[:AdminTo]->(c1) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2) + WITH COLLECT(c1) + COLLECT(c2) as tempVar + UNWIND tempVar AS computers + RETURN DISTINCT(computers.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Authenticated Users with Local Admin: {}", results) + + def domain_users_control(self, sheet): + + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-513" + OPTIONAL MATCH (g)-[{isacl:true}]->(n) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(m) + WITH COLLECT(n) + COLLECT(m) as tempVar + UNWIND tempVar AS objects + RETURN DISTINCT(objects) + ORDER BY objects.name ASC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Objects Controlled by Domain Users: {}", results) + + def everyone_control(self, sheet): + + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid = 'S-1-1-0' + OPTIONAL MATCH (g)-[{isacl:true}]->(n) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(m) + WITH COLLECT(n) + COLLECT(m) as tempVar + UNWIND tempVar AS objects + RETURN DISTINCT(objects) + ORDER BY objects.name ASC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Objects Controlled by Everyone: {}", results) + + def authenticated_users_control(self, sheet): + + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid = 'S-1-5-11' + OPTIONAL MATCH (g)-[{isacl:true}]->(n) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(m) + WITH COLLECT(n) + COLLECT(m) as tempVar + UNWIND tempVar AS objects + RETURN DISTINCT(objects) + ORDER BY objects.name ASC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Objects Controlled by Authenticated Users: {}", results) + + def domain_users_rdp(self, sheet): + + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-513" + OPTIONAL MATCH (g)-[:CanRDP]->(c1) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c2) + WITH COLLECT(c1) + COLLECT(c2) as tempVar + UNWIND tempVar AS computers + RETURN DISTINCT(computers.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Domain Users with RDP Rights: {}", results) + + def everyone_rdp(self, sheet): + + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid = "S-1-1-0" + OPTIONAL MATCH (g)-[:CanRDP]->(c1) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c2) + WITH COLLECT(c1) + COLLECT(c2) as tempVar + UNWIND tempVar AS computers + RETURN DISTINCT(computers.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data(sheet, "Everyone with RDP Rights: {}", results) + + def authenticated_users_rdp(self, sheet): + + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid = "S-1-5-11" + OPTIONAL MATCH (g)-[:CanRDP]->(c1) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c2) + WITH COLLECT(c1) + COLLECT(c2) as tempVar + UNWIND tempVar AS computers + RETURN DISTINCT(computers.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Authenticated Users with RDP Rights: {}", results) + + def domain_users_dcom(self, sheet): + + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-513" + OPTIONAL MATCH (g)-[:ExecuteDCOM]->(c1) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:ExecuteDCOM]->(c2) + WITH COLLECT(c1) + COLLECT(c2) as tempVar + UNWIND tempVar AS computers + RETURN DISTINCT(computers.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Domain Users with DCOM Rights: {}", results) + + def everyone_dcom(self, sheet): + + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid = "S-1-1-0" + OPTIONAL MATCH (g)-[:ExecuteDCOM]->(c1) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:ExecuteDCOM]->(c2) + WITH COLLECT(c1) + COLLECT(c2) as tempVar + UNWIND tempVar AS computers + RETURN DISTINCT(computers.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Domain Users with DCOM Rights: {}", results) + + def authenticated_users_dcom(self, sheet): + + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid = "S-1-5-11" + OPTIONAL MATCH (g)-[:ExecuteDCOM]->(c1) + OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:ExecuteDCOM]->(c2) + WITH COLLECT(c1) + COLLECT(c2) as tempVar + UNWIND tempVar AS computers + RETURN DISTINCT(computers.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Domain Users with DCOM Rights: {}", results) + + def shortest_acl_path_domain_users(self, sheet): + count_query = """MATCH (g1:Group {domain:{domain}}) + WHERE g1.objectsid ENDS WITH "-513" + MATCH (g2:Group {domain:{domain}}) + WHERE g2.objectsid ENDS WITH "-512" + MATCH p = shortestPath((g1)-[:Owns|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner*1..]->(g2)) + RETURN LENGTH(p) + """ + + session = self.driver.session() + count = 0 + for result in session.run(count_query, domain=self.domain): + count = result[0] + + session.close() + self.write_single_cell( + sheet, 2, 1, "Shortest ACL Path Length: {}".format(count)) + + def shortest_derivative_path_domain_users(self, sheet): + count_query = """MATCH (g1:Group {domain:{domain}}) + WHERE g1.objectsid ENDS WITH "-513" + MATCH (g2:Group {domain:{domain}}) + WHERE g2.objectsid ENDS WITH "-512" + MATCH p = shortestPath((g1)-[:AdminTo|HasSession|MemberOf*1..]->(g2)) + RETURN LENGTH(p) + """ + + session = self.driver.session() + count = 0 + for result in session.run(count_query, domain=self.domain): + count = result[0] + + session.close() + self.write_single_cell( + sheet, 3, 1, "Shortest Derivative Path Length: {}".format(count)) + + def shortest_hybrid_path_domain_users(self, sheet): + count_query = """MATCH (g1:Group {domain:{domain}}) + WHERE g1.objectsid ENDS WITH "-513" + MATCH (g2:Group {domain:{domain}}) + WHERE g2.objectsid ENDS WITH "-512" + MATCH p = shortestPath((g1)-[r*1..]->(g2)) + WHERE NONE(rel in r WHERE type(rel)="GetChanges") + WITH * + WHERE NONE(rel in r WHERE type(rel)="GetChangesAll") + RETURN LENGTH(p) + """ + + session = self.driver.session() + count = 0 + for result in session.run(count_query, domain=self.domain): + count = result[0] + + session.close() + self.write_single_cell( + sheet, 4, 1, "Shortest Hybrid Path Length: {}".format(count)) + + def shortest_acl_path_everyone(self, sheet): + count_query = """MATCH (g1:Group {domain:{domain}}) + WHERE g1.objectsid = 'S-1-1-0' + MATCH (g2:Group {domain:{domain}}) + WHERE g2.objectsid ENDS WITH "-512" + MATCH p = shortestPath((g1)-[:Owns|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner*1..]->(g2)) + RETURN LENGTH(p) + """ + + session = self.driver.session() + count = 0 + for result in session.run(count_query, domain=self.domain): + count = result[0] + + session.close() + self.write_single_cell( + sheet, 2, 2, "Shortest ACL Path Length: {}".format(count)) + + def shortest_derivative_path_everyone(self, sheet): + count_query = """MATCH (g1:Group {domain:{domain}}) + WHERE g1.objectsid = 'S-1-1-0' + MATCH (g2:Group {domain:{domain}}) + WHERE g2.objectsid ENDS WITH "-512" + MATCH p = shortestPath((g1)-[:AdminTo|HasSession|MemberOf*1..]->(g2)) + RETURN LENGTH(p) + """ + + session = self.driver.session() + count = 0 + for result in session.run(count_query, domain=self.domain): + count = result[0] + + session.close() + self.write_single_cell( + sheet, 3, 2, "Shortest Derivative Path Length: {}".format(count)) + + def shortest_hybrid_path_everyone(self, sheet): + count_query = """MATCH (g1:Group {domain:{domain}}) + WHERE g1.objectsid = 'S-1-1-0' + MATCH (g2:Group {domain:{domain}}) + WHERE g2.objectsid ENDS WITH "-512" + MATCH p = shortestPath((g1)-[r*1..]->(g2)) + WHERE NONE(rel in r WHERE type(rel)="GetChanges") + WITH * + WHERE NONE(rel in r WHERE type(rel)="GetChangesAll") + RETURN LENGTH(p) + """ + + session = self.driver.session() + count = 0 + for result in session.run(count_query, domain=self.domain): + count = result[0] + + session.close() + self.write_single_cell( + sheet, 4, 2, "Shortest Hybrid Path Length: {}".format(count)) + + def shortest_acl_path_auth_users(self, sheet): + count_query = """MATCH (g1:Group {domain:{domain}}) + WHERE g1.objectsid = 'S-1-5-11' + MATCH (g2:Group {domain:{domain}}) + WHERE g2.objectsid ENDS WITH "-512" + MATCH p = shortestPath((g1)-[:Owns|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner*1..]->(g2)) + RETURN LENGTH(p) + """ + + session = self.driver.session() + count = 0 + for result in session.run(count_query, domain=self.domain): + count = result[0] + + session.close() + self.write_single_cell( + sheet, 2, 3, "Shortest ACL Path Length: {}".format(count)) + + def shortest_derivative_path_auth_users(self, sheet): + count_query = """MATCH (g1:Group {domain:{domain}}) + WHERE g1.objectsid = 'S-1-5-11' + MATCH (g2:Group {domain:{domain}}) + WHERE g2.objectsid ENDS WITH "-512" + MATCH p = shortestPath((g1)-[:AdminTo|HasSession|MemberOf*1..]->(g2)) + RETURN LENGTH(p) + """ + + session = self.driver.session() + count = 0 + for result in session.run(count_query, domain=self.domain): + count = result[0] + + session.close() + self.write_single_cell( + sheet, 3, 3, "Shortest Derivative Path Length: {}".format(count)) + + def shortest_hybrid_path_auth_users(self, sheet): + count_query = """MATCH (g1:Group {domain:{domain}}) + WHERE g1.objectsid = 'S-1-5-11' + MATCH (g2:Group {domain:{domain}}) + WHERE g2.objectsid ENDS WITH "-512" + MATCH p = shortestPath((g1)-[r*1..]->(g2)) + WHERE NONE(rel in r WHERE type(rel)="GetChanges") + WITH * + WHERE NONE(rel in r WHERE type(rel)="GetChangesAll") + RETURN LENGTH(p) + """ + + session = self.driver.session() + count = 0 + for result in session.run(count_query, domain=self.domain): + count = result[0] + + session.close() + self.write_single_cell( + sheet, 4, 3, "Shortest Hybrid Path Length: {}".format(count)) + + def kerberoastable_path_len(self, sheet): + list_query = """MATCH (u:User {domain:{domain},hasspn:true}) + MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-512" AND NOT u.name STARTS WITH "KRBTGT@" + MATCH p = shortestPath((u)-[*1..]->(g)) + RETURN u.name,LENGTH(p) + ORDER BY LENGTH(p) ASC + """ + + session = self.driver.session() + results = [] + for result in session.run(list_query, domain=self.domain): + results.append( + "{} - {}".format(result[0], result[1])) + + session.close() + self.write_column_data( + sheet, "Kerberoastable User to DA Path Length", results) + + def asreproastable_path_len(self, sheet): + list_query = """MATCH (u:User {domain:{domain},dontreqpreauth:True}) + MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-512" AND NOT u.name STARTS WITH "KRBTGT@" + MATCH p = shortestPath((u)-[*1..]->(g)) + RETURN u.name,LENGTH(p) + ORDER BY LENGTH(p) ASC + """ + + session = self.driver.session() + results = [] + for result in session.run(list_query, domain=self.domain): + results.append( + "{} - {}".format(result[0], result[1])) + + session.close() + self.write_column_data( + sheet, "ASReproastable User to DA Path Length", results) + + def high_admin_comps(self, sheet): + list_query = """MATCH (c:Computer {domain:{domain}}) + OPTIONAL MATCH (n)-[:AdminTo]->(c) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) + WITH COLLECT(n) + COLLECT(m) as tempVar,c + UNWIND tempVar as admins + RETURN c.name,COUNT(DISTINCT(admins)) + ORDER BY COUNT(DISTINCT(admins)) DESC + """ + + session = self.driver.session() + results = [] + for result in session.run(list_query, domain=self.domain): + count = result[1] + if (count > 1000): + results.append( + "{} - {}".format(result[0], count)) + + session.close() + self.write_column_data( + sheet, "Computers with > 1000 Admins: {}", results) + + +class CriticalAssets(object): + def __init__(self, driver, domain, workbook): + self.driver = driver + self.domain = domain + self.col_count = 1 + self.workbook = workbook + + def write_column_data(self, sheet, title, results): + count = len(results) + offset = 1 + font = styles.Font(bold=True) + c = sheet.cell(offset, self.col_count) + c.font = font + sheet.cell(offset, self.col_count, value=title.format(count)) + for i in range(0, count): + sheet.cell(i+offset+1, self.col_count, value=results[i]) + self.col_count += 1 + + def do_critical_asset_analysis(self): + func_list = [ + self.admins_on_dc, self.rdp_on_dc, self.gpo_on_dc, self.admin_on_exch, + self.rdp_on_exch, self.gpo_on_exch, self.da_controllers, self.da_sessions, + self.gpo_on_da, self.da_equiv_controllers, self.da_equiv_sessions, self.gpo_on_da_equiv] + sheet = self.workbook._sheets[1] + for f in func_list: + s = timer() + f(sheet) + print("{} completed in {}s".format(f.__name__, timer() - s)) + + def admins_on_dc(self, sheet): + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-516" + MATCH (c:Computer)-[:MemberOf*1..]->(g) + OPTIONAL MATCH (n)-[:AdminTo]->(c) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) + WHERE (n:User OR n:Computer) AND (m:User OR m:Computer) + WITH COLLECT(n) + COLLECT(m) as tempVar1 + UNWIND tempVar1 as tempVar2 + WITH DISTINCT(tempVar2) as tempVar3 + RETURN tempVar3.name + ORDER BY tempVar3.name ASC""" + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Admins on Domain Controllers: {}", results) + + def rdp_on_dc(self, sheet): + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-516" + MATCH (c:Computer)-[:MemberOf*1..]->(g) + OPTIONAL MATCH (n)-[:CanRDP]->(c) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c) + WHERE (n:User OR n:Computer) AND (m:User OR m:Computer) + + WITH COLLECT(n) + COLLECT(m) as tempVar1 + UNWIND tempVar1 as tempVar2 + WITH DISTINCT(tempVar2) as tempVar3 + RETURN tempVar3.name + ORDER BY tempVar3.name ASC + """ + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "RDPers on Domain Controllers: {}", results) + + def gpo_on_dc(self, sheet): + list_query = """MATCH (g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-516" + MATCH (c:Computer)-[:MemberOf*1..]->(g) + OPTIONAL MATCH p1 = (g1:GPO)-[r1:GpLink {enforced:true}]->(container1)-[r2:Contains*1..]->(c) + OPTIONAL MATCH p2 = (g2:GPO)-[r3:GpLink {enforced:false}]->(container2)-[r4:Contains*1..]->(c) + WHERE NONE (x in NODES(p2) WHERE x.blocksinheritance = true AND x:OU AND NOT (g2)-->(x)) + WITH COLLECT(g1) + COLLECT(g2) AS tempVar1 + UNWIND tempVar1 as tempVar2 + WITH DISTINCT(tempVar2) as GPOs + OPTIONAL MATCH (n)-[{isacl:true}]->(GPOs) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(GPOs) + WITH COLLECT(n) + COLLECT(m) as tempVar1 + UNWIND tempVar1 as tempVar2 + RETURN DISTINCT(tempVar2.name) + ORDER BY tempVar2.name ASC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Domain Controller GPO Controllers: {}", results) + + def admin_on_exch(self, sheet): + list_query = """MATCH (n:Computer) + UNWIND n.serviceprincipalnames AS spn + MATCH (n) WHERE TOUPPER(spn) CONTAINS "EXCHANGEMDB" + WITH n as c + MATCH (c)-[:MemberOf*1..]->(g:Group {domain:{domain}}) + WHERE g.name CONTAINS "EXCHANGE" + OPTIONAL MATCH (n)-[:AdminTo]->(c) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) + WITH COLLECT(n) + COLLECT(m) as tempVar1 + UNWIND tempVar1 AS exchangeAdmins + RETURN DISTINCT(exchangeAdmins.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Admins on Exchange Servers: {}", results) + + def rdp_on_exch(self, sheet): + list_query = """MATCH (n:Computer) + UNWIND n.serviceprincipalnames AS spn + MATCH (n) WHERE TOUPPER(spn) CONTAINS "EXCHANGEMDB" + WITH n as c + MATCH (c)-[:MemberOf*1..]->(g:Group {domain:{domain}}) + WHERE g.name CONTAINS "EXCHANGE" + OPTIONAL MATCH (n)-[:CanRDP]->(c) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c) + WITH COLLECT(n) + COLLECT(m) as tempVar1 + UNWIND tempVar1 AS exchangeAdmins + RETURN DISTINCT(exchangeAdmins.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "RDPers on Exchange Servers: {}", results) + + def gpo_on_exch(self, sheet): + list_query = """MATCH (n:Computer) + UNWIND n.serviceprincipalnames AS spn + MATCH (n) WHERE TOUPPER(spn) CONTAINS "EXCHANGEMDB" + WITH n as c + MATCH (c)-[:MemberOf*1..]->(g:Group {domain:{domain}}) + WHERE g.name CONTAINS "EXCHANGE" + OPTIONAL MATCH p1 = (g1:GPO)-[r1:GpLink {enforced:true}]->(container1)-[r2:Contains*1..]->(c) + OPTIONAL MATCH p2 = (g2:GPO)-[r3:GpLink {enforced:false}]->(container2)-[r4:Contains*1..]->(c) + WHERE NONE (x in NODES(p2) WHERE x.blocksinheritance = true AND x:OU AND NOT (g2)-->(x)) + WITH COLLECT(g1) + COLLECT(g2) AS tempVar1 + UNWIND tempVar1 as tempVar2 + WITH DISTINCT(tempVar2) as GPOs + OPTIONAL MATCH (n)-[{isacl:true}]->(GPOs) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(GPOs) + WITH COLLECT(n) + COLLECT(m) as tempVar1 + UNWIND tempVar1 as tempVar2 + RETURN DISTINCT(tempVar2.name) + ORDER BY tempVar2.name ASC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Exchange Server GPO Controllers: {}", results) + + def da_controllers(self, sheet): + list_query = """MATCH (DAUser)-[:MemberOf*1..]->(g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-512" + OPTIONAL MATCH (n)-[{isacl:true}]->(DAUser) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(DAUser) + WITH COLLECT(n) + COLLECT(m) as tempVar1 + UNWIND tempVar1 AS DAControllers + RETURN DISTINCT(DAControllers.name) + ORDER BY DAControllers.name ASC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Domain Admin Controllers: {}", results) + + def da_sessions(self, sheet): + list_query = """MATCH (c:Computer)-[:HasSession]->()-[:MemberOf*1..]->(g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-512" + RETURN DISTINCT(c.name) + ORDER BY c.name ASC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Computers with DA Sessions: {}", results) + + def gpo_on_da(self, sheet): + list_query = """MATCH (DAUser)-[:MemberOf*1..]->(g:Group {domain:{domain}}) + WHERE g.objectsid ENDS WITH "-512" + OPTIONAL MATCH p1 = (g1:GPO)-[r1:GpLink {enforced:true}]->(container1)-[r2:Contains*1..]->(DAUser) + OPTIONAL MATCH p2 = (g2:GPO)-[r3:GpLink {enforced:false}]->(container2)-[r4:Contains*1..]->(DAUser) + WHERE NONE (x in NODES(p2) WHERE x.blocksinheritance = true AND x:OU AND NOT (g2)-->(x)) + WITH COLLECT(g1) + COLLECT(g2) AS tempVar1 + UNWIND tempVar1 as tempVar2 + WITH DISTINCT(tempVar2) as GPOs + OPTIONAL MATCH (n)-[{isacl:true}]->(GPOs) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(GPOs) + WITH COLLECT(n) + COLLECT(m) as tempVar1 + UNWIND tempVar1 as tempVar2 + RETURN DISTINCT(tempVar2.name) + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "Domain Admin GPO Controllers: {}", results) + + def da_equiv_controllers(self, sheet): + list_query = """MATCH (u:User)-[:MemberOf*1..]->(g:Group {domain:{domain},highvalue:true}) + OPTIONAL MATCH (n)-[{isacl:true}]->(u) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(u) + WITH COLLECT(n) + COLLECT(m) as tempVar + UNWIND tempVar as highValueControllers + RETURN DISTINCT(highValueControllers.name) + ORDER BY highValueControllers.name ASC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "High Value Object Controllers: {}", results) + + def da_equiv_sessions(self, sheet): + list_query = """MATCH (c:Computer)-[:HasSession]->(u:User)-[:MemberOf*1..]->(g:Group {domain:{domain},highvalue:true}) + RETURN DISTINCT(c.name) + ORDER BY c.name ASC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "High Value User Sessions: {}", results) + + def gpo_on_da_equiv(self, sheet): + list_query = """MATCH (u:User)-[:MemberOf*1..]->(g:Group {domain:{domain},highvalue:true}) + OPTIONAL MATCH p1 = (g1:GPO)-[r1:GpLink {enforced:true}]->(container1)-[r2:Contains*1..]->(u) + OPTIONAL MATCH p2 = (g2:GPO)-[r3:GpLink {enforced:false}]->(container2)-[r4:Contains*1..]->(u) + WHERE NONE (x in NODES(p2) WHERE x.blocksinheritance = true AND x:OU AND NOT (g2)-->(x)) + WITH COLLECT(g1) + COLLECT(g2) AS tempVar1 + UNWIND tempVar1 as tempVar2 + WITH DISTINCT(tempVar2) as GPOs + OPTIONAL MATCH (n)-[{isacl:true}]->(GPOs) + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(GPOs) + WITH COLLECT(n) + COLLECT(m) as tempVar1 + UNWIND tempVar1 as tempVar2 + RETURN DISTINCT(tempVar2.name) + ORDER BY tempVar2.name ASC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append(result[0]) + + session.close() + self.write_column_data( + sheet, "High Value User GPO Controllers: {}", results) + +class CrossDomain(object): + def __init__(self, driver, domain, workbook): + self.driver = driver + self.domain = domain + self.col_count = 1 + self.workbook = workbook + + def write_column_data(self, sheet, title, results): + count = len(results) + offset = 1 + font = styles.Font(bold=True) + c = sheet.cell(offset, self.col_count) + c.font = font + sheet.cell(offset, self.col_count, value=title.format(count)) + for i in range(0, count): + sheet.cell(i+offset+1, self.col_count, value=results[i]) + self.col_count += 1 + + def write_single_cell(self, sheet, row, column, text): + sheet.cell(row, column, value=text) + + def do_cross_domain_analysis(self): + func_list = [ + self.foreign_admins, self.foreign_gpo_controllers, self.foreign_user_controllers + ] + sheet = self.workbook._sheets[3] + + for f in func_list: + s = timer() + f(sheet) + print("{} completed in {}s".format(f.__name__, timer() - s)) + + def foreign_admins(self, sheet): + list_query = """MATCH (c:Computer {domain:{domain}}) + OPTIONAL MATCH (n)-[:AdminTo]->(c) + WHERE (n:User OR n:Computer) AND NOT n.domain = c.domain + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) + WHERE (m:User OR m:Computer) AND NOT m.domain = c.domain + WITH COLLECT(n) + COLLECT(m) AS tempVar,c + UNWIND tempVar AS foreignAdmins + RETURN c.name,COUNT(DISTINCT(foreignAdmins)) + ORDER BY COUNT(DISTINCT(foreignAdmins)) DESC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append("{} - {}".format(result[0], result[1])) + + session.close() + self.write_column_data( + sheet, "Computers with Foreign Admins: {}", results) + + def foreign_gpo_controllers(self, sheet): + list_query = """MATCH (g:GPO) + WHERE SPLIT(g.name,'@')[1] = {domain} + OPTIONAL MATCH (n)-[{isacl:true}]->(g) + WHERE (n:User OR n:Computer) AND NOT n.domain = {domain} + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(g) + WHERE (m:User OR m:Computer) AND NOT m.domain = {domain} + WITH COLLECT(n) + COLLECT(m) AS tempVar,g + UNWIND tempVar AS foreignGPOControllers + RETURN g.name,COUNT(DISTINCT(foreignGPOControllers)) + ORDER BY COUNT(DISTINCT(foreignGPOControllers)) DESC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append("{} - {}".format(result[0], result[1])) + + session.close() + self.write_column_data( + sheet, "GPOs with Foreign Controllers: {}", results) + + def foreign_user_controllers(self, sheet): + list_query = """MATCH (g:Group {domain:{domain}}) + OPTIONAL MATCH (n)-[{isacl:true}]->(g) + WHERE (n:User OR n:Computer) AND NOT n.domain = g.domain + OPTIONAL MATCH (m)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(g) + WHERE (m:User OR m:Computer) AND NOT m.domain = g.domain + WITH COLLECT(n) + COLLECT(m) AS tempVar,g + UNWIND tempVar AS foreignGroupControllers + RETURN g.name,COUNT(DISTINCT(foreignGroupControllers)) + ORDER BY COUNT(DISTINCT(foreignGroupControllers)) DESC + """ + + session = self.driver.session() + results = [] + + for result in session.run(list_query, domain=self.domain): + results.append("{} - {}".format(result[0], result[1])) + + session.close() + self.write_column_data( + sheet, "Groups with Foreign Controllers: {}", results) + +class MainMenu(cmd.Cmd): + def __init__(self): + self.m = Messages() + self.url = "bolt://localhost:7687" + self.username = "neo4j" + self.password = "BloodHound" + self.driver = None + self.connected = False + self.num_nodes = 500 + self.filename = "BloodHoundAnalytics.xlsx" + if (len(sys.argv) < 2): + print("No domain specified.") + print("Usage: python {} DOMAINNAME".format(sys.argv[0])) + sys.exit() + self.domain = sys.argv[1].upper() + self.domain_validated = False + + cmd.Cmd.__init__(self) + + def do_changefilename(self, args): + if args == "": + print("No filename specified") + return + self.filename = args + print("Change filename to {}".format(self.filename)) + + def do_changedomain(self, args): + if args == "": + print("No domain specified") + return + self.domain_validated = False + self.domain = args.upper() + self.validate_domain() + + def cmdloop(self): + while True: + self.m.title() + try: + cmd.Cmd.cmdloop(self) + except KeyboardInterrupt: + if self.driver is not None: + self.driver.close() + raise KeyboardInterrupt + + def do_dbconfig(self, args): + "Configure connection settings to the neo4j database" + print("Current Settings:") + print("DB Url: {}".format(self.url)) + print("DB Username: {}".format(self.username)) + print("DB Password: {}".format(self.password)) + print("") + self.url = self.m.input_default("Enter DB URL", self.url) + self.username = self.m.input_default( + "Enter DB Username", self.username) + self.password = self.m.input_default( + "Enter DB Password", self.password) + print("") + print("New Settings:") + print("DB Url: {}".format(self.url)) + print("DB Username: {}".format(self.username)) + print("DB Password: {}".format(self.password)) + print("") + print("Testing DB Connection") + self.test_db_conn() + + def do_exit(self, args): + raise KeyboardInterrupt + + def do_connect(self, args): + self.test_db_conn() + + def test_db_conn(self): + self.connected = False + if self.driver is not None: + self.driver.close() + try: + self.driver = GraphDatabase.driver( + self.url, auth=(self.username, self.password)) + self.connected = True + print("Database Connection Successful!") + self.validate_domain() + except Exception as e: + print(e) + self.connected = False + print("Database Connection Failed. Check your settings.") + + def validate_domain(self): + if not self.connected: + print("Cant validate domain. Connect using connect first") + return + + print("Validating Selected Domain") + session = self.driver.session() + for result in session.run("MATCH (n {domain:{domain}}) RETURN COUNT(n)", domain=self.domain): + if (int(result[0]) > 0): + print("Domain {domain} validated!".format(domain=self.domain)) + self.domain_validated = True + self.create_workbook() + self.create_analytics() + else: + print("Invalid domain specified, use changedomain to pick a new one") + + def do_startanalysis(self, args): + if not self.connected: + print("Not connected to database. Use connect command") + return + + if not self.domain_validated: + print("Invalid domain or not validated. Use changedomain command") + return + + print("----------------------------------") + print("Generating Front Page") + print("----------------------------------") + print("") + self.front.do_front_page_analysis() + print("----------------------------------") + print("Generating Critical Assets Page") + print("----------------------------------") + print("") + self.crit.do_critical_asset_analysis() + print("----------------------------------") + print("Generating Low Hanging Fruit Page") + print("----------------------------------") + print("") + self.low.do_low_hanging_fruit_analysis() + print("----------------------------------") + print("Generating Cross Domain Page") + print("----------------------------------") + print("") + self.cross.do_cross_domain_analysis() + print("Analytics Complete! Saving workbook to {}".format(self.filename)) + self.save_workbook() + + def create_analytics(self): + self.crit = CriticalAssets(self.driver, self.domain, self.workbook) + self.low = LowHangingFruit(self.driver, self.domain, self.workbook) + self.front = FrontPage(self.driver, self.domain, self.workbook) + self.cross = CrossDomain(self.driver, self.domain, self.workbook) + self.save_workbook() + + def create_workbook(self): + wb = Workbook() + ws = wb.active + ws.title = '{domain} Overview'.format(domain=self.domain) + wb.create_sheet(title="Critical Assets") + wb.create_sheet(title="Low Hanging Fruit") + wb.create_sheet(title="Cross Domain Attacks") + self.workbook = wb + + def save_workbook(self): + for worksheet in self.workbook._sheets: + for col in worksheet.columns: + max_length = 0 + column = get_column_letter(col[0].column) # Get the column name + for cell in col: + try: # Necessary to avoid error on empty cells + if len(str(cell.value)) > max_length: + max_length = len(cell.value) + except: + pass + adjusted_width = (max_length + 2) * 1.2 + worksheet.column_dimensions[column].width = adjusted_width + self.workbook.save(self.filename) + + +def main(): + try: + MainMenu().cmdloop() + except KeyboardInterrupt: + print("Exiting") + sys.exit() diff --git a/bloodhoundanalytics.pbix b/bh_tools/data/bloodhoundanalytics.pbix similarity index 100% rename from bloodhoundanalytics.pbix rename to bh_tools/data/bloodhoundanalytics.pbix diff --git a/DBCreator/first.pkl b/bh_tools/data/first.pkl similarity index 100% rename from DBCreator/first.pkl rename to bh_tools/data/first.pkl diff --git a/DBCreator/last.pkl b/bh_tools/data/last.pkl similarity index 100% rename from DBCreator/last.pkl rename to bh_tools/data/last.pkl diff --git a/DBCreator/DBCreator.py b/bh_tools/db_creator.py similarity index 80% rename from DBCreator/DBCreator.py rename to bh_tools/db_creator.py index 1579d03..86a96e6 100644 --- a/DBCreator/DBCreator.py +++ b/bh_tools/db_creator.py @@ -1,4 +1,3 @@ -# Requirements - pip install neo4j-driver # This script is used to create randomized sample databases. # Commands # dbconfig - Set the credentials and URL for the database you're connecting too @@ -10,6 +9,7 @@ # clear_and_generate - Connects to the database, clears the DB, sets the schema, and generates random data from neo4j import GraphDatabase +import pkgutil import cmd import os import sys @@ -53,11 +53,13 @@ def __init__(self): self.domain = "TESTLAB.LOCAL" self.current_time = int(time.time()) self.base_sid = "S-1-5-21-883232822-274137685-4173207997" - with open('first.pkl', 'rb') as f: - self.first_names = pickle.load(f) - with open('last.pkl', 'rb') as f: - self.last_names = pickle.load(f) + self.first_names = pickle.loads( + pkgutil.get_data(__name__, 'data/first.pkl') + ) + self.last_names = pickle.loads( + pkgutil.get_data(__name__, 'data/last.pkl') + ) cmd.Cmd.__init__(self) @@ -170,18 +172,14 @@ def do_cleardb(self, args): print("Resetting Schema") for constraint in session.run("CALL db.constraints"): - session.run("DROP {}".format(constraint['description'])) + session.run("DROP CONSTRAINT {}".format(constraint['name'])) for index in session.run("CALL db.indexes"): - session.run("DROP {}".format(index['description'])) - - session.run( - "CREATE CONSTRAINT id_constraint ON (c:Base) ASSERT c.objectid IS UNIQUE") - session.run("CREATE INDEX name_index FOR (n:Base) ON (n.name)") + session.run("DROP INDEX {}".format(index['name'])) session.close() - print("DB Cleared and Schema Set") + print("DB Cleared and Schema Reset") def test_db_conn(self): self.connected = False @@ -201,7 +199,7 @@ def do_generate(self, args): def do_clear_and_generate(self, args): self.test_db_conn() - self.do_cleardb("a") + self.do_cleardb(args) self.generate_data() def split_seq(self, iterable, size): @@ -250,6 +248,11 @@ def cws(security_id): print("Starting data generation with nodes={}".format(self.num_nodes)) + print("Generating Schema") + session.run( + "CREATE CONSTRAINT id_constraint ON (c:Base) ASSERT c.objectid IS UNIQUE") + session.run("CREATE INDEX name_index FOR (n:Base) ON (n.name)") + print("Populating Standard Nodes") base_statement = "MERGE (n:Base {name: $gname}) SET n:Group, n.objectid=$sid" session.run(f"{base_statement},n.highvalue=true", @@ -274,7 +277,7 @@ def cws(security_id): base_statement = "MERGE (n:Base {name:$gpo, objectid:$guid}) SET n:GPO" session.run(base_statement, gpo=cn("DEFAULT DOMAIN POLICY"), guid=ddp) session.run(base_statement, gpo=cn( - "DEFAULT DOMAIN CONTROLLERS POLICY"), guid=ddp) + "DEFAULT DOMAIN CONTROLLERS POLICY"), guid=ddcp) session.run("MERGE (n:Base {name:$ou, objectid:$guid, blocksInheritance: false}) SET n:OU", ou=cn( "DOMAIN CONTROLLERS"), guid=dcou) @@ -353,10 +356,10 @@ def cws(security_id): if (len(props) > 500): session.run( - 'UNWIND {props} as prop MERGE (n:Base {objectid: prop.id}) SET n:Computer, n += prop.props WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) + 'UNWIND $props as prop MERGE (n:Base {objectid: prop.id}) SET n:Computer, n += prop.props WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) props = [] session.run( - 'UNWIND {props} as prop MERGE (n:Base {objectid:prop.id}) SET n:Computer, n += prop.props WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) + 'UNWIND $props as prop MERGE (n:Base {objectid:prop.id}) SET n:Computer, n += prop.props WITH n MERGE (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) print("Creating Domain Controllers") for state in states: @@ -401,11 +404,11 @@ def cws(security_id): }}) if (len(props) > 500): session.run( - 'UNWIND {props} as prop MERGE (n:Base {objectid:prop.id}) SET n:User, n += prop.props WITH n MATCH (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) + 'UNWIND $props as prop MERGE (n:Base {objectid:prop.id}) SET n:User, n += prop.props WITH n MATCH (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) props = [] session.run( - 'UNWIND {props} as prop MERGE (n:Base {objectid:prop.id}) SET n:User, n += prop.props WITH n MATCH (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) + 'UNWIND $props as prop MERGE (n:Base {objectid:prop.id}) SET n:User, n += prop.props WITH n MATCH (m:Group {name:$gname}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props, gname=group_name) print("Generating Group Nodes") weighted_parts = ["IT"] * 7 + ["HR"] * 13 + \ @@ -420,11 +423,11 @@ def cws(security_id): props.append({'name': group_name, 'id': sid}) if len(props) > 500: session.run( - 'UNWIND {props} as prop MERGE (n:Base {objectid:prop.id}) SET n:Group, n.name=prop.name', props=props) + 'UNWIND $props as prop MERGE (n:Base {objectid:prop.id}) SET n:Group, n.name=prop.name', props=props) props = [] session.run( - 'UNWIND {props} as prop MERGE (n:Base {objectid:prop.id}) SET n:Group, n.name=prop.name', props=props) + 'UNWIND $props as prop MERGE (n:Base {objectid:prop.id}) SET n:Group, n.name=prop.name', props=props) print("Adding Domain Admins to Local Admins of Computers") session.run( @@ -458,11 +461,11 @@ def cws(security_id): if (len(props) > 500): session.run( - 'UNWIND {props} AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) props = [] session.run( - 'UNWIND {props} AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) print("Adding users to groups") props = [] @@ -496,11 +499,11 @@ def cws(security_id): if len(props) > 500: session.run( - 'UNWIND {props} AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) props = [] session.run( - 'UNWIND {props} AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Group {name:prop.b}) WITH n,m MERGE (n)-[:MemberOf]->(m)', props=props) it_users = it_users + das it_users = list(set(it_users)) @@ -534,7 +537,7 @@ def cws(security_id): if len(props) > 500: session.run( - 'UNWIND {props} AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (n)-[:AdminTo]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (n)-[:AdminTo]->(m)', props=props) props = [] for x in super_groups: @@ -543,11 +546,11 @@ def cws(security_id): if len(props) > 500: session.run( - 'UNWIND {props} AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (n)-[:AdminTo]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (n)-[:AdminTo]->(m)', props=props) props = [] session.run( - 'UNWIND {props} AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (n)-[:AdminTo]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:Group {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (n)-[:AdminTo]->(m)', props=props) print("Adding RDP/ExecuteDCOM/AllowedToDelegateTo") count = int(math.floor(len(computers) * .1)) @@ -558,7 +561,7 @@ def cws(security_id): props.append({'a': user, 'b': comp}) session.run( - 'UNWIND {props} AS prop MERGE (n:User {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:CanRDP]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:User {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:CanRDP]->(m)', props=props) props = [] for i in range(0, count): @@ -567,7 +570,7 @@ def cws(security_id): props.append({'a': user, 'b': comp}) session.run( - 'UNWIND {props} AS prop MERGE (n:User {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:ExecuteDCOM]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:User {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:ExecuteDCOM]->(m)', props=props) props = [] for i in range(0, count): @@ -576,7 +579,7 @@ def cws(security_id): props.append({'a': user, 'b': comp}) session.run( - 'UNWIND {props} AS prop MERGE (n:Group {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:CanRDP]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:Group {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:CanRDP]->(m)', props=props) props = [] for i in range(0, count): @@ -585,7 +588,7 @@ def cws(security_id): props.append({'a': user, 'b': comp}) session.run( - 'UNWIND {props} AS prop MERGE (n:Group {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:ExecuteDCOM]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:Group {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:ExecuteDCOM]->(m)', props=props) props = [] for i in range(0, count): @@ -594,7 +597,7 @@ def cws(security_id): props.append({'a': user, 'b': comp}) session.run( - 'UNWIND {props} AS prop MERGE (n:User {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:AllowedToDelegate]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:User {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:AllowedToDelegate]->(m)', props=props) props = [] for i in range(0, count): @@ -605,7 +608,7 @@ def cws(security_id): props.append({'a': user, 'b': comp}) session.run( - 'UNWIND {props} AS prop MERGE (n:Computer {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:AllowedToDelegate]->(m)', props=props) + 'UNWIND $props AS prop MERGE (n:Computer {name: prop.a}) MERGE (m:Computer {name: prop.b}) MERGE (n)-[r:AllowedToDelegate]->(m)', props=props) print("Adding sessions") max_sessions_per_user = int(math.ceil(math.log10(self.num_nodes))) @@ -624,11 +627,11 @@ def cws(security_id): if (len(props) > 500): session.run( - 'UNWIND {props} AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (m)-[:HasSession]->(n)', props=props) + 'UNWIND $props AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (m)-[:HasSession]->(n)', props=props) props = [] session.run( - 'UNWIND {props} AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (m)-[:HasSession]->(n)', props=props) + 'UNWIND $props AS prop MERGE (n:User {name:prop.a}) WITH n,prop MERGE (m:Computer {name:prop.b}) WITH n,m MERGE (m)-[:HasSession]->(n)', props=props) print("Adding Domain Admin ACEs") group_name = "DOMAIN ADMINS@{}".format(self.domain) @@ -638,33 +641,33 @@ def cws(security_id): if len(props) > 500: session.run( - 'UNWIND {props} as prop MATCH (n:Computer {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) + 'UNWIND $props as prop MATCH (n:Computer {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) props = [] session.run( - 'UNWIND {props} as prop MATCH (n:Computer {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) + 'UNWIND $props as prop MATCH (n:Computer {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) for x in users: props.append({'name': x}) if len(props) > 500: session.run( - 'UNWIND {props} as prop MATCH (n:User {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) + 'UNWIND $props as prop MATCH (n:User {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) props = [] session.run( - 'UNWIND {props} as prop MATCH (n:User {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) + 'UNWIND $props as prop MATCH (n:User {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) for x in groups: props.append({'name': x}) if len(props) > 500: session.run( - 'UNWIND {props} as prop MATCH (n:Group {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) + 'UNWIND $props as prop MATCH (n:Group {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) props = [] session.run( - 'UNWIND {props} as prop MATCH (n:Group {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) + 'UNWIND $props as prop MATCH (n:Group {name:prop.name}) WITH n MATCH (m:Group {name:$gname}) WITH m,n MERGE (m)-[r:GenericAll {isacl:true}]->(n)', props=props, gname=group_name) print("Creating OUs") temp_comps = computers @@ -682,11 +685,11 @@ def cws(security_id): props.append({'compname': c, 'ouguid': guid, 'ouname': ouname}) if len(props) > 500: session.run( - 'UNWIND {props} as prop MERGE (n:Computer {name:prop.compname}) WITH n,prop MERGE (m:Base {objectid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) SET m:OU WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) + 'UNWIND $props as prop MERGE (n:Computer {name:prop.compname}) WITH n,prop MERGE (m:Base {objectid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) SET m:OU WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) props = [] session.run( - 'UNWIND {props} as prop MERGE (n:Computer {name:prop.compname}) WITH n,prop MERGE (m:Base {objectid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) SET m:OU WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) + 'UNWIND $props as prop MERGE (n:Computer {name:prop.compname}) WITH n,prop MERGE (m:Base {objectid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) SET m:OU WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) temp_users = users random.shuffle(temp_users) @@ -703,11 +706,11 @@ def cws(security_id): props.append({'username': c, 'ouguid': guid, 'ouname': ouname}) if len(props) > 500: session.run( - 'UNWIND {props} as prop MERGE (n:User {name:prop.username}) WITH n,prop MERGE (m:Base {objectid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) SET m:OU WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) + 'UNWIND $props as prop MERGE (n:User {name:prop.username}) WITH n,prop MERGE (m:Base {objectid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) SET m:OU WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) props = [] session.run( - 'UNWIND {props} as prop MERGE (n:User {name:prop.username}) WITH n,prop MERGE (m:Base {objectid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) SET m:OU WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) + 'UNWIND $props as prop MERGE (n:User {name:prop.username}) WITH n,prop MERGE (m:Base {objectid:prop.ouguid, name:prop.ouname, blocksInheritance: false}) SET m:OU WITH n,m,prop MERGE (m)-[:Contains]->(n)', props=props) props = [] for x in list(ou_guid_map.keys()): @@ -715,7 +718,7 @@ def cws(security_id): props.append({'b': guid}) session.run( - 'UNWIND {props} as prop MERGE (n:OU {objectid:prop.b}) WITH n MERGE (m:Domain {name:$domain}) WITH n,m MERGE (m)-[:Contains]->(n)', props=props, domain=self.domain) + 'UNWIND $props as prop MERGE (n:OU {objectid:prop.b}) WITH n MERGE (m:Domain {name:$domain}) WITH n,m MERGE (m)-[:Contains]->(n)', props=props, domain=self.domain) print("Creating GPOs") @@ -762,35 +765,35 @@ def cws(security_id): p = random.choice(all_principals) p2 = random.choice(gpos) session.run( - 'MERGE (n:Group {name:{group}}) MERGE (m {name:{principal}}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p) - session.run('MERGE (n:Group {name:{group}}) MERGE (m:GPO {name:{principal}}) MERGE (n)-' + + 'MERGE (n:Group {name:$group}) MERGE (m {name:$principal}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p) + session.run('MERGE (n:Group {name:$group}) MERGE (m:GPO {name:$principal}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p2) elif ace == 'AddMember': p = random.choice(it_groups) session.run( - 'MERGE (n:Group {name:{group}}) MERGE (m:Group {name:{principal}}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p) + 'MERGE (n:Group {name:$group}) MERGE (m:Group {name:$principal}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p) elif ace == 'ReadLAPSPassword': p = random.choice(all_principals) targ = random.choice(computers) session.run( - 'MERGE (n {name:{principal}}) MERGE (m:Computer {name:{target}}) MERGE (n)-[r:ReadLAPSPassword]->(m)', target=targ, principal=p) + 'MERGE (n {name:$principal}) MERGE (m:Computer {name:$target}) MERGE (n)-[r:ReadLAPSPassword]->(m)', target=targ, principal=p) else: p = random.choice(it_users) session.run( - 'MERGE (n:Group {name:{group}}) MERGE (m:User {name:{principal}}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p) + 'MERGE (n:Group {name:$group}) MERGE (m:User {name:$principal}) MERGE (n)-' + ace_string + '->(m)', group=i, principal=p) print("Marking some users as Kerberoastable") i = random.randint(10, 20) i = min(i, len(it_users)) for user in random.sample(it_users, i): session.run( - 'MATCH (n:User {name:{user}}) SET n.hasspn=true', user=user) + 'MATCH (n:User {name:$user}) SET n.hasspn=true', user=user) print("Adding unconstrained delegation to a few computers") i = random.randint(10, 20) i = min(i, len(computers)) session.run( - 'MATCH (n:Computer {name:{user}}) SET n.unconstrainteddelegation=true', user=user) + 'MATCH (n:Computer {name:$user}) SET n.unconstrainteddelegation=true', user=user) session.run('MATCH (n:User) SET n.owned=false') session.run('MATCH (n:Computer) SET n.owned=false') @@ -800,10 +803,26 @@ def cws(security_id): print("Database Generation Finished!") +def main(): + menu = MainMenu() + NEO4J_URL = os.environ.get("NEO4J_URL") + NEO4J_USERNAME = os.environ.get("NEO4J_USERNAME") + NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD") + + NEO4J_ENCRYPTION = False + if os.environ.get("NEO4J_ENCRYPTION"): + NEO4J_ENCRYPTION = bool(int(os.environ["NEO4J_ENCRYPTION"])) -if __name__ == '__main__': - try: - MainMenu().cmdloop() - except KeyboardInterrupt: - print("Exiting") - sys.exit() + if NEO4J_URL and NEO4J_USERNAME and NEO4J_PASSWORD: + menu.url = NEO4J_URL + menu.username = NEO4J_USERNAME + menu.password = NEO4J_PASSWORD + menu.use_encryption = NEO4J_ENCRYPTION + menu.do_clear_and_generate(None) + + else: + try: + menu.cmdloop() + except KeyboardInterrupt: + print("Exiting") + sys.exit() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..30a11c8 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,361 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "black" +version = "21.12b0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0,<1" +platformdirs = ">=2" +tomli = ">=0.2.6,<2.0.0" +typing-extensions = [ + {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, + {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, +] + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +python2 = ["typed-ast (>=1.4.3)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.0.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "neo4j" +version = "4.4.1" +description = "Neo4j Bolt driver for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytz = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.4.1" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "3.0.6" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.17.2" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" + +[package.extras] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)"] + +[[package]] +name = "pytz" +version = "2021.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "1.2.3" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.0.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "dev" +optional = false +python-versions = ">=3.6" + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "7ea6d6a2bf7d7b4904e36a38d8cd62edcffa71da0507b237f265309153581adf" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +black = [ + {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, + {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, +] +click = [ + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +flake8 = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +neo4j = [ + {file = "neo4j-4.4.1.tar.gz", hash = "sha256:6b61d6b0a98f775dba7260009be549574b0f0bf6ea2d6a2bd1eb72ecbbed7308"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, + {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] +pyparsing = [ + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.17.2.tar.gz", hash = "sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4"}, + {file = "pytest_asyncio-0.17.2-py3-none-any.whl", hash = "sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d"}, +] +pytz = [ + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, + {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, +] +typing-extensions = [ + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c12e31b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[tool.poetry] +name = "bloodhound-tools" +version = "0.0.2" +description = "" +authors = ["rvazarkar", "andyrobbins", "byt3bl33d3r", "voutilad"] +readme = "README.md" +homepage = "https://github.com/BloodHoundAD/BloodHound-Tools" +repository = "https://github.com/BloodHoundAD/BloodHound-Tools" +exclude = ["tests"] +include = ["LICENSE", "bh_tools/data/*"] +license = "" +classifiers = [ + "Environment :: Console", + "Programming Language :: Python :: 3", + "Topic :: Security", +] + +packages = [ + { include = "bh_tools"} +] + +[tool.poetry.scripts] +bh-dbcreator = 'bh_tools.db_creator:main' +bh-analytics = 'bh_tools.analytics:main' + +[tool.poetry.dependencies] +python = "^3.10" +neo4j = "^4.4.1" + +[tool.poetry.dev-dependencies] +pytest = "*" +pytest-asyncio = "*" +flake8 = "*" +black = "*" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..5e27a7c --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,71 @@ +atomicwrites==1.4.0; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0" \ + --hash=sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197 \ + --hash=sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a +attrs==21.4.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" \ + --hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \ + --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd +black==21.12b0; python_full_version >= "3.6.2" \ + --hash=sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f \ + --hash=sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3 +click==8.0.3; python_version >= "3.6" and python_full_version >= "3.6.2" \ + --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \ + --hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b +colorama==0.4.4; sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows" \ + --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 \ + --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b +flake8==4.0.1; python_version >= "3.6" \ + --hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \ + --hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d +iniconfig==1.1.1; python_version >= "3.7" \ + --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ + --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 +mccabe==0.6.1; python_version >= "3.6" \ + --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ + --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f +mypy-extensions==0.4.3; python_full_version >= "3.6.2" \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 +neo4j==4.4.1; python_version >= "3.6" \ + --hash=sha256:6b61d6b0a98f775dba7260009be549574b0f0bf6ea2d6a2bd1eb72ecbbed7308 +packaging==21.3; python_version >= "3.7" \ + --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 \ + --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb +pathspec==0.9.0; python_full_version >= "3.6.2" \ + --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ + --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 +platformdirs==2.4.1; python_version >= "3.7" and python_full_version >= "3.6.2" \ + --hash=sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca \ + --hash=sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda +pluggy==1.0.0; python_version >= "3.7" \ + --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 \ + --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 +py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" \ + --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 \ + --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 +pycodestyle==2.8.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \ + --hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \ + --hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f +pyflakes==2.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ + --hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e \ + --hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c +pyparsing==3.0.6; python_version >= "3.6" \ + --hash=sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4 \ + --hash=sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81 +pytest-asyncio==0.17.2; python_version >= "3.7" \ + --hash=sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4 \ + --hash=sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d +pytest==6.2.5; python_version >= "3.6" \ + --hash=sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134 \ + --hash=sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89 +pytz==2021.3; python_version >= "3.6" \ + --hash=sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c \ + --hash=sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326 +toml==0.10.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" \ + --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ + --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f +tomli==1.2.3; python_version >= "3.6" and python_full_version >= "3.6.2" \ + --hash=sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c \ + --hash=sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f +typing-extensions==4.0.1 \ + --hash=sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b \ + --hash=sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4227017 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +neo4j==4.4.1; python_version >= "3.6" \ + --hash=sha256:6b61d6b0a98f775dba7260009be549574b0f0bf6ea2d6a2bd1eb72ecbbed7308 +pytz==2021.3; python_version >= "3.6" \ + --hash=sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c \ + --hash=sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e3e3da3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +import logging + +handler = logging.StreamHandler() +handler.setFormatter( + logging.Formatter( + style="{", + fmt="[{name}:{filename}] {levelname} - {message}" + ) +) + +log = logging.getLogger("bh-tools") +log.setLevel(logging.INFO) +log.addHandler(handler)