Skip to content

Commit

Permalink
add support for outflankc2
Browse files Browse the repository at this point in the history
  • Loading branch information
Tw1sm committed Dec 17, 2024
1 parent b07051e commit a52225b
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Changelog
## [0.4.5] - 12/17/2024
#### Added
- Support for pasing ldapsearch BOF results within OutflankC2 log files

## [0.4.4] - 12/13/2024
### Fixed
- Addressed [#13](https://github.com/coffeegist/bofhound/issues/13)
Expand Down
24 changes: 15 additions & 9 deletions bofhound/__main__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import sys
import os

# Debug helpful
# root = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/..")
# if root not in sys.path:
# sys.path.insert(0, root)

import logging
import typer
import glob
from bofhound.parsers import LdapSearchBofParser, Brc4LdapSentinelParser, HavocParser, ParserType
from bofhound.parsers import LdapSearchBofParser, Brc4LdapSentinelParser, HavocParser, ParserType, OutflankC2JsonParser
from bofhound.writer import BloodHoundWriter
from bofhound.ad import ADDS
from bofhound.local import LocalBroker
Expand All @@ -18,7 +12,8 @@

app = typer.Typer(
add_completion=False,
rich_markup_mode="rich"
rich_markup_mode="rich",
context_settings={'help_option_names': ['-h', '--help']}
)

@app.command()
Expand Down Expand Up @@ -62,6 +57,11 @@ def main(
logfile_name_format = "Console_*.log"
if input_files == "/opt/cobaltstrike/logs":
input_files = "/opt/havoc/data/loot"

case ParserType.OUTFLANKC2:
logging.debug('Using OutflankC2 parser')
parser = OutflankC2JsonParser
logfile_name_format = "*.json"

case _:
raise ValueError(f"Unknown parser type: {parser}")
Expand Down Expand Up @@ -94,7 +94,13 @@ def main(
status.update(f" [bold] Parsing {log}")
formatted_data = parser.prep_file(log)
new_objects = parser.parse_data(formatted_data)
new_local_objects = parser.parse_local_objects(formatted_data)

# jank insert to reparse outflank logs for local data
if parser == OutflankC2JsonParser:
new_local_objects = parser.parse_local_objects(log)
else:
new_local_objects = parser.parse_local_objects(formatted_data)

logging.debug(f"Parsed {log}")
logging.debug(f"Found {len(new_objects)} objects in {log}")
parsed_ldap_objects.extend(new_objects)
Expand Down
10 changes: 9 additions & 1 deletion bofhound/ad/adds.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,15 @@ def import_objects(self, objects):
self.CROSSREF_MAP[new_crossref.netBiosName] = new_crossref
continue

accountType = int(object.get(ADDS.AT_SAMACCOUNTTYPE, 0))
#
# if samaccounttype comes back as something other
# than int, skip the object
#
try:
accountType = int(object.get(ADDS.AT_SAMACCOUNTTYPE, 0))
except:
continue

target_list = None

# objectClass: top, container
Expand Down
3 changes: 2 additions & 1 deletion bofhound/parsers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .ldap_search_bof import LdapSearchBofParser
from .brc4_ldap_sentinel import Brc4LdapSentinelParser
from .havoc import HavocParser
from .parsertype import ParserType
from .parsertype import ParserType
from .outflankc2 import OutflankC2JsonParser
31 changes: 28 additions & 3 deletions bofhound/parsers/generic_parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .shared_parsers import __all_generic_parsers__
import json
import codecs
from .shared_parsers import __all_generic_parsers__


class GenericParser:

Expand All @@ -8,9 +10,32 @@ def __init__(self):


@staticmethod
def parse_file(file):
def parse_file(file, is_outflankc2=False):
with codecs.open(file, 'r', 'utf-8') as f:
return GenericParser.parse_data(f.read())
if is_outflankc2:
return GenericParser.parse_outflank_file(f.read())
else:
return GenericParser.parse_data(f.read())


@staticmethod
def parse_outflank_file(contents):
parsed_objects = []

for line in contents.splitlines():
event_json = json.loads(line.split('UTC ', 1)[1])

# we only care about task_resonse events
if event_json['event_type'] != 'task_response':
continue

# within task_response events, we only care about tasks with specific BOF names
if event_json['task']['name'].lower() not in ['netsession2', 'netloggedon2', 'regsession', 'netlocalgrouplistmembers2']:
continue

parsed_objects.extend(GenericParser.parse_data(event_json['task']['response']))

return parsed_objects


@staticmethod
Expand Down
115 changes: 115 additions & 0 deletions bofhound/parsers/outflankc2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import re
import codecs
import json
import logging
from bofhound.parsers.generic_parser import GenericParser
from bofhound.parsers import LdapSearchBofParser


#
# Parses ldapsearch BOF objects from Outflank C2 JSON logfiles
# Assumes that the BOF was registered as a command in OC2 named 'ldapserach'
#

class OutflankC2JsonParser(LdapSearchBofParser):
BOFNAME = 'ldapsearch'


@staticmethod
def prep_file(file):
with codecs.open(file, 'r', 'utf-8') as f:
return f.read()


#
# Slightly modified from LdapSearchBofParser to account for
# needing only part of each JSON object, instead of the whole file
#
@staticmethod
def parse_data(contents):
parsed_objects = []
current_object = None
in_result_region = False
previous_attr = None

in_result_region = False

lines = contents.splitlines()
for line in lines:
event_json = json.loads(line.split('UTC ', 1)[1])

# we only care about task_resonse events
if event_json['event_type'] != 'task_response':
continue

# within task_response events, we only care about tasks with the name 'ldapsearch'
if event_json['task']['name'].lower() != OutflankC2JsonParser.BOFNAME:
continue

# now we have a block of ldapsearch data we can parse through for objects
response_lines = event_json['task']['response'].splitlines()
for response_line in response_lines:

is_boundary_line = OutflankC2JsonParser._is_boundary_line(response_line)

if (not in_result_region and
not is_boundary_line):
continue

if (is_boundary_line
and is_boundary_line != OutflankC2JsonParser._COMPLETE_BOUNDARY_LINE):
while True:
try:
next_line = next(response_lines)[1]
remaining_length = OutflankC2JsonParser._is_boundary_line(next_line, is_boundary_line)

if remaining_length:
is_boundary_line = remaining_length
if is_boundary_line == OutflankC2JsonParser._COMPLETE_BOUNDARY_LINE:
break
except:
# probably ran past the end of the iterable
break

if (is_boundary_line):
if not in_result_region:
in_result_region = True
elif current_object is not None:
# self.store_object(current_object)
parsed_objects.append(current_object)
current_object = {}
continue
elif re.match("^(R|r)etr(e|i)(e|i)ved \\d+ results?", response_line):
#self.store_object(current_object)
parsed_objects.append(current_object)
in_result_region = False
current_object = None
continue

data = response_line.split(': ')

try:
# If we previously encountered a control message, we're probably still in the old property
if len(data) == 1:
if previous_attr is not None:
value = current_object[previous_attr] + response_line
else:
data = response_line.split(':')
attr = data[0].strip().lower()
value = ''.join(data[1:]).strip()
previous_attr = attr

current_object[attr] = value

except Exception as e:
logging.debug(f'Error - {str(e)}')

return parsed_objects


#
# Get local groups, sessions, etc by feeding data to GenericParser class
#
@staticmethod
def parse_local_objects(file):
return GenericParser.parse_file(file, is_outflankc2=True)
3 changes: 2 additions & 1 deletion bofhound/parsers/parsertype.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
class ParserType(Enum):
LdapsearchBof = 'ldapsearch'
BRC4 = 'BRC4'
HAVOC = 'Havoc'
HAVOC = 'Havoc'
OUTFLANKC2 = 'OutflankC2'
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "bofhound"
version = "0.4.4"
version = "0.4.5"
description = "Parse output from common sources and transform it into BloodHound-ingestible data"
authors = [
"Adam Brown",
Expand Down

0 comments on commit a52225b

Please sign in to comment.