Skip to content

Commit 9a999e4

Browse files
authored
Merge pull request #8 from P-aLu/main
Add PKI (ADCS) support
2 parents d811dd6 + 76aae0e commit 9a999e4

30 files changed

+2217
-675
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# Changelog
2+
## [0.3.2] - 3/30/2024
3+
### Added
4+
- ADDS model for AD ADCS objects (PKI)
5+
- ACE parser for ADCS objects
6+
27
## [0.3.1] - 1/25/2024
38
### Fixed
49
- GPO JSON file not matching JSON definition for BHCE

bofhound/__main__.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import sys
2+
import os
3+
4+
# Debug helpful
5+
# root = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/..")
6+
# if root not in sys.path:
7+
# sys.path.insert(0, root)
8+
29
import logging
310
import typer
411
import glob
5-
import os
612
from bofhound.parsers import LdapSearchBofParser, Brc4LdapSentinelParser, GenericParser
713
from bofhound.writer import BloodHoundWriter
814
from bofhound.ad import ADDS
915
from bofhound.local import LocalBroker
1016
from bofhound import console
17+
from bofhound.ad.helpers import PropertiesLevel
1118

1219
app = typer.Typer(
1320
add_completion=False,
@@ -18,7 +25,7 @@
1825
def main(
1926
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"),
2027
output_folder: str = typer.Option(".", "--output", "-o", help="Location to export bloodhound files"),
21-
all_properties: bool = typer.Option(False, "--all-properties", "-a", help="Write all properties to BloodHound files (instead of only common properties)"),
28+
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'),
2229
brute_ratel: bool = typer.Option(False, "--brute-ratel", help="Parse logs from Brute Ratel's LDAP Sentinel"),
2330
debug: bool = typer.Option(False, "--debug", help="Enable debug output"),
2431
zip_files: bool = typer.Option(False, "--zip", "-z", help="Compress the JSON output files into a zip archive")):
@@ -96,7 +103,14 @@ def main(
96103
logging.info(f"Parsed {len(ad.domains)} Domains")
97104
logging.info(f"Parsed {len(ad.trustaccounts)} Trust Accounts")
98105
logging.info(f"Parsed {len(ad.ous)} OUs")
106+
logging.info(f"Parsed {len(ad.containers)} Containers")
99107
logging.info(f"Parsed {len(ad.gpos)} GPOs")
108+
logging.info(f"Parsed {len(ad.enterprisecas)} Enterprise CAs")
109+
logging.info(f"Parsed {len(ad.aiacas)} AIA CAs")
110+
logging.info(f"Parsed {len(ad.rootcas)} Root CAs")
111+
logging.info(f"Parsed {len(ad.ntauthstores)} NTAuth Stores")
112+
logging.info(f"Parsed {len(ad.issuancepolicies)} Issuance Policies")
113+
logging.info(f"Parsed {len(ad.certtemplates)} Cert Templates")
100114
logging.info(f"Parsed {len(ad.schemas)} Schemas")
101115
logging.info(f"Parsed {len(ad.CROSSREF_MAP)} Referrals")
102116
logging.info(f"Parsed {len(ad.unknown_objects)} Unknown Objects")
@@ -115,8 +129,15 @@ def main(
115129
users=ad.users,
116130
groups=ad.groups,
117131
ous=ad.ous,
132+
containers=ad.containers,
118133
gpos=ad.gpos,
119-
common_properties_only=(not all_properties),
134+
enterprisecas=ad.enterprisecas,
135+
aiacas=ad.aiacas,
136+
rootcas=ad.rootcas,
137+
ntauthstores=ad.ntauthstores,
138+
issuancepolicies=ad.issuancepolicies,
139+
certtemplates = ad.certtemplates,
140+
properties_level=properties_level,
120141
zip_files=zip_files
121142
)
122143

@@ -125,9 +146,9 @@ def banner():
125146
print('''
126147
_____________________________ __ __ ______ __ __ __ __ _______
127148
| _ / / __ / | ____/| | | | / __ \\ | | | | | \\ | | | \\
128-
| |_) | | | | | | |__ | |__| | | | | | | | | | | \| | | .--. |
149+
| |_) | | | | | | |__ | |__| | | | | | | | | | | \\| | | .--. |
129150
| _ < | | | | | __| | __ | | | | | | | | | | . ` | | | | |
130-
| |_) | | `--' | | | | | | | | `--' | | `--' | | |\ | | '--' |
151+
| |_) | | `--' | | | | | | | | `--' | | `--' | | |\\ | | '--' |
131152
|______/ \\______/ |__| |__| |___\\_\\________\\_\\________\\|__| \\___\\|_________\\
132153
133154
<< @coffeegist | @Tw1sm >>

bofhound/ad/adds.py

Lines changed: 230 additions & 8 deletions
Large diffs are not rendered by default.

bofhound/ad/helpers/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .trustdirection import TrustDirection
2+
from .trusttype import TrustType
3+
from .propertieslevel import PropertiesLevel

bofhound/ad/helpers/cert_utils.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
## From Certipy https://github.com/ly4k/Certipy/blob/main/certipy/lib/constants.py
2+
## https://github.com/ly4k/Certipy/blob/main/certipy/lib/structs.py
3+
## https://github.com/ly4k/Certipy/blob/main/certipy/lib/formatting.py
4+
5+
import enum
6+
import struct
7+
8+
def filetime_to_span(filetime: str) -> int:
9+
(span,) = struct.unpack("<q", filetime)
10+
11+
span *= -0.0000001
12+
13+
return int(span)
14+
15+
def span_to_str(span: int) -> str:
16+
if (span % 31536000 == 0) and (span // 31536000) >= 1:
17+
if (span / 31536000) == 1:
18+
return "1 year"
19+
return "%i years" % (span // 31536000)
20+
elif (span % 2592000 == 0) and (span // 2592000) >= 1:
21+
if (span // 2592000) == 1:
22+
return "1 month"
23+
else:
24+
return "%i months" % (span // 2592000)
25+
elif (span % 604800 == 0) and (span // 604800) >= 1:
26+
if (span / 604800) == 1:
27+
return "1 week"
28+
else:
29+
return "%i weeks" % (span // 604800)
30+
31+
elif (span % 86400 == 0) and (span // 86400) >= 1:
32+
if (span // 86400) == 1:
33+
return "1 day"
34+
else:
35+
return "%i days" % (span // 86400)
36+
elif (span % 3600 == 0) and (span / 3600) >= 1:
37+
if (span // 3600) == 1:
38+
return "1 hour"
39+
else:
40+
return "%i hours" % (span // 3600)
41+
else:
42+
return ""
43+
44+
def to_pascal_case(snake_str: str) -> str:
45+
components = snake_str.split("_")
46+
return "".join(x.title() for x in components)
47+
48+
def _high_bit(value):
49+
"""returns index of highest bit, or -1 if value is zero or negative"""
50+
return value.bit_length() - 1
51+
52+
def _decompose(flag, value):
53+
"""Extract all members from the value."""
54+
# _decompose is only called if the value is not named
55+
not_covered = value
56+
negative = value < 0
57+
members = []
58+
for member in flag:
59+
member_value = member.value
60+
if member_value and member_value & value == member_value:
61+
members.append(member)
62+
not_covered &= ~member_value
63+
if not negative:
64+
tmp = not_covered
65+
while tmp:
66+
flag_value = 2 ** _high_bit(tmp)
67+
if flag_value in flag._value2member_map_:
68+
members.append(flag._value2member_map_[flag_value])
69+
not_covered &= ~flag_value
70+
tmp &= ~flag_value
71+
if not members and value in flag._value2member_map_:
72+
members.append(flag._value2member_map_[value])
73+
members.sort(key=lambda m: m._value_, reverse=True)
74+
if len(members) > 1 and members[0].value == value:
75+
# we have the breakdown, don't need the value member itself
76+
members.pop(0)
77+
return members, not_covered
78+
79+
class PkiCertificateAuthorityFlags(enum.Enum):
80+
NO_TEMPLATE_SUPPORT = 1
81+
SUPPORTS_NT_AUTHENTICATION = 2
82+
CA_SUPPORTS_MANUAL_AUTHENTICATION = 4
83+
CA_SERVERTYPE_ADVANCED = 8
84+
85+
OID_TO_STR_MAP = {
86+
"1.3.6.1.4.1.311.76.6.1": "Windows Update",
87+
"1.3.6.1.4.1.311.10.3.11": "Key Recovery",
88+
"1.3.6.1.4.1.311.10.3.25": "Windows Third Party Application Component",
89+
"1.3.6.1.4.1.311.21.6": "Key Recovery Agent",
90+
"1.3.6.1.4.1.311.10.3.6": "Windows System Component Verification",
91+
"1.3.6.1.4.1.311.61.4.1": "Early Launch Antimalware Drive",
92+
"1.3.6.1.4.1.311.10.3.23": "Windows TCB Component",
93+
"1.3.6.1.4.1.311.61.1.1": "Kernel Mode Code Signing",
94+
"1.3.6.1.4.1.311.10.3.26": "Windows Software Extension Verification",
95+
"2.23.133.8.3": "Attestation Identity Key Certificate",
96+
"1.3.6.1.4.1.311.76.3.1": "Windows Store",
97+
"1.3.6.1.4.1.311.10.6.1": "Key Pack Licenses",
98+
"1.3.6.1.4.1.311.20.2.2": "Smart Card Logon",
99+
"1.3.6.1.5.2.3.5": "KDC Authentication",
100+
"1.3.6.1.5.5.7.3.7": "IP security use",
101+
"1.3.6.1.4.1.311.10.3.8": "Embedded Windows System Component Verification",
102+
"1.3.6.1.4.1.311.10.3.20": "Windows Kits Component",
103+
"1.3.6.1.5.5.7.3.6": "IP security tunnel termination",
104+
"1.3.6.1.4.1.311.10.3.5": "Windows Hardware Driver Verification",
105+
"1.3.6.1.5.5.8.2.2": "IP security IKE intermediate",
106+
"1.3.6.1.4.1.311.10.3.39": "Windows Hardware Driver Extended Verification",
107+
"1.3.6.1.4.1.311.10.6.2": "License Server Verification",
108+
"1.3.6.1.4.1.311.10.3.5.1": "Windows Hardware Driver Attested Verification",
109+
"1.3.6.1.4.1.311.76.5.1": "Dynamic Code Generato",
110+
"1.3.6.1.5.5.7.3.8": "Time Stamping",
111+
"1.3.6.1.4.1.311.10.3.4.1": "File Recovery",
112+
"1.3.6.1.4.1.311.2.6.1": "SpcRelaxedPEMarkerCheck",
113+
"2.23.133.8.1": "Endorsement Key Certificate",
114+
"1.3.6.1.4.1.311.2.6.2": "SpcEncryptedDigestRetryCount",
115+
"1.3.6.1.4.1.311.10.3.4": "Encrypting File System",
116+
"1.3.6.1.5.5.7.3.1": "Server Authentication",
117+
"1.3.6.1.4.1.311.61.5.1": "HAL Extension",
118+
"1.3.6.1.5.5.7.3.4": "Secure Email",
119+
"1.3.6.1.5.5.7.3.5": "IP security end system",
120+
"1.3.6.1.4.1.311.10.3.9": "Root List Signe",
121+
"1.3.6.1.4.1.311.10.3.30": "Disallowed List",
122+
"1.3.6.1.4.1.311.10.3.19": "Revoked List Signe",
123+
"1.3.6.1.4.1.311.10.3.21": "Windows RT Verification",
124+
"1.3.6.1.4.1.311.10.3.10": "Qualified Subordination",
125+
"1.3.6.1.4.1.311.10.3.12": "Document Signing",
126+
"1.3.6.1.4.1.311.10.3.24": "Protected Process Verification",
127+
"1.3.6.1.4.1.311.80.1": "Document Encryption",
128+
"1.3.6.1.4.1.311.10.3.22": "Protected Process Light Verification",
129+
"1.3.6.1.4.1.311.21.19": "Directory Service Email Replication",
130+
"1.3.6.1.4.1.311.21.5": "Private Key Archival",
131+
"1.3.6.1.4.1.311.10.5.1": "Digital Rights",
132+
"1.3.6.1.4.1.311.10.3.27": "Preview Build Signing",
133+
"1.3.6.1.4.1.311.20.2.1": "Certificate Request Agent",
134+
"2.23.133.8.2": "Platform Certificate",
135+
"1.3.6.1.4.1.311.20.1": "CTL Usage",
136+
"1.3.6.1.5.5.7.3.9": "OCSP Signing",
137+
"1.3.6.1.5.5.7.3.3": "Code Signing",
138+
"1.3.6.1.4.1.311.10.3.1": "Microsoft Trust List Signing",
139+
"1.3.6.1.4.1.311.10.3.2": "Microsoft Time Stamping",
140+
"1.3.6.1.4.1.311.76.8.1": "Microsoft Publishe",
141+
"1.3.6.1.5.5.7.3.2": "Client Authentication",
142+
"1.3.6.1.5.2.3.4": "PKINIT Client Authentication",
143+
"1.3.6.1.4.1.311.10.3.13": "Lifetime Signing",
144+
"2.5.29.37.0": "Any Purpose",
145+
"1.3.6.1.4.1.311.64.1.1": "Server Trust",
146+
"1.3.6.1.4.1.311.10.3.7": "OEM Windows System Component Verification",
147+
}
148+
149+
class IntFlag(enum.IntFlag):
150+
def to_list(self):
151+
cls = self.__class__
152+
members, _ = _decompose(cls, self._value_)
153+
return members
154+
155+
def to_str_list(self):
156+
return list(map(lambda x: str(x), self.to_list()))
157+
158+
159+
def __str__(self):
160+
cls = self.__class__
161+
if self._name_ is not None:
162+
return "%s" % (to_pascal_case(self._name_))
163+
members, _ = _decompose(cls, self._value_)
164+
if len(members) == 1 and members[0]._name_ is None:
165+
return "%r" % (members[0]._value_)
166+
else:
167+
return "%s" % (
168+
", ".join(
169+
[to_pascal_case(str(m._name_ or m._value_)) for m in members]
170+
),
171+
)
172+
173+
def __repr__(self):
174+
return str(self)
175+
176+
class MS_PKI_CERTIFICATE_NAME_FLAG(IntFlag):
177+
NONE = 0
178+
ENROLLEE_SUPPLIES_SUBJECT = 1
179+
ADD_EMAIL = 0x00000002
180+
ADD_OBJ_GUID = 0x00000004
181+
OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME = 0x00000008
182+
ADD_DIRECTORY_PATH = 0x00000100
183+
ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME = 0x00010000
184+
SUBJECT_ALT_REQUIRE_DOMAIN_DNS = 0x00400000
185+
SUBJECT_ALT_REQUIRE_SPN = 0x00800000
186+
SUBJECT_ALT_REQUIRE_DIRECTORY_GUID = 0x01000000
187+
SUBJECT_ALT_REQUIRE_UPN = 0x02000000
188+
SUBJECT_ALT_REQUIRE_EMAIL = 0x04000000
189+
SUBJECT_ALT_REQUIRE_DNS = 0x08000000
190+
SUBJECT_REQUIRE_DNS_AS_CN = 0x10000000
191+
SUBJECT_REQUIRE_EMAIL = 0x20000000
192+
SUBJECT_REQUIRE_COMMON_NAME = 0x40000000
193+
SUBJECT_REQUIRE_DIRECTORY_PATH = 0x80000000
194+
195+
def __str__(self):
196+
return self.name
197+
198+
class MS_PKI_PRIVATE_KEY_FLAG(IntFlag):
199+
REQUIRE_PRIVATE_KEY_ARCHIVAL = 0x00000001
200+
EXPORTABLE_KEY = 0x00000010
201+
STRONG_KEY_PROTECTION_REQUIRED = 0x00000020
202+
REQUIRE_ALTERNATE_SIGNATURE_ALGORITHM = 0x00000040
203+
REQUIRE_SAME_KEY_RENEWAL = 0x00000080
204+
USE_LEGACY_PROVIDER = 0x00000100
205+
ATTEST_NONE = 0x00000000
206+
ATTEST_REQUIRED = 0x00002000
207+
ATTEST_PREFERRED = 0x00001000
208+
ATTESTATION_WITHOUT_POLICY = 0x00004000
209+
EK_TRUST_ON_USE = 0x00000200
210+
EK_VALIDATE_CERT = 0x00000400
211+
EK_VALIDATE_KEY = 0x00000800
212+
HELLO_LOGON_KEY = 0x00200000
213+
214+
def __str__(self):
215+
cls = self.__class__
216+
if self._name_ is not None:
217+
return "%s" % (self._name_)
218+
members, _ = _decompose(cls, self._value_)
219+
if len(members) == 1 and members[0]._name_ is None:
220+
return "%r" % (members[0]._value_)
221+
else:
222+
return "%s" % (
223+
", ".join(
224+
[str(m._name_ or m._value_) for m in members]
225+
),
226+
)
227+
228+
class MS_PKI_ENROLLMENT_FLAG(IntFlag):
229+
NONE = 0x00000000
230+
INCLUDE_SYMMETRIC_ALGORITHMS = 0x00000001
231+
PEND_ALL_REQUESTS = 0x00000002
232+
PUBLISH_TO_KRA_CONTAINER = 0x00000004
233+
PUBLISH_TO_DS = 0x00000008
234+
AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE = 0x00000010
235+
AUTO_ENROLLMENT = 0x00000020
236+
CT_FLAG_DOMAIN_AUTHENTICATION_NOT_REQUIRED = 0x80
237+
PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT = 0x00000040
238+
USER_INTERACTION_REQUIRED = 0x00000100
239+
ADD_TEMPLATE_NAME = 0x200
240+
REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE = 0x00000400
241+
ALLOW_ENROLL_ON_BEHALF_OF = 0x00000800
242+
ADD_OCSP_NOCHECK = 0x00001000
243+
ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL = 0x00002000
244+
NOREVOCATIONINFOINISSUEDCERTS = 0x00004000
245+
INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS = 0x00008000
246+
ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT = 0x00010000
247+
ISSUANCE_POLICIES_FROM_REQUEST = 0x00020000
248+
SKIP_AUTO_RENEWAL = 0x00040000
249+
NO_SECURITY_EXTENSION = 0x00080000
250+
251+
def __str__(self):
252+
cls = self.__class__
253+
if self._name_ is not None:
254+
return "%s" % (self._name_)
255+
members, _ = _decompose(cls, self._value_)
256+
if len(members) == 1 and members[0]._name_ is None:
257+
return "%r" % (members[0]._value_)
258+
else:
259+
return "%s" % (
260+
", ".join(
261+
[str(m._name_ or m._value_) for m in members]
262+
),
263+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from enum import Enum
2+
3+
class PropertiesLevel(Enum):
4+
Standard = 'Standard'
5+
Member = 'Member'
6+
All = 'All'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from enum import Enum
2+
3+
class TrustDirection(Enum):
4+
Disabled = 0
5+
Inbound = 1
6+
Outbound = 2
7+
Bidirectional = 3

bofhound/ad/helpers/trusttype.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from enum import Enum
2+
3+
class TrustType(Enum):
4+
ParentChild = 0
5+
CrossLink = 1
6+
Forest = 2
7+
External = 3
8+
Unknown = 4

bofhound/ad/models/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
from .bloodhound_object import BloodHoundObject
66
from .bloodhound_schema import BloodHoundSchema
77
from .bloodhound_ou import BloodHoundOU
8+
from .bloodhound_container import BloodHoundContainer
89
from .bloodhound_gpo import BloodHoundGPO
10+
from .bloodhound_enterpriseca import BloodHoundEnterpriseCA
11+
from .bloodhound_rootca import BloodHoundRootCA
12+
from .bloodhound_aiaca import BloodHoundAIACA
13+
from .bloodhound_ntauthstore import BloodHoundNTAuthStore
14+
from .bloodhound_issuancepolicy import BloodHoundIssuancePolicy
15+
from .bloodhound_certtemplate import BloodHoundCertTemplate
916
from .bloodhound_domaintrust import BloodHoundDomainTrust
1017
from .bloodhound_crossref import BloodHoundCrossRef

0 commit comments

Comments
 (0)