Skip to content

Commit b1f0368

Browse files
authored
Merge pull request Pennyw0rth#664 from Pennyw0rth/be
Make nxc compatible with bloodhound-ce zip
2 parents 1281b0f + 1afef2f commit b1f0368

6 files changed

Lines changed: 111 additions & 13 deletions

File tree

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/data/nxc.conf

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

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

nxc/helpers/misc.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def called_from_cmd_args():
4141
# Stolen from https://github.com/pydanny/whichcraft/
4242
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
4343
"""Find the path which conforms to the given mode on the PATH for a command.
44-
44+
4545
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.
4646
`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.
4747
Note: This function was backported from the Python 3 source code.
@@ -79,9 +79,67 @@ def _access_check(fn, mode):
7979
if _access_check(name, mode):
8080
return name
8181

82+
def get_bloodhound_info():
83+
"""
84+
Detect which BloodHound package is installed (regular or CE) and its version.
85+
86+
Returns
87+
-------
88+
tuple: (package_name, version, is_ce)
89+
- package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None)
90+
- version: Version string of the installed package (or None if not installed)
91+
- is_ce: Boolean indicating if it's the Community Edition
92+
"""
93+
import importlib.metadata
94+
import importlib.util
95+
96+
# First check if any BloodHound package is available to import
97+
if importlib.util.find_spec("bloodhound") is None:
98+
return None, None, False
99+
100+
# Try to get version info from both possible packages
101+
version = None
102+
package_name = None
103+
is_ce = False
104+
105+
# Check for bloodhound-ce first
106+
try:
107+
version = importlib.metadata.version("bloodhound-ce")
108+
package_name = "bloodhound-ce"
109+
is_ce = True
110+
except importlib.metadata.PackageNotFoundError:
111+
# Check for regular bloodhound
112+
try:
113+
version = importlib.metadata.version("bloodhound")
114+
package_name = "bloodhound"
115+
116+
# Even when installed as 'bloodhound', check if it's actually the CE version
117+
if version and ("ce" in version.lower() or "community" in version.lower()):
118+
is_ce = True
119+
except importlib.metadata.PackageNotFoundError:
120+
# No bloodhound package found via metadata
121+
pass
122+
123+
# In case we can import it but metadata is not working, check the module itself
124+
if not version:
125+
try:
126+
import bloodhound
127+
version = getattr(bloodhound, "__version__", "unknown")
128+
package_name = "bloodhound"
129+
130+
# Check if it's CE based on version string
131+
if "ce" in version.lower() or "community" in version.lower():
132+
is_ce = True
133+
package_name = "bloodhound-ce"
134+
except ImportError:
135+
pass
136+
137+
return package_name, version, is_ce
138+
82139
def detect_if_ip(target):
83140
try:
84141
ip_address(target)
85142
return True
86143
except Exception:
87-
return False
144+
return False
145+

nxc/protocols/ldap.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from nxc.protocols.ldap.kerberos import KerberosAttacks
4444
from nxc.parsers.ldap_results import parse_result_attributes
4545
from nxc.helpers.ntlm_parser import parse_challenge
46+
from nxc.helpers.misc import get_bloodhound_info
4647

4748
ldap_error_status = {
4849
"1": "STATUS_NOT_SUPPORTED",
@@ -423,7 +424,6 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="",
423424
return False
424425

425426
def plaintext_login(self, domain, username, password):
426-
427427
self.username = username
428428
self.password = password
429429
self.domain = domain
@@ -1220,6 +1220,38 @@ def gmsa_decrypt_lsa(self):
12201220
self.logger.fail("No string provided :'(")
12211221

12221222
def bloodhound(self):
1223+
# Check which version is desired
1224+
use_bhce = self.config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False)
1225+
package_name, version, is_ce = get_bloodhound_info()
1226+
1227+
if use_bhce and not is_ce:
1228+
self.logger.fail("⚠️ Configuration Issue Detected ⚠️")
1229+
self.logger.fail("Your configuration has BloodHound-CE enabled, but the regular BloodHound package is installed. Modify your ~/.nxc/nxc.conf config file or follow the instructions:")
1230+
self.logger.fail("Please run the following commands to fix this:")
1231+
self.logger.fail("poetry remove bloodhound-ce # poetry falsely recognizes bloodhound-ce as a the old bloodhound package")
1232+
self.logger.fail("poetry add bloodhound-ce")
1233+
self.logger.fail("")
1234+
1235+
# If using pipx
1236+
self.logger.fail("Or if you installed with pipx:")
1237+
self.logger.fail("pipx runpip netexec uninstall -y bloodhound")
1238+
self.logger.fail("pipx inject netexec bloodhound-ce --force")
1239+
return False
1240+
1241+
elif not use_bhce and is_ce:
1242+
self.logger.fail("⚠️ Configuration Issue Detected ⚠️")
1243+
self.logger.fail("Your configuration has regular BloodHound enabled, but the BloodHound-CE package is installed.")
1244+
self.logger.fail("Please run the following commands to fix this:")
1245+
self.logger.fail("poetry remove bloodhound-ce")
1246+
self.logger.fail("poetry add bloodhound")
1247+
self.logger.fail("")
1248+
1249+
# If using pipx
1250+
self.logger.fail("Or if you installed with pipx:")
1251+
self.logger.fail("pipx runpip netexec uninstall -y bloodhound-ce")
1252+
self.logger.fail("pipx inject netexec bloodhound --force")
1253+
return False
1254+
12231255
auth = ADAuthentication(
12241256
username=self.username,
12251257
password=self.password,
@@ -1239,7 +1271,7 @@ def bloodhound(self):
12391271
)
12401272
collect = resolve_collection_methods("Default" if not self.args.collection else self.args.collection)
12411273
if not collect:
1242-
return
1274+
return None
12431275
self.logger.highlight("Resolved collection methods: " + ", ".join(list(collect)))
12441276

12451277
self.logger.debug("Using DNS to retrieve domain information")

poetry.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ dependencies = [
2121
"argcomplete>=3.1.4",
2222
"asyauth>=0.0.20",
2323
"beautifulsoup4>=4.11,<5",
24-
"bloodhound>=1.8.0",
24+
"bloodhound-ce>=1.8.0",
2525
"dploot>=3.1.0",
2626
"dsinternals>=1.2.4",
2727
"jwt>=1.3.1",

0 commit comments

Comments
 (0)