Skip to content

Commit ddc2b45

Browse files
authored
Merge pull request #15 from coffeegist/feature/havoc-parsing
Feature/havoc parsing
2 parents bf0eec9 + ff0133f commit ddc2b45

File tree

12 files changed

+4892
-66
lines changed

12 files changed

+4892
-66
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
# Changelog
2+
## [0.4.3] - 10/30/2024
3+
### Added
4+
- Support for pasing ldapsearch BOF results within Havoc log files
5+
6+
### Changed
7+
- Parsers now can inherit from the `LdapSearchBofParser` (since support for other C2s usually still relies on the same BOF) to cut down on code copypasta
8+
- The `GenericParser` class (used to parse local group memberships, session data) is now called from main parsers (`LdapSearchBofParser`, `HavocParser`, etc.) to prevent each logfile from being opened, read, formatted, and parsed twice (each file is now read once and just parsed twice, once for LDAP objects and once for local objects)
9+
210
## [0.4.2] - 10/24/2024
311
### Fixed
412
- Addressed [#12](https://github.com/coffeegist/bofhound/issues/12), an issue with duplicate trusted domain objects

README.md

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,51 +15,88 @@
1515
![PyPi](https://img.shields.io/pypi/v/bofhound?style=for-the-badge)
1616
</h1>
1717

18-
BOFHound is an offline BloodHound ingestor and LDAP result parser compatible with TrustedSec's [ldapsearch BOF](https://github.com/trustedsec/CS-Situational-Awareness-BOF), the Python adaptation, [pyldapsearch](https://github.com/fortalice/pyldapsearch) and Brute Ratel's [LDAP Sentinel](https://bruteratel.com/tabs/commander/badgers/#ldapsentinel).
18+
BOFHound is an offline BloodHound ingestor and LDAP result parser compatible with TrustedSec's [ldapsearch BOF](https://github.com/trustedsec/CS-Situational-Awareness-BOF), the Python adaptation, [pyldapsearch](https://github.com/fortalice/pyldapsearch) and Brute Ratel's [LDAP Sentinel](https://bruteratel.com/tabs/commander/badgers/#ldapsentinel). ldapsearch BOF logs can also be parsed from [Havoc](https://github.com/HavocFramework/Havoc) logs.
1919

2020
By parsing log files generated by the aforementioned tools, BOFHound allows operators to utilize BloodHound's beloved interface while maintaining full control over the LDAP queries being run and the spped at which they are executed. This leaves room for operator discretion to account for potential honeypot accounts, expensive LDAP query thresholds and other detection mechanisms designed with the traditional, automated BloodHound collectors in mind.
2121

2222
Check this [PR](https://github.com/trustedsec/CS-Situational-Awareness-BOF/pull/114) to the SA BOF repo for BOFs that collect session and local group membership data and can be parsed by BOFHound.
2323

24-
### Blog Posts
24+
### References
2525

26-
| Title | Date |
27-
|------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|
26+
Blog Posts:
27+
28+
| Title| Date|
29+
|------|-----|
30+
| [*BOFHound: AD CS Integration*](https://medium.com/specter-ops-posts/bofhound-ad-cs-integration-91b706bc7958) | Oct 30, 2024 |
2831
| [*BOFHound: Session Integration*](https://posts.specterops.io/bofhound-session-integration-7b88b6f18423) | Jan 30, 2024 |
2932
| [*Granularize Your AD Recon Game Part 2*](https://www.fortalicesolutions.com/posts/granularize-your-active-directory-reconnaissance-game-part-2) | Jun 15, 2022 |
3033
| [*Granularize Your AD Recon Game*](https://www.fortalicesolutions.com/posts/bofhound-granularize-your-active-directory-reconnaissance-game) | May 10, 2022 |
3134

35+
Presentations:
36+
37+
| Conference| Materials| Date|
38+
|-----------|----------|-----|
39+
| *SO-CON 2024*| [Slides](https://github.com/SpecterOps/presentations/blob/main/SO-CON%202024/Matt%20Creel%20%26%20Adam%20Brown%20-%20Manually%20Enumerating%20AD%20Attack%20Paths%20with%20BOFHound/Matt%20Creel%20and%20Adam%20Brown%20-%20Manually%20Enumerating%20AD%20Attack%20Paths%20With%20BOFHound%20-%20SO-CON%202024.pdf) & [Recording](https://www.youtube.com/watch?v=Xxm4YktSKVY)| Mar 11, 2024|
40+
3241
# Installation
3342
BOFHound can be installed with `pip3 install bofhound` or by cloning this repository and running `pip3 install .`
3443

3544
# Usage
36-
![](.assets/usage.png)
37-
45+
```
46+
Usage: bofhound [OPTIONS]
47+
48+
Generate BloodHound compatible JSON from logs written by ldapsearch BOF, pyldapsearch and Brute Ratel's
49+
LDAP Sentinel
50+
51+
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────╮
52+
│ --input -i TEXT Directory or file containing logs of ldapsearch │
53+
│ results │
54+
│ [default: /opt/cobaltstrike/logs] │
55+
│ --output -o TEXT Location to export bloodhound files [default: .] │
56+
│ --properties-level -p [Standard|Member|All] Change the verbosity of properties exported to │
57+
│ JSON: Standard - Common BH properties | Member - │
58+
│ Includes MemberOf and Member | All - Includes all │
59+
│ properties │
60+
│ [default: Member] │
61+
│ --parser [ldapsearch|BRC4|Havoc] Parser to use for log files. ldapsearch parser │
62+
│ (default) supports ldapsearch BOF logs from Cobalt │
63+
│ Strike and pyldapsearch logs │
64+
│ [default: ldapsearch] │
65+
│ --debug Enable debug output │
66+
│ --zip -z Compress the JSON output files into a zip archive │
67+
│ --help Show this message and exit. │
68+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯
69+
```
3870

3971
## Example Usage
4072
Parse ldapseach BOF results from Cobalt Strike logs (`/opt/cobaltstrike/logs` by default) to /data/
4173
```
4274
bofhound -o /data/
4375
```
4476

45-
Parse pyldapsearch logs and only include all properties (vs only common properties)
77+
Parse pyldapsearch logs and only include all properties (vs other property levels)
4678
```
47-
bofhound -i ~/.pyldapsearch/logs/ --all-properties
79+
bofhound -i ~/.pyldapsearch/logs/ --properties-level all
4880
```
4981

5082
Parse LDAP Sentinel data from BRc4 logs (will change default input path to `/opt/bruteratel/logs`)
5183
```
52-
bofhound --brute-ratel
84+
bofhound --parser brc4
85+
```
86+
87+
Parse Havoc loot logs (will change default input path to `/opt/havoc/data/loot`) and zip the resulting JSON files
88+
```
89+
bofhound --parser havoc --zip
5390
```
5491

5592
# ldapsearch
93+
Specify `*,ntsecuritydescriptor` as the attributes to return to be able to parse ACL edges. You are missing a ton of data if you don't include this in your `ldapsearch` queries!
5694

57-
## Required Data
95+
#### Required Data
5896
The following attributes are required for proper functionality:
59-
6097
```
6198
samaccounttype
62-
dn
99+
distinguishedname
63100
objectsid
64101
```
65102

@@ -70,28 +107,50 @@ ldapsearch (distinguishedname=DC=windomain,DC=local) *,ntsecuritydescriptor
70107
```
71108

72109
## Example ldapsearch Queries
73-
Get All the Data (Maybe Run BloodHound Instead?)
74110
```
111+
# Get All the Data (Maybe Run BloodHound Instead?)
75112
ldapsearch (objectclass=*) *,ntsecuritydescriptor
76-
```
77113
78-
Retrieve All Schema Info
79-
```
80-
ldapsearch (schemaIDGUID=*) name,schemaidguid -1 "" CN=Schema,CN=Configuration,DC=windomain,DC=local
81-
```
114+
# Retrieve All Schema Info
115+
ldapsearch (schemaIDGUID=*) name,schemaidguid 0 3 "" CN=Schema,CN=Configuration,DC=windomain,DC=local
82116
83-
Retrieve Only the ms-Mcs-AdmPwd schemaIDGUID
84-
```
85-
ldapsearch (name=ms-mcs-admpwd) name,schemaidguid 1 "" CN=Schema,CN=Configuration,DC=windomain,DC=local
86-
```
117+
# Retrieve Only the ms-Mcs-AdmPwd schemaIDGUID
118+
ldapsearch (name=ms-mcs-admpwd) name,schemaidguid 1 3 "" CN=Schema,CN=Configuration,DC=windomain,DC=local
87119
88-
Retrieve Domain NetBIOS Names (useful if collecting data via `netsession2/netloggedon2` BOFs)
89-
```
90-
ldapsearch (netbiosname=*) * 0 "" "CN=Partitions,CN=Configuration,DC=windomain,DC=local"
120+
# Retrieve Domain NetBIOS Names (useful if collecting data via `netsession2/netloggedon2` BOFs)
121+
ldapsearch (netbiosname=*) * 0 3 "" "CN=Partitions,CN=Configuration,DC=windomain,DC=local"
122+
123+
# Unroll a group's nested members
124+
ldapsearch (memberOf:1.2.840.113556.1.4.1941:=CN=TargetGroup,CN=Users,DC=windomain,DC=local) *,ntsecuritydescriptor
125+
126+
# Query domain trusts
127+
ldapsearch (objectclass=trusteddomain) *,ntsecuritydescriptor
128+
129+
# Query across a trust
130+
ldapsearch (objectclass=domain) *,ntsecuritydescriptor 0 3 dc1.trusted.windomain.local "DC=TRUSTED,DC=WINDOMAIN,DC=LOCAL"
131+
132+
#####
133+
# Queries below populate objects for AD CS parsing
134+
135+
# Query the domain object
136+
ldapsearch (objectclass=domain) *,ntsecuritydescriptor
137+
138+
# Query Enterprise CAs
139+
ldapsearch (objectclass=pKIEnrollmentService) *,ntsecuritydescriptor 0 3 “” “CN=Configuration,DC=domain,DC=local”
140+
141+
# Query AIACAs, Root CAs and NTAuth Stores
142+
ldapsearch (objectclass=certificationAuthority) *,ntsecuritydescriptor 0 3 “” “CN=Configuration,DC=domain,DC=local”
143+
144+
# Query Certificate Templates
145+
ldapsearch (objectclass=pKICertificateTemplate) *,ntsecuritydescriptor 0 3 “” “CN=Configuration,DC=domain,DC=local”
146+
147+
# Query Issuance Policies
148+
ldapsearch (objectclass=msPKI-Enterprise-Oid) *,ntsecuritydescriptor 0 3 “” “CN=Configuration,DC=domain,DC=local”
91149
```
92150

93151
# Versions
94152
Check the tagged releases to download a specific version
153+
- v0.4.0 and onward support parsing AD CS objects and edges
95154
- v0.3.0 and onward support session/local group data
96155
- v0.2.1 and onward are compatible with BloodHound CE
97156
- v0.2.0 is the last release supporting BloodHound Legacy
@@ -109,4 +168,4 @@ poetry run bofhound --help
109168
# References and Credits
110169
- [@_dirkjan](https://twitter.com/_dirkjan) (and other contributors) for [BloodHound.py](https://github.com/fox-it/BloodHound.py)
111170
- TrustedSec for [CS-Situational-Awareness-BOF](https://github.com/trustedsec/CS-Situational-Awareness-BOF)
112-
- [P-aLu](https://github.com/P-aLu) for collaboration on bofhoud's [ADCS support](https://github.com/coffeegist/bofhound/pull/8)
171+
- [P-aLu](https://github.com/P-aLu) for collaboration on bofhoud's [AD CS support](https://github.com/coffeegist/bofhound/pull/8)

bofhound/__main__.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import logging
1010
import typer
1111
import glob
12-
from bofhound.parsers import LdapSearchBofParser, Brc4LdapSentinelParser, GenericParser
12+
from bofhound.parsers import LdapSearchBofParser, Brc4LdapSentinelParser, HavocParser, ParserType
1313
from bofhound.writer import BloodHoundWriter
1414
from bofhound.ad import ADDS
1515
from bofhound.local import LocalBroker
@@ -23,10 +23,10 @@
2323

2424
@app.command()
2525
def main(
26-
input_files: str = typer.Option("/opt/cobaltstrike/logs", "--input", "-i", help="Directory or file containing logs of ldapsearch results. Will default to [green]/opt/bruteratel/logs[/] if --brute-ratel is specified"),
26+
input_files: str = typer.Option("/opt/cobaltstrike/logs", "--input", "-i", help="Directory or file containing logs of ldapsearch results"),
2727
output_folder: str = typer.Option(".", "--output", "-o", help="Location to export bloodhound files"),
2828
properties_level: PropertiesLevel = typer.Option(PropertiesLevel.Member.value, "--properties-level", "-p", case_sensitive=False, help='Change the verbosity of properties exported to JSON: Standard - Common BH properties | Member - Includes MemberOf and Member | All - Includes all properties'),
29-
brute_ratel: bool = typer.Option(False, "--brute-ratel", help="Parse logs from Brute Ratel's LDAP Sentinel"),
29+
parser: ParserType = typer.Option(ParserType.LdapsearchBof.value, "--parser", case_sensitive=False, help="Parser to use for log files. ldapsearch parser (default) supports ldapsearch BOF logs from Cobalt Strike and pyldapsearch logs"),
3030
debug: bool = typer.Option(False, "--debug", help="Enable debug output"),
3131
zip_files: bool = typer.Option(False, "--zip", "-z", help="Compress the JSON output files into a zip archive")):
3232
"""
@@ -40,15 +40,32 @@ def main(
4040

4141
banner()
4242

43-
# if BRc4 and input_files is the default, set it to the default BRc4 logs directory
44-
if brute_ratel and input_files == "/opt/cobaltstrike/logs":
45-
input_files = "/opt/bruteratel/logs"
46-
47-
# default to Cobalt logfile naming format
43+
# default to Cobalt logfile naming format
4844
logfile_name_format = "beacon*.log"
49-
if brute_ratel:
50-
logfile_name_format = "b-*.log"
5145

46+
match parser:
47+
48+
case ParserType.LdapsearchBof:
49+
logging.debug('Using ldapsearch parser')
50+
parser = LdapSearchBofParser
51+
52+
case ParserType.BRC4:
53+
logging.debug('Using Brute Ratel parser')
54+
parser = Brc4LdapSentinelParser
55+
logfile_name_format = "b-*.log"
56+
if input_files == "/opt/cobaltstrike/logs":
57+
input_files = "/opt/bruteratel/logs"
58+
59+
case ParserType.HAVOC:
60+
logging.debug('Using Havoc parser')
61+
parser = HavocParser
62+
logfile_name_format = "Console_*.log"
63+
if input_files == "/opt/cobaltstrike/logs":
64+
input_files = "/opt/havoc/data/loot"
65+
66+
case _:
67+
raise ValueError(f"Unknown parser type: {parser}")
68+
5269
if os.path.isfile(input_files):
5370
cs_logs = [input_files]
5471
logging.debug(f"Log file explicitly provided {input_files}")
@@ -70,18 +87,14 @@ def main(
7087
logging.error(f"Could not find {input_files} on disk")
7188
sys.exit(-1)
7289

73-
parser = LdapSearchBofParser
74-
if brute_ratel:
75-
logging.debug('Using Brute Ratel parser')
76-
parser = Brc4LdapSentinelParser
77-
7890
parsed_ldap_objects = []
7991
parsed_local_objects = []
8092
with console.status(f"", spinner="aesthetic") as status:
8193
for log in cs_logs:
8294
status.update(f" [bold] Parsing {log}")
83-
new_objects = parser.parse_file(log)
84-
new_local_objects = GenericParser.parse_file(log)
95+
formatted_data = parser.prep_file(log)
96+
new_objects = parser.parse_data(formatted_data)
97+
new_local_objects = parser.parse_local_objects(formatted_data)
8598
logging.debug(f"Parsed {log}")
8699
logging.debug(f"Found {len(new_objects)} objects in {log}")
87100
parsed_ldap_objects.extend(new_objects)

bofhound/parsers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .ldap_search_bof import LdapSearchBofParser
22
from .brc4_ldap_sentinel import Brc4LdapSentinelParser
3-
from .generic_parser import GenericParser
3+
from .havoc import HavocParser
4+
from .parsertype import ParserType

bofhound/parsers/brc4_ldap_sentinel.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
import codecs
33
import logging
44
from datetime import datetime as dt
5+
from bofhound.parsers import LdapSearchBofParser
56

6-
# If field is empty, DO NOT WRITE IT TO FILE!
7-
8-
class Brc4LdapSentinelParser():
7+
class Brc4LdapSentinelParser(LdapSearchBofParser):
98
# BRC4 LDAP Sentinel currently only queries attributes=["*"] and objectClass
109
# is always the top result. May need to be updated in the future.
1110
START_BOUNDARY = '[+] objectclass :'
@@ -19,12 +18,25 @@ class Brc4LdapSentinelParser():
1918
def __init__(self):
2019
pass #self.objects = []
2120

21+
#
22+
# Legacy, used by test cases for 1 liner
23+
# Removed from __main__.py to avoid duplicating file reads and formatting
24+
#
2225
@staticmethod
2326
def parse_file(file):
24-
2527
with codecs.open(file, 'r', 'utf-8', errors='ignore') as f:
2628
return Brc4LdapSentinelParser.parse_data(f.read())
2729

30+
31+
#
32+
# Replaces parse_file() usage in __main__.py to avoid duplicate file reads
33+
#
34+
@staticmethod
35+
def prep_file(file):
36+
with codecs.open(file, 'r', 'utf-8', errors='ignore') as f:
37+
return f.read()
38+
39+
2840
@staticmethod
2941
def parse_data(contents):
3042
parsed_objects = []

bofhound/parsers/havoc.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import re
2+
import codecs
3+
from bofhound.parsers import LdapSearchBofParser
4+
5+
6+
class HavocParser(LdapSearchBofParser):
7+
8+
@staticmethod
9+
def prep_file(file):
10+
with codecs.open(file, 'r', 'utf-8', errors='ignore') as f:
11+
contents = f.read()
12+
13+
return re.sub(r'\[\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}\] \[\+\] Received Output \[\d+ bytes\]:\n', '', contents)

0 commit comments

Comments
 (0)