From f4af373e43b56497e4582db774d4e0d6db89af71 Mon Sep 17 00:00:00 2001 From: Stephen Tomkinson Date: Tue, 19 Nov 2024 11:11:42 +0000 Subject: [PATCH] Refactor BloodHound related views out of the main views.py to reduce its size --- event_tracker/urls.py | 15 +- event_tracker/views.py | 355 +----------------------------- event_tracker/views_bloodhound.py | 352 +++++++++++++++++++++++++++++ 3 files changed, 370 insertions(+), 352 deletions(-) create mode 100644 event_tracker/views_bloodhound.py diff --git a/event_tracker/urls.py b/event_tracker/urls.py index c208635..f734708 100644 --- a/event_tracker/urls.py +++ b/event_tracker/urls.py @@ -1,5 +1,6 @@ from django.urls import path +import event_tracker.views_bloodhound import event_tracker.views_credentials from . import views from .views import EventCreateView, EventUpdateView, EventDeleteView, EventListView, EventCloneView, ContextAutocomplete, CSVEventListView, TeamServerCreateView, \ @@ -8,11 +9,12 @@ BeaconExclusionDeleteView, WebhookListView, WebhookCreateView, WebhookUpdateView, WebhookDeleteView, \ CSBeaconsTimelineView, beaconwatch_add, beaconwatch_remove, CSDownloadsListView, CSDownloadToEventView, \ EventLatMoveCloneView, CSLogsListJSON, \ - BloodhoundServerListView, BloodhoundServerCreateView, BloodhoundServerUpdateView, BloodhoundServerDeleteView, \ UserListAutocomplete, HostListAutocomplete, ProcessListAutocomplete, InitialConfigTask, InitialConfigAdmin, \ toggle_event_star, EventTagAutocomplete, TeamServerConfigView, EventStreamListView, EventStreamListJSON, EventStreamUpload, \ EventStreamToEventView, toggle_qs_stars, LimitedEventUpdateView, EventBulkEdit, \ TeamServerHealthCheckView +from .views_bloodhound import BloodhoundServerListView, BloodhoundServerCreateView, BloodhoundServerUpdateView, \ + BloodhoundServerDeleteView from .views_credentials import CredentialListView, CredentialListJson, CredentialCreateView, CredentialUpdateView, \ CredentialDeleteView, credential_wordlist, prefix_wordlist, suffix_wordlist, credential_uncracked_hashes, credential_masklist, prefix_masklist, suffix_masklist @@ -104,11 +106,12 @@ path('bloodhound-server/add/', BloodhoundServerCreateView.as_view(), name='bloodhound-server-add'), path('bloodhound-server//', BloodhoundServerUpdateView.as_view(), name='bloodhound-server-update'), path('bloodhound-server//delete/', BloodhoundServerDeleteView.as_view(), name='bloodhound-server-delete'), - path('bloodhound-server/stats', views.BloodhoundServerStatsView.as_view(), name='bloodhound-stats'), - path('bloodhound-server/ou', views.BloodhoundServerOUView.as_view(), name='bloodhound-ou'), - path('bloodhound-server/node/', views.BloodhoundServerNode.as_view(), name='bloodhound-node'), - path('bloodhound-server/toggle-high-value/', views.toggle_bloodhound_node_highvalue, name='bloodhound-node-toggle-highvalue'), - path('bloodhound-server/ou-api', views.BloodhoundServerOUAPI.as_view(), name='bloodhound-ou-api'), + path('bloodhound-server/stats', event_tracker.views_bloodhound.BloodhoundServerStatsView.as_view(), name='bloodhound-stats'), + path('bloodhound-server/ou', event_tracker.views_bloodhound.BloodhoundServerOUView.as_view(), name='bloodhound-ou'), + path('bloodhound-server/node/', event_tracker.views_bloodhound.BloodhoundServerNode.as_view(), name='bloodhound-node'), + path('bloodhound-server/toggle-high-value/', + event_tracker.views_bloodhound.toggle_bloodhound_node_highvalue, name='bloodhound-node-toggle-highvalue'), + path('bloodhound-server/ou-api', event_tracker.views_bloodhound.BloodhoundServerOUAPI.as_view(), name='bloodhound-ou-api'), path('host-list-autocomplete/', HostListAutocomplete.as_view(), name='host-list-autocomplete'), path('user-list-autocomplete/', UserListAutocomplete.as_view(), name='user-list-autocomplete'), diff --git a/event_tracker/views.py b/event_tracker/views.py index 8b63ab5..13b1767 100644 --- a/event_tracker/views.py +++ b/event_tracker/views.py @@ -5,7 +5,6 @@ from abc import ABC, abstractmethod from io import BytesIO from json import JSONDecodeError -from typing import Optional import json2table import jsonschema @@ -21,18 +20,16 @@ from django.contrib.auth.models import User from django.contrib.staticfiles import finders from django.db import transaction, connection -from django.db.models import Max, Window, F, Q, Value, DateTimeField -from django.db.models.functions import Greatest, Coalesce, Lag, Trunc +from django.db.models import Max, Q, DateTimeField +from django.db.models.functions import Greatest, Coalesce, Trunc from django.forms import inlineformset_factory -from django.http import JsonResponse, HttpResponse, HttpRequest +from django.http import JsonResponse, HttpResponse from django.shortcuts import render, get_object_or_404, redirect from django.template.defaultfilters import truncatechars_html from django.utils import timezone, html from django.utils.dateparse import parse_datetime from django.utils.html import escape from django.utils.safestring import mark_safe -from django.views import View -from django.views.decorators.clickjacking import xframe_options_exempt from django.views.generic import ListView, TemplateView from django_datatables_view.base_datatable_view import BaseDatatableView from djangoplugins.models import ENABLED @@ -42,12 +39,12 @@ from taggit.forms import TagField from taggit.models import Tag -from cobalt_strike_monitor.models import TeamServer, Archive, BeaconLog, Beacon, BeaconExclusion, BeaconPresence, \ +from cobalt_strike_monitor.models import TeamServer, Archive, Beacon, BeaconExclusion, BeaconPresence, \ Download from cobalt_strike_monitor.poll_team_server import healthcheck_teamserver from .models import Task, Event, AttackTactic, AttackTechnique, Context, AttackSubTechnique, FileDistribution, File, \ - EventMapping, Credential, Webhook, BeaconReconnectionWatcher, BloodhoundServer, UserPreferences, \ - ImportedEvent, HashCatMode + EventMapping, Webhook, BeaconReconnectionWatcher, BloodhoundServer, UserPreferences, \ + ImportedEvent from django.urls import reverse_lazy, reverse from django.views.generic.edit import CreateView, DeleteView, UpdateView, FormView from datetime import datetime, timedelta @@ -57,7 +54,8 @@ from .plugins import EventReportingPluginPoint from .signals import cs_beacon_to_context, cs_beaconlog_to_file, notify_webhook_new_beacon, cs_listener_to_context, \ get_driver_for -from .templatetags.custom_tags import render_ts_local, breakonpunctuation +from .templatetags.custom_tags import render_ts_local +from .views_bloodhound import get_bh_users, get_bh_hosts @permission_required('event_tracker.view_task') @@ -389,17 +387,6 @@ def save(self, commit=True): super(FileDistributionForm, self).save(commit=commit) -def get_bh_users(tx, q): - users = set() - - if q: - result = tx.run('match (n) where (n:User or n:AZUser) and toLower(split(n.name, "@")[0]) CONTAINS toLower($q) return split(n.name, "@")[0] limit 50', q=q) - for record in result: - users.add(record[0]) - - return users - - class UserListAutocomplete(autocomplete.Select2ListView): def get_list(self): if not self.request.user.has_perm('event_tracker.change_context'): @@ -422,17 +409,6 @@ def get_list(self): return result -def get_bh_hosts(tx, q): - hosts = set() - - if q: - result = tx.run('match (n) where (n:Computer or n:AZDevice) and toLower(split(n.name, ".")[0]) CONTAINS toLower($q) return split(n.name, ".")[0] limit 50', q=q) - for record in result: - hosts.add(record[0]) - - return hosts - - class HostListAutocomplete(autocomplete.Select2ListView): def get_list(self): if not self.request.user.has_perm('event_tracker.change_context'): @@ -1636,319 +1612,6 @@ def trigger_dummy_webhook(request, webhook_id): return redirect(reverse_lazy('event_tracker:webhook-list')) -# --- Team Server Views --- -class BloodhoundServerListView(PermissionRequiredMixin, ListView): - permission_required = 'event_tracker.view_bloodhoundserver' - model = BloodhoundServer - ordering = ['neo4j_connection_url'] - -def _get_kerberoastables(tx, system: Optional[str]): - if system: - return tx.run(""" - match (n:User) where - n.domain = $system and - n.hasspn=true and - n.enabled=true - OPTIONAL MATCH shortestPath((n:User)-[:MemberOf]->(g:Group)) WHERE g.highvalue=true - return - toLower(n.name), toLower(g.name) - order by n.name""", system=system.upper()).values() - else: - return tx.run(""" - match (n:User) where - n.hasspn=true and - n.enabled=true - OPTIONAL MATCH shortestPath((n:User)-[:MemberOf]->(g:Group)) WHERE g.highvalue=true - return - toLower(n.name), toLower(g.name) - order by n.name""").values() - - -def _get_asreproastables(tx, system: Optional[str]): - if system: - return tx.run(""" - match (n:User) where - n.domain = $system and - n.dontreqpreauth=true and - n.enabled=true - OPTIONAL MATCH shortestPath((n:User)-[:MemberOf]->(g:Group)) WHERE g.highvalue=true - return - toLower(n.name), toLower(g.name) - order by n.name""", system=system.upper()).values() - else: - return tx.run(""" - match (n:User) where - n.dontreqpreauth=true and - n.enabled=true - OPTIONAL MATCH shortestPath((n:User)-[:MemberOf]->(g:Group)) WHERE g.highvalue=true - return - toLower(n.name), toLower(g.name) - order by n.name""").values() - - -def _get_recent_os_distribution(tx, system: Optional[str], most_recent_machine_login): - if system: - return tx.run("match (n:Computer) where n.domain = $system and n.lastlogontimestamp > $most_recent_machine_login - 2628000 return n.operatingsystem as os, count(n.operatingsystem) as freq order by os", - system=system.upper(), most_recent_machine_login=most_recent_machine_login).values() - else: - return tx.run( - "match (n:Computer) where n.lastlogontimestamp > $most_recent_machine_login - 2628000 return n.operatingsystem as os, count(n.operatingsystem) as freq order by os desc", - most_recent_machine_login=most_recent_machine_login).values() - - -def _get_most_recent_machine_login(tx, system: Optional[str]): - if system: - return tx.run("match (n:Computer) where n.domain = $system return max(n.lastlogontimestamp)", system=system.upper()).single()[0] - else: - return tx.run("match (n:Computer) return max(n.lastlogontimestamp)").single()[0] - - -class BloodhoundServerOUView(PermissionRequiredMixin, TemplateView): - permission_required = 'event_tracker.view_bloodhoundserver' - template_name = 'event_tracker/bloodhoundserver_outree.html' - - -def _get_dn_children(tx, parent): - # jsTree uses '#' as the root of the tree, switch it to an empty array to make universal logic work - if parent == ['#']: - parent = [] - - children = tx.run(""" - match (n) where reverse(split(n.distinguishedname, ','))[$parent_len] is not null and - reverse(split(n.distinguishedname, ','))[0..$parent_len] = $parent - return distinct reverse(split(n.distinguishedname, ','))[$parent_len] as nodetext, - reverse(split(n.distinguishedname, ','))[0..$node_len] as nodepath, - count(*) as childcount, - not max(size(split(n.distinguishedname, ',')) > $node_len) as isleaf, - collect(distinct labels(n)) as labs, - true in collect(n.owned) as owned - order by left(nodetext, 3) <> "DC=", isleaf, toLower(split(nodetext, '=')[-1])""", parent=parent, parent_len=len(parent), node_len=len(parent) + 1) - - return children.fetch(100_000) - - -class BloodhoundServerOUAPI(PermissionRequiredMixin, View): - permission_required = 'event_tracker.view_bloodhoundserver' - - def get(self, request: HttpRequest, *args, **kwargs): - result = [] - - for server in BloodhoundServer.objects.filter(active=True).all(): - if driver := get_driver_for(server): - with driver.session() as session: - children = session.execute_read(_get_dn_children, request.GET["id"].split(",")) - - for nodetext, nodepath, childcount, isleaf, types, owned in children: - try: - if nodetext[:2] == "DC": - nodetype = "globe" - elif not isleaf: - nodetype = "folder" - else: - nodetype = types[0][0].lower() - except: - nodetype = "unknown" - - if owned and nodetype in ['user', 'computer', 'folder']: - nodetype += "-owned" - - result.append({'id': nodepath, - 'parent': request.GET["id"], - 'text': f"{nodetext}{' (' + str(childcount) + ')' if not isleaf else ''}", - 'children': not isleaf, - 'type': nodetype, - }) - - if result: - return JsonResponse(result, safe=False) - else: - return JsonResponse([], safe=False) - - -def _get_node_by_dn(tx, dn): - node_rows = tx.run("""match (n) where n.distinguishedname = $dn return n""", dn=dn) - try: - return node_rows.fetch(1)[0]['n'] - except: - return None - -class BloodhoundServerNode(PermissionRequiredMixin, TemplateView): - permission_required = 'event_tracker.view_bloodhoundserver' - template_name = 'event_tracker/bloodhoundserver_node.html' - - @xframe_options_exempt - def get(self, request, *args, **kwargs): - return super().get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - node = None - for server in BloodhoundServer.objects.filter(active=True).all(): - if driver := get_driver_for(server): - with driver.session() as session: - node = session.execute_read(_get_node_by_dn, kwargs["dn"]) - if node: - break - - if not node: - return None - - dict_version = {} - for items in node.items(): - dict_version[items[0]] = items[1] - - return {"node_dict": dict_version, - "dn": kwargs["dn"]} - - -def _toggle_node_highvalue_by_dn(tx, dn, user): - return tx.run( - f'match (n) where n.distinguishedname = $dn set n.highvalue = not n.highvalue, n.highvaluenotes="Marked as High Value by " + $user + " at {datetime.now():%Y-%m-%d %H:%M:%S%z}"', - dn=dn, user=user) - -@permission_required('event_tracker.view_bloodhoundserver') -def toggle_bloodhound_node_highvalue(request, dn): - for server in BloodhoundServer.objects.filter(active=True).all(): - if driver := get_driver_for(server): - with driver.session() as session: - node = session.execute_write(_toggle_node_highvalue_by_dn, dn, request.user.username) - - return redirect(reverse_lazy('event_tracker:bloodhound-node', kwargs={"dn": dn})) - -class BloodhoundServerStatsView(PermissionRequiredMixin, TemplateView): - permission_required = 'event_tracker.view_bloodhoundserver' - template_name = 'event_tracker/bloodhoundserver_stats.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - system = None # Todo make this configurable - - kerberosoatable_hashtypes = [HashCatMode.Kerberos_5_TGSREP_RC4, - HashCatMode.Kerberos_5_TGSREP_AES128, - HashCatMode.Kerberos_5_TGSREP_AES256] - - asreproastable_hashtypes = [HashCatMode.Kerberos_5_ASREP_RC4] - - os_distribution = {} - kerberoastable_users = {} - kerberoastable_ticket_count = 0 - kerberoastable_cracked_count = 0 - kerberoastable_domains = set() - - asreproastable_users = {} - asreproastable_ticket_count = 0 - asreproastable_cracked_count = 0 - asreproastable_domains = set() - - for server in BloodhoundServer.objects.filter(active=True).all(): - if driver := get_driver_for(server): - with driver.session() as session: - try: - # Machine OS - most_recent_machine_login = session.execute_read(_get_most_recent_machine_login, system) - if most_recent_machine_login: - results = session.execute_read(_get_recent_os_distribution, system, - int(most_recent_machine_login)) - for result in results: - if not result[0]: - continue - if result[0] not in os_distribution: - os_distribution[result[0]] = 0 - os_distribution[result[0]] += result[1] - # Kerberoastables - results = session.execute_read(_get_kerberoastables, system) - for result in results: - user_parts = result[0].split('@') - username = user_parts[0].lower() - domain = user_parts[1].lower() - kerberoastable_domains.add(domain) - - credential_obj_query = Credential.objects.filter(account__iexact=username, hash_type__in=kerberosoatable_hashtypes) - if system: - credential_obj_query = credential_obj_query.filter(system=system) - - credential_obj = credential_obj_query.order_by("hash_type").first() - kerberoastable_users[username] = {"credential": credential_obj, - "high_value_group": result[1], - "domain": domain} - - if credential_obj: - kerberoastable_ticket_count += 1 - if credential_obj.secret: - kerberoastable_cracked_count += 1 - - # ASREP roastable users - results = session.execute_read(_get_asreproastables, system) - for result in results: - user_parts = result[0].split('@') - username = user_parts[0].lower() - domain = user_parts[1].lower() - asreproastable_domains.add(domain) - - credential_obj_query = Credential.objects.filter(account__iexact=username, - hash_type__in=asreproastable_hashtypes) - if system: - credential_obj_query = credential_obj_query.filter(system=system) - - credential_obj = credential_obj_query.order_by("hash_type").first() - asreproastable_users[username] = {"credential": credential_obj, - "high_value_group": result[1], - "domain": domain} - if credential_obj: - asreproastable_ticket_count += 1 - if credential_obj.secret: - asreproastable_cracked_count += 1 - except Exception as e: - print(f"Skipping {server} due to {e}") - - context["os_distribution"] = os_distribution - context["kerberoastable_users"] = kerberoastable_users - context["kerberoastable_ticket_count"] = kerberoastable_ticket_count - context["kerberoastable_cracked_count"] = kerberoastable_cracked_count - context["kerberoastable_domain_count"] = len(kerberoastable_domains) - context["asreproastable_users"] = asreproastable_users - context["asreproastable_ticket_count"] = asreproastable_ticket_count - context["asreproastable_cracked_count"] = asreproastable_cracked_count - context["asreproastable_domain_count"] = len(asreproastable_domains) - return context - - -class BloodhoundServerCreateView(PermissionRequiredMixin, CreateView): - permission_required = 'event_tracker.add_bloodhoundserver' - model = BloodhoundServer - fields = ['neo4j_connection_url', 'neo4j_browser_url', 'username', 'password', 'active'] - - def get_success_url(self): - return reverse_lazy('event_tracker:bloodhound-server-list') - - def get_context_data(self, **kwargs): - context = super(BloodhoundServerCreateView, self).get_context_data(**kwargs) - context['action'] = "Create" - return context - - -class BloodhoundServerUpdateView(PermissionRequiredMixin, UpdateView): - permission_required = 'event_tracker.change_bloodhoundserver' - model = BloodhoundServer - fields = ['neo4j_connection_url', 'neo4j_browser_url', 'username', 'password', 'active'] - - def get_success_url(self): - return reverse_lazy('event_tracker:bloodhound-server-list') - - def get_context_data(self, **kwargs): - context = super(BloodhoundServerUpdateView, self).get_context_data(**kwargs) - context['action'] = "Update" - return context - - -class BloodhoundServerDeleteView(PermissionRequiredMixin, DeleteView): - permission_required = 'event_tracker.delete_bloodhoundserver' - model = BloodhoundServer - - def get_success_url(self): - return reverse_lazy('event_tracker:bloodhound-server-list') - - class TaskForm(forms.ModelForm): class Meta: model = Task @@ -2027,4 +1690,4 @@ def toggle_qs_stars(request, task_id): else: qs.update(starred=False) - return redirect(reverse_lazy("event_tracker:event-list", kwargs={"task_id": task_id})) \ No newline at end of file + return redirect(reverse_lazy("event_tracker:event-list", kwargs={"task_id": task_id})) diff --git a/event_tracker/views_bloodhound.py b/event_tracker/views_bloodhound.py new file mode 100644 index 0000000..ce2657e --- /dev/null +++ b/event_tracker/views_bloodhound.py @@ -0,0 +1,352 @@ +from datetime import datetime +from typing import Optional + +from django.contrib.auth.decorators import permission_required +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.http import HttpRequest, JsonResponse +from django.shortcuts import redirect +from django.urls import reverse_lazy +from django.views import View +from django.views.decorators.clickjacking import xframe_options_exempt +from django.views.generic import ListView, TemplateView, CreateView, UpdateView, DeleteView + +from event_tracker.models import BloodhoundServer, HashCatMode, Credential +from event_tracker.signals import get_driver_for + + +def get_bh_users(tx, q): + users = set() + + if q: + result = tx.run('match (n) where (n:User or n:AZUser) and toLower(split(n.name, "@")[0]) CONTAINS toLower($q) return split(n.name, "@")[0] limit 50', q=q) + for record in result: + users.add(record[0]) + + return users + + +def get_bh_hosts(tx, q): + hosts = set() + + if q: + result = tx.run('match (n) where (n:Computer or n:AZDevice) and toLower(split(n.name, ".")[0]) CONTAINS toLower($q) return split(n.name, ".")[0] limit 50', q=q) + for record in result: + hosts.add(record[0]) + + return hosts + + +class BloodhoundServerListView(PermissionRequiredMixin, ListView): + permission_required = 'event_tracker.view_bloodhoundserver' + model = BloodhoundServer + ordering = ['neo4j_connection_url'] + + +def _get_kerberoastables(tx, system: Optional[str]): + if system: + return tx.run(""" + match (n:User) where + n.domain = $system and + n.hasspn=true and + n.enabled=true + OPTIONAL MATCH shortestPath((n:User)-[:MemberOf]->(g:Group)) WHERE g.highvalue=true + return + toLower(n.name), toLower(g.name) + order by n.name""", system=system.upper()).values() + else: + return tx.run(""" + match (n:User) where + n.hasspn=true and + n.enabled=true + OPTIONAL MATCH shortestPath((n:User)-[:MemberOf]->(g:Group)) WHERE g.highvalue=true + return + toLower(n.name), toLower(g.name) + order by n.name""").values() + + +def _get_asreproastables(tx, system: Optional[str]): + if system: + return tx.run(""" + match (n:User) where + n.domain = $system and + n.dontreqpreauth=true and + n.enabled=true + OPTIONAL MATCH shortestPath((n:User)-[:MemberOf]->(g:Group)) WHERE g.highvalue=true + return + toLower(n.name), toLower(g.name) + order by n.name""", system=system.upper()).values() + else: + return tx.run(""" + match (n:User) where + n.dontreqpreauth=true and + n.enabled=true + OPTIONAL MATCH shortestPath((n:User)-[:MemberOf]->(g:Group)) WHERE g.highvalue=true + return + toLower(n.name), toLower(g.name) + order by n.name""").values() + + +def _get_recent_os_distribution(tx, system: Optional[str], most_recent_machine_login): + if system: + return tx.run("match (n:Computer) where n.domain = $system and n.lastlogontimestamp > $most_recent_machine_login - 2628000 return n.operatingsystem as os, count(n.operatingsystem) as freq order by os", + system=system.upper(), most_recent_machine_login=most_recent_machine_login).values() + else: + return tx.run( + "match (n:Computer) where n.lastlogontimestamp > $most_recent_machine_login - 2628000 return n.operatingsystem as os, count(n.operatingsystem) as freq order by os desc", + most_recent_machine_login=most_recent_machine_login).values() + + +def _get_most_recent_machine_login(tx, system: Optional[str]): + if system: + return tx.run("match (n:Computer) where n.domain = $system return max(n.lastlogontimestamp)", system=system.upper()).single()[0] + else: + return tx.run("match (n:Computer) return max(n.lastlogontimestamp)").single()[0] + + +class BloodhoundServerOUView(PermissionRequiredMixin, TemplateView): + permission_required = 'event_tracker.view_bloodhoundserver' + template_name = 'event_tracker/bloodhoundserver_outree.html' + + +def _get_dn_children(tx, parent): + # jsTree uses '#' as the root of the tree, switch it to an empty array to make universal logic work + if parent == ['#']: + parent = [] + + children = tx.run(""" + match (n) where reverse(split(n.distinguishedname, ','))[$parent_len] is not null and + reverse(split(n.distinguishedname, ','))[0..$parent_len] = $parent + return distinct reverse(split(n.distinguishedname, ','))[$parent_len] as nodetext, + reverse(split(n.distinguishedname, ','))[0..$node_len] as nodepath, + count(*) as childcount, + not max(size(split(n.distinguishedname, ',')) > $node_len) as isleaf, + collect(distinct labels(n)) as labs, + true in collect(n.owned) as owned + order by left(nodetext, 3) <> "DC=", isleaf, toLower(split(nodetext, '=')[-1])""", parent=parent, parent_len=len(parent), node_len=len(parent) + 1) + + return children.fetch(100_000) + + +class BloodhoundServerOUAPI(PermissionRequiredMixin, View): + permission_required = 'event_tracker.view_bloodhoundserver' + + def get(self, request: HttpRequest, *args, **kwargs): + result = [] + + for server in BloodhoundServer.objects.filter(active=True).all(): + if driver := get_driver_for(server): + with driver.session() as session: + children = session.execute_read(_get_dn_children, request.GET["id"].split(",")) + + for nodetext, nodepath, childcount, isleaf, types, owned in children: + try: + if nodetext[:2] == "DC": + nodetype = "globe" + elif not isleaf: + nodetype = "folder" + else: + nodetype = types[0][0].lower() + except: + nodetype = "unknown" + + if owned and nodetype in ['user', 'computer', 'folder']: + nodetype += "-owned" + + result.append({'id': nodepath, + 'parent': request.GET["id"], + 'text': f"{nodetext}{' (' + str(childcount) + ')' if not isleaf else ''}", + 'children': not isleaf, + 'type': nodetype, + }) + + if result: + return JsonResponse(result, safe=False) + else: + return JsonResponse([], safe=False) + + +def _get_node_by_dn(tx, dn): + node_rows = tx.run("""match (n) where n.distinguishedname = $dn return n""", dn=dn) + try: + return node_rows.fetch(1)[0]['n'] + except: + return None + + +class BloodhoundServerNode(PermissionRequiredMixin, TemplateView): + permission_required = 'event_tracker.view_bloodhoundserver' + template_name = 'event_tracker/bloodhoundserver_node.html' + + @xframe_options_exempt + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + node = None + for server in BloodhoundServer.objects.filter(active=True).all(): + if driver := get_driver_for(server): + with driver.session() as session: + node = session.execute_read(_get_node_by_dn, kwargs["dn"]) + if node: + break + + if not node: + return None + + dict_version = {} + for items in node.items(): + dict_version[items[0]] = items[1] + + return {"node_dict": dict_version, + "dn": kwargs["dn"]} + + +def _toggle_node_highvalue_by_dn(tx, dn, user): + return tx.run( + f'match (n) where n.distinguishedname = $dn set n.highvalue = not n.highvalue, n.highvaluenotes="Marked as High Value by " + $user + " at {datetime.now():%Y-%m-%d %H:%M:%S%z}"', + dn=dn, user=user) + + +@permission_required('event_tracker.view_bloodhoundserver') +def toggle_bloodhound_node_highvalue(request, dn): + for server in BloodhoundServer.objects.filter(active=True).all(): + if driver := get_driver_for(server): + with driver.session() as session: + node = session.execute_write(_toggle_node_highvalue_by_dn, dn, request.user.username) + + return redirect(reverse_lazy('event_tracker:bloodhound-node', kwargs={"dn": dn})) + + +class BloodhoundServerStatsView(PermissionRequiredMixin, TemplateView): + permission_required = 'event_tracker.view_bloodhoundserver' + template_name = 'event_tracker/bloodhoundserver_stats.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + system = None # Todo make this configurable + + kerberosoatable_hashtypes = [HashCatMode.Kerberos_5_TGSREP_RC4, + HashCatMode.Kerberos_5_TGSREP_AES128, + HashCatMode.Kerberos_5_TGSREP_AES256] + + asreproastable_hashtypes = [HashCatMode.Kerberos_5_ASREP_RC4] + + os_distribution = {} + kerberoastable_users = {} + kerberoastable_ticket_count = 0 + kerberoastable_cracked_count = 0 + kerberoastable_domains = set() + + asreproastable_users = {} + asreproastable_ticket_count = 0 + asreproastable_cracked_count = 0 + asreproastable_domains = set() + + for server in BloodhoundServer.objects.filter(active=True).all(): + if driver := get_driver_for(server): + with driver.session() as session: + try: + # Machine OS + most_recent_machine_login = session.execute_read(_get_most_recent_machine_login, system) + if most_recent_machine_login: + results = session.execute_read(_get_recent_os_distribution, system, + int(most_recent_machine_login)) + for result in results: + if not result[0]: + continue + if result[0] not in os_distribution: + os_distribution[result[0]] = 0 + os_distribution[result[0]] += result[1] + # Kerberoastables + results = session.execute_read(_get_kerberoastables, system) + for result in results: + user_parts = result[0].split('@') + username = user_parts[0].lower() + domain = user_parts[1].lower() + kerberoastable_domains.add(domain) + + credential_obj_query = Credential.objects.filter(account__iexact=username, hash_type__in=kerberosoatable_hashtypes) + if system: + credential_obj_query = credential_obj_query.filter(system=system) + + credential_obj = credential_obj_query.order_by("hash_type").first() + kerberoastable_users[username] = {"credential": credential_obj, + "high_value_group": result[1], + "domain": domain} + + if credential_obj: + kerberoastable_ticket_count += 1 + if credential_obj.secret: + kerberoastable_cracked_count += 1 + + # ASREP roastable users + results = session.execute_read(_get_asreproastables, system) + for result in results: + user_parts = result[0].split('@') + username = user_parts[0].lower() + domain = user_parts[1].lower() + asreproastable_domains.add(domain) + + credential_obj_query = Credential.objects.filter(account__iexact=username, + hash_type__in=asreproastable_hashtypes) + if system: + credential_obj_query = credential_obj_query.filter(system=system) + + credential_obj = credential_obj_query.order_by("hash_type").first() + asreproastable_users[username] = {"credential": credential_obj, + "high_value_group": result[1], + "domain": domain} + if credential_obj: + asreproastable_ticket_count += 1 + if credential_obj.secret: + asreproastable_cracked_count += 1 + except Exception as e: + print(f"Skipping {server} due to {e}") + + context["os_distribution"] = os_distribution + context["kerberoastable_users"] = kerberoastable_users + context["kerberoastable_ticket_count"] = kerberoastable_ticket_count + context["kerberoastable_cracked_count"] = kerberoastable_cracked_count + context["kerberoastable_domain_count"] = len(kerberoastable_domains) + context["asreproastable_users"] = asreproastable_users + context["asreproastable_ticket_count"] = asreproastable_ticket_count + context["asreproastable_cracked_count"] = asreproastable_cracked_count + context["asreproastable_domain_count"] = len(asreproastable_domains) + return context + + +class BloodhoundServerCreateView(PermissionRequiredMixin, CreateView): + permission_required = 'event_tracker.add_bloodhoundserver' + model = BloodhoundServer + fields = ['neo4j_connection_url', 'neo4j_browser_url', 'username', 'password', 'active'] + + def get_success_url(self): + return reverse_lazy('event_tracker:bloodhound-server-list') + + def get_context_data(self, **kwargs): + context = super(BloodhoundServerCreateView, self).get_context_data(**kwargs) + context['action'] = "Create" + return context + + +class BloodhoundServerUpdateView(PermissionRequiredMixin, UpdateView): + permission_required = 'event_tracker.change_bloodhoundserver' + model = BloodhoundServer + fields = ['neo4j_connection_url', 'neo4j_browser_url', 'username', 'password', 'active'] + + def get_success_url(self): + return reverse_lazy('event_tracker:bloodhound-server-list') + + def get_context_data(self, **kwargs): + context = super(BloodhoundServerUpdateView, self).get_context_data(**kwargs) + context['action'] = "Update" + return context + + +class BloodhoundServerDeleteView(PermissionRequiredMixin, DeleteView): + permission_required = 'event_tracker.delete_bloodhoundserver' + model = BloodhoundServer + + def get_success_url(self): + return reverse_lazy('event_tracker:bloodhound-server-list')