Skip to content

Commit 3b12e14

Browse files
committed
Merge branch 'main' into presence
2 parents b87f525 + 6a0a985 commit 3b12e14

71 files changed

Lines changed: 735 additions & 347 deletions

Some content is hidden

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

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
## Description
22

33
Please include a summary of the change and which issue is fixed, or what the enhancement does.
4-
Please also include relevant motivation and context.
54
List any dependencies that are required for this change.
65

76
## Type of change
8-
Please delete options that are not relevant.
97
- [ ] Bug fix (non-breaking change which fixes an issue)
108
- [ ] New feature (non-breaking change which adds functionality)
119
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
1210
- [ ] This change requires a documentation update
1311
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
1412

15-
## How Has This Been Tested?
16-
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
17-
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
18-
19-
If you are using poetry, you can easily run tests via:
20-
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
21-
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
13+
## Setup guide for the review
14+
Please provide guidance on what setup is needed to test the introduced changes, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions.
15+
In particular:
16+
- Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer.
17+
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes. E.g. is additional software needed? GPO changes required? Specific registry settings that need to be changed?
2218

2319
## Screenshots (if appropriate):
2420
Screenshots are always nice to have and can give a visual representation of the change.
@@ -29,8 +25,7 @@ If appropriate include before and after screenshot(s) to show which results are
2925
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
3026
- [ ] I have added or updated the tests/e2e_commands.txt file if necessary
3127
- [ ] New and existing e2e tests pass locally with my changes
32-
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
33-
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
28+
- [ ] If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
3429
- [ ] I have performed a self-review of my own code
3530
- [ ] I have commented my code, particularly in hard-to-understand areas
3631
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)

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: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def call_modules(self):
275275
extra={
276276
"module_name": module.name.upper(),
277277
"host": self.host,
278-
"port": self.args.port,
278+
"port": self.port,
279279
"hostname": self.hostname,
280280
},
281281
)
@@ -293,8 +293,7 @@ def call_modules(self):
293293
module.on_admin_login(context, self)
294294

295295
def inc_failed_login(self, username):
296-
global global_failed_logins
297-
global user_failed_logins
296+
global global_failed_logins, user_failed_logins
298297

299298
if username not in user_failed_logins:
300299
user_failed_logins[username] = 0
@@ -304,16 +303,15 @@ def inc_failed_login(self, username):
304303
self.failed_logins += 1
305304

306305
def over_fail_limit(self, username):
307-
global global_failed_logins
308-
global user_failed_logins
306+
global global_failed_logins, user_failed_logins
309307

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

313311
if self.failed_logins == self.args.fail_limit:
314312
return True
315313

316-
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]:
314+
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
317315
return True
318316

319317
return False

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/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+

0 commit comments

Comments
 (0)