Skip to content

Commit 3333781

Browse files
authored
Merge pull request #3 from blacklanternsecurity/upstream-merge-backup
Resolve upstream merge conflicts / Ruff linting fixes / Updates to LDAP protocol
2 parents 541a2ae + 2c69310 commit 3333781

74 files changed

Lines changed: 1650 additions & 1327 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

netexec.spec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ a = Analysis(
2727
'impacket.dcerpc.v5.gkdi',
2828
'impacket.dcerpc.v5.rprn',
2929
'impacket.dcerpc.v5.even',
30+
'impacket.dcerpc.v5.even6',
3031
'impacket.dpapi_ng',
3132
'impacket.tds',
3233
'impacket.version',
@@ -43,6 +44,7 @@ a = Analysis(
4344
'nxc.parsers.ldap_results',
4445
'nxc.helpers.bash',
4546
'nxc.helpers.bloodhound',
47+
'nxc.helpers.even6_parser',
4648
'nxc.helpers.msada_guids',
4749
'nxc.helpers.ntlm_parser',
4850
'paramiko',

nxc/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818

1919
# Check if there are any missing options in the config file
2020
for section in nxc_default_config.sections():
21+
if not nxc_config.has_section(section):
22+
nxc_logger.display(f"Adding missing section '{section}' to nxc.conf")
23+
nxc_config.add_section(section)
24+
with open(path_join(NXC_PATH, "nxc.conf"), "w") as config_file:
25+
nxc_config.write(config_file)
2126
for option in nxc_default_config.options(section):
2227
if not nxc_config.has_option(section, option):
2328
nxc_logger.display(f"Adding missing option '{option}' in config section '{section}' to nxc.conf")

nxc/connection.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,7 @@ def call_modules(self):
294294
module.on_admin_login(context, self)
295295

296296
def inc_failed_login(self, username):
297-
global global_failed_logins
298-
global user_failed_logins
297+
global global_failed_logins, user_failed_logins
299298

300299
if username not in user_failed_logins:
301300
user_failed_logins[username] = 0
@@ -305,8 +304,7 @@ def inc_failed_login(self, username):
305304
self.failed_logins += 1
306305

307306
def over_fail_limit(self, username):
308-
global global_failed_logins
309-
global user_failed_logins
307+
global global_failed_logins, user_failed_logins
310308

311309
if global_failed_logins == self.args.gfail_limit:
312310
return True

nxc/data/nxc.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ bh_port = 7687
1616
bh_user = neo4j
1717
bh_pass = bloodhoundcommunityedition
1818

19+
[BloodHound-CE]
20+
bhce_enabled = True
21+
1922
[Empire]
2023
api_host = 127.0.0.1
2124
api_port = 1337

nxc/database.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ def initialize_db():
111111
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
112112
init_protocol_dbs("default")
113113

114+
114115
def format_host_query(q, filter_term, HostsTable):
115116
"""One annoying thing is that if you search for an ip such as '10.10.10.5',
116117
it will return 10.10.10.5 and 10.10.10.52, so we have to check if its an ip address first
@@ -141,6 +142,7 @@ def format_host_query(q, filter_term, HostsTable):
141142

142143
return q
143144

145+
144146
class BaseDB:
145147
def __init__(self, db_engine):
146148
self.db_engine = db_engine

nxc/helpers/args.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
22
from argparse import Action
33

4+
45
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
56
def _get_help_string(self, action):
67
help_string = action.help

nxc/helpers/even6_parser.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from datetime import datetime
77

8+
89
class Substitution:
910
def __init__(self, buf, offset):
1011
(sub_token, sub_id, sub_type) = struct.unpack_from("<BHB", buf, offset)
@@ -46,6 +47,7 @@ def xml(self, template=None):
4647
else:
4748
print("Unknown value type", hex(value.type))
4849

50+
4951
class Value:
5052
def __init__(self, buf, offset):
5153
token, string_type, length = struct.unpack_from("<BBH", buf, offset)
@@ -56,6 +58,7 @@ def __init__(self, buf, offset):
5658
def xml(self, template=None):
5759
return self._val
5860

61+
5962
class Attribute:
6063
def __init__(self, buf, offset):
6164
struct.unpack_from("<B", buf, offset)
@@ -75,13 +78,15 @@ def xml(self, template=None):
7578
val = self._value.xml(template)
7679
return None if val is None else f'{self._name.val}="{val}"'
7780

81+
7882
class Name:
7983
def __init__(self, buf, offset):
8084
hashs, length = struct.unpack_from("<HH", buf, offset)
8185

8286
self.val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
8387
self.length = 4 + (length + 1) * 2
8488

89+
8590
class Element:
8691
def __init__(self, buf, offset):
8792
token, dependency_id, length = struct.unpack_from("<BHI", buf, offset)
@@ -151,6 +156,7 @@ def xml(self, template=None):
151156
children = (x.xml(template) for x in self._children)
152157
return "<{}{}>{}</{}>".format(self._name.val, attrs, "".join(children), self._name.val)
153158

159+
154160
class ValueSpec:
155161
def __init__(self, buf, offset, value_offset):
156162
self.length, self.type, value_eof = struct.unpack_from("<HBB", buf, offset)
@@ -159,6 +165,7 @@ def __init__(self, buf, offset, value_offset):
159165
if self.type == 0x21:
160166
self.template = BinXML(buf, value_offset)
161167

168+
162169
class TemplateInstance:
163170
def __init__(self, buf, offset):
164171
token, unknown0, guid, length, next_token = struct.unpack_from("<BB16sIB", buf, offset)
@@ -179,6 +186,7 @@ def __init__(self, buf, offset):
179186
def xml(self, template=None):
180187
return self._xml.xml(self)
181188

189+
182190
class BinXML:
183191
def __init__(self, buf, offset):
184192
header_token, major_version, minor_version, flags, next_token = struct.unpack_from("<BBBBB", buf, offset)
@@ -195,6 +203,7 @@ def __init__(self, buf, offset):
195203
def xml(self, template=None):
196204
return self._element.xml(template)
197205

206+
198207
class ResultSet:
199208
def __init__(self, buf):
200209
total_size, header_size, event_offset, bookmark_offset, binxml_size = struct.unpack_from("<IIIII", buf)

nxc/helpers/misc.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import inspect
55
import os
66

7+
from ipaddress import ip_address
8+
79

810
def identify_target_file(target_file):
911
with open(target_file) as target_file_handle:
@@ -22,7 +24,7 @@ def gen_random_string(length=10):
2224

2325

2426
def validate_ntlm(data):
25-
allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE)
27+
allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE)
2628
return bool(allowed.match(data))
2729

2830

@@ -40,7 +42,7 @@ def called_from_cmd_args():
4042
# Stolen from https://github.com/pydanny/whichcraft/
4143
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
4244
"""Find the path which conforms to the given mode on the PATH for a command.
43-
45+
4446
Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file.
4547
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path.
4648
Note: This function was backported from the Python 3 source code.
@@ -77,3 +79,70 @@ def _access_check(fn, mode):
7779
name = os.path.join(p, thefile)
7880
if _access_check(name, mode):
7981
return name
82+
83+
84+
def get_bloodhound_info():
85+
"""
86+
Detect which BloodHound package is installed (regular or CE) and its version.
87+
88+
Returns
89+
-------
90+
tuple: (package_name, version, is_ce)
91+
- package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None)
92+
- version: Version string of the installed package (or None if not installed)
93+
- is_ce: Boolean indicating if it's the Community Edition
94+
"""
95+
import importlib.metadata
96+
import importlib.util
97+
98+
# First check if any BloodHound package is available to import
99+
if importlib.util.find_spec("bloodhound") is None:
100+
return None, None, False
101+
102+
# Try to get version info from both possible packages
103+
version = None
104+
package_name = None
105+
is_ce = False
106+
107+
# Check for bloodhound-ce first
108+
try:
109+
version = importlib.metadata.version("bloodhound-ce")
110+
package_name = "bloodhound-ce"
111+
is_ce = True
112+
except importlib.metadata.PackageNotFoundError:
113+
# Check for regular bloodhound
114+
try:
115+
version = importlib.metadata.version("bloodhound")
116+
package_name = "bloodhound"
117+
118+
# Even when installed as 'bloodhound', check if it's actually the CE version
119+
if version and ("ce" in version.lower() or "community" in version.lower()):
120+
is_ce = True
121+
except importlib.metadata.PackageNotFoundError:
122+
# No bloodhound package found via metadata
123+
pass
124+
125+
# In case we can import it but metadata is not working, check the module itself
126+
if not version:
127+
try:
128+
import bloodhound
129+
version = getattr(bloodhound, "__version__", "unknown")
130+
package_name = "bloodhound"
131+
132+
# Check if it's CE based on version string
133+
if "ce" in version.lower() or "community" in version.lower():
134+
is_ce = True
135+
package_name = "bloodhound-ce"
136+
except ImportError:
137+
pass
138+
139+
return package_name, version, is_ce
140+
141+
142+
def detect_if_ip(target):
143+
try:
144+
ip_address(target)
145+
return True
146+
except Exception:
147+
return False
148+

nxc/helpers/pfx.py

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import secrets
3131
import hashlib
3232
import datetime
33-
import logging
3433
import random
3534
import base64
3635

@@ -47,8 +46,7 @@
4746
from minikerberos.pkinit import PKINIT, DirtyDH
4847
from minikerberos.protocol.constants import NAME_TYPE, PaDataType
4948
from minikerberos.protocol.encryption import Enctype, _enctype_table, Key
50-
from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, \
51-
PA_PAC_REQUEST
49+
from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, PA_PAC_REQUEST
5250
from minikerberos.protocol.rfc4556 import PKAuthenticator, AuthPack, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ
5351

5452
from pyasn1.codec.der import decoder, encoder
@@ -70,6 +68,7 @@
7068
from impacket.krb5.ccache import CCache as impacket_CCache
7169

7270
from nxc.paths import NXC_PATH
71+
from nxc.logger import nxc_logger
7372

7473

7574
class myPKINIT(PKINIT):
@@ -304,8 +303,8 @@ def truncate_key(value, keysize):
304303

305304
key = Key(cipher.enctype, t_key)
306305
enc_data = as_rep["enc-part"]["cipher"]
307-
logging.info("AS-REP encryption key (you might need this later):")
308-
logging.info(hexlify(t_key).decode("utf-8"))
306+
nxc_logger.info("AS-REP encryption key (you might need this later):")
307+
nxc_logger.info(hexlify(t_key).decode("utf-8"))
309308
dec_data = cipher.decrypt(key, 3, enc_data)
310309
encasrep = EncASRepPart.load(dec_data).native
311310
cipher = _enctype_table[int(encasrep["key"]["keytype"])]
@@ -327,34 +326,27 @@ def printPac(self, data, key=None):
327326
for _bufferN in range(pacType["cBuffers"]):
328327
infoBuffer = PAC_INFO_BUFFER(buff)
329328
data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]]
330-
if logging.getLogger().level == logging.DEBUG:
331-
print("TYPE 0x%x" % infoBuffer["ulType"])
329+
nxc_logger.debug(f"TYPE 0x{infoBuffer['ulType']}")
332330
if infoBuffer["ulType"] == 2:
333331
found = True
334332
credinfo = PAC_CREDENTIAL_INFO(data)
335-
if logging.getLogger().level == logging.DEBUG:
336-
credinfo.dump()
337333
newCipher = _enctype_table[credinfo["EncryptionType"]]
338334
out = newCipher.decrypt(key, 16, credinfo["SerializedData"])
339335
type1 = TypeSerialization1(out)
340336
# I'm skipping here 4 bytes with its the ReferentID for the pointer
341337
newdata = out[len(type1) + 4:]
342338
pcc = PAC_CREDENTIAL_DATA(newdata)
343-
if logging.getLogger().level == logging.DEBUG:
344-
pcc.dump()
345339
for cred in pcc["Credentials"]:
346340
credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"]))
347-
if logging.getLogger().level == logging.DEBUG:
348-
credstruct.dump()
349341

350-
logging.info("Recovered NT Hash")
351-
logging.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
342+
nxc_logger.info("Recovered NT Hash")
343+
nxc_logger.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
352344
nthash = hexlify(credstruct["NtPassword"]).decode("utf-8")
353345

354346
buff = buff[len(infoBuffer):]
355347

356348
if not found:
357-
logging.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?")
349+
nxc_logger.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?")
358350
return nthash
359351

360352
def __init__(self, username, domain, kdcHost, key, tgt):
@@ -399,10 +391,8 @@ def dump(self):
399391
authenticator["cusec"] = now.microsecond
400392
authenticator["ctime"] = KerberosTime.to_asn1(now)
401393

402-
if logging.getLogger().level == logging.DEBUG:
403-
logging.debug("AUTHENTICATOR")
404-
print(authenticator.prettyPrint())
405-
print("\n")
394+
nxc_logger.debug("AUTHENTICATOR")
395+
nxc_logger.debug(authenticator.prettyPrint() + "\n")
406396

407397
encodedAuthenticator = encoder.encode(authenticator)
408398

@@ -452,23 +442,18 @@ def dump(self):
452442

453443
myTicket = ticket.to_asn1(TicketAsn1())
454444
seq_set_iter(reqBody, "additional-tickets", (myTicket,))
455-
if logging.getLogger().level == logging.DEBUG:
456-
logging.debug("Final TGS")
457-
print(tgsReq.prettyPrint())
458-
if logging.getLogger().level == logging.DEBUG:
459-
logging.debug("Final TGS")
460-
print(tgsReq.prettyPrint())
445+
nxc_logger.debug("Final TGS")
446+
nxc_logger.debug(tgsReq.prettyPrint())
461447

462448
message = encoder.encode(tgsReq)
463-
logging.info("Requesting ticket to self with PAC")
449+
nxc_logger.info("Requesting ticket to self with PAC")
464450

465451
r = sendReceive(message, self.__domain, self.__kdcHost)
466452

467453
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
468454

469-
if logging.getLogger().level == logging.DEBUG:
470-
logging.debug("TGS_REP")
471-
print(tgs.prettyPrint())
455+
nxc_logger.debug("TGS_REP")
456+
nxc_logger.debug(tgs.prettyPrint())
472457

473458
cipherText = tgs["ticket"]["enc-part"]["cipher"]
474459

0 commit comments

Comments
 (0)