Skip to content

Commit 72eeb7e

Browse files
authored
Merge branch 'main' into main
2 parents 68c197d + cd0d3c8 commit 72eeb7e

42 files changed

Lines changed: 909 additions & 484 deletions

Some content is hidden

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

.github/workflows/lint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
name: Lint Python code with ruff
22
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
33

4-
on: [push, pull_request]
4+
on:
5+
push:
56

67
jobs:
78
lint:
8-
name: Lint Python code with ruff
99
runs-on: ubuntu-latest
1010
if:
1111
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository

.github/workflows/test.yml

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,48 @@
11
name: NetExec Tests
22

33
on:
4+
workflow_dispatch:
45
pull_request_review:
56
types: [submitted]
67

78
jobs:
89
build:
9-
name: NetExec Tests for Py${{ matrix.python-version }}
10+
name: Test for Py${{ matrix.python-version }}
11+
if: github.event.review.state == 'APPROVED'
1012
runs-on: ${{ matrix.os }}
1113
strategy:
1214
max-parallel: 5
1315
matrix:
1416
os: [ubuntu-latest]
15-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
17+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
1618
steps:
17-
- uses: actions/checkout@v3
18-
- name: NetExec set up python on ${{ matrix.os }}
19-
uses: actions/setup-python@v4
20-
with:
21-
python-version: ${{ matrix.python-version }}
22-
- name: Install poetry
23-
run: |
24-
pipx install poetry --python python${{ matrix.python-version }}
25-
poetry --version
26-
poetry env info
27-
- name: Install libraries with dev group
28-
run: |
29-
poetry install --with dev
30-
- name: Run the e2e test
31-
run: |
32-
poetry run pytest tests
19+
- uses: actions/checkout@v3
20+
- name: Install poetry
21+
run: |
22+
pipx install poetry
23+
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
24+
uses: actions/setup-python@v4
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
cache: poetry
28+
cache-dependency-path: poetry.lock
29+
- name: Install poetry
30+
run: |
31+
pipx install poetry --python python${{ matrix.python-version }}
32+
poetry --version
33+
poetry env info
34+
- name: Install libraries with dev group
35+
run: |
36+
poetry install --with dev
37+
- name: Load every protocol and module
38+
run: |
39+
poetry run netexec winrm 127.0.0.1
40+
poetry run netexec vnc 127.0.0.1
41+
poetry run netexec smb 127.0.0.1
42+
poetry run netexec ldap 127.0.0.1
43+
poetry run netexec wmi 127.0.0.1
44+
poetry run netexec rdp 127.0.0.1
45+
poetry run netexec mssql 127.0.0.1
46+
poetry run netexec ssh 127.0.0.1
47+
poetry run netexec ftp 127.0.0.1
48+
poetry run netexec smb 127.0.0.1 -M veeam

nxc/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ def gen_cli_args():
1717
VERSION = importlib.metadata.version("netexec")
1818
CODENAME = "nxc4u"
1919

20-
parser = argparse.ArgumentParser(description=f"""
20+
parser = argparse.ArgumentParser(description=rf"""
2121
. .
2222
.| |. _ _ _ _____
2323
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
24-
\\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
24+
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
2525
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
2626
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
2727
ॱ \ / ॱ
@@ -36,7 +36,7 @@ def gen_cli_args():
3636
{highlight('Codename', 'red')}: {highlight(CODENAME)}
3737
""", formatter_class=RawTextHelpFormatter)
3838

39-
parser.add_argument("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)")
39+
parser.add_argument("-t", type=int, dest="threads", default=256, help="set how many concurrent threads to use (default: 256)")
4040
parser.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread (default: None)")
4141
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each connection (default: None)")
4242
parser.add_argument("--no-progress", action="store_true", help="Not displaying progress bar during scan")

nxc/connection.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ def __init__(self, args, db, host):
8787
self.port = self.args.port
8888
self.conn = None
8989
self.admin_privs = False
90-
self.password = None
91-
self.username = None
90+
self.password = ""
91+
self.username = ""
9292
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
9393
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
9494
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
@@ -121,7 +121,10 @@ def __init__(self, args, db, host):
121121
try:
122122
self.proto_flow()
123123
except Exception as e:
124-
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
124+
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
125+
self.logger.error(f"Exception while calling proto_flow() on target {self.host}: {e}")
126+
else:
127+
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
125128

126129
@staticmethod
127130
def proto_args(std_parser, module_parser):
@@ -163,7 +166,9 @@ def hash_login(self, domain, username, ntlm_hash):
163166
def proto_flow(self):
164167
self.logger.debug("Kicking off proto_flow")
165168
self.proto_logger()
166-
if self.create_conn_obj():
169+
if not self.create_conn_obj():
170+
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
171+
else:
167172
self.logger.debug("Created connection object")
168173
self.enum_host_info()
169174
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):

nxc/data/nxc.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ bh_enabled = False
1313
bh_uri = 127.0.0.1
1414
bh_port = 7687
1515
bh_user = neo4j
16-
bh_pass = neo4j
16+
bh_pass = bloodhoundcommunityedition
1717

1818
[Empire]
1919
api_host = 127.0.0.1

nxc/helpers/bloodhound.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ def add_user_bh(user, domain, logger, config):
5252
_add_with_domain(user_info, domain, tx, logger)
5353
except AuthError:
5454
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
55-
return
55+
exit()
5656
except ServiceUnavailable:
5757
logger.fail(f"Neo4J does not seem to be available on {uri}.")
58-
return
58+
exit()
5959
except Exception as e:
6060
logger.fail(f"Unexpected error with Neo4J: {e}")
61-
return
62-
driver.close()
61+
finally:
62+
driver.close()
6363

6464

6565
def _add_with_domain(user_info, domain, tx, logger):

nxc/loaders/moduleloader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def get_module_info(self, module_path):
9595
module_spec = spec.loader.load_module().NXCModule
9696

9797
module = {
98-
f"{module_spec.name.lower()}": {
98+
f"{module_spec.name}": {
9999
"path": module_path,
100100
"description": module_spec.description,
101101
"options": module_spec.options.__doc__,

nxc/logger.py

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,61 @@
99
from datetime import datetime
1010
from rich.text import Text
1111
from rich.logging import RichHandler
12+
import functools
13+
import inspect
14+
15+
16+
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
17+
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
18+
temp_logger = logging.getLogger("temp")
19+
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
20+
handler = SmartDebugRichHandler(formatter=formatter)
21+
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
22+
23+
24+
class SmartDebugRichHandler(RichHandler):
25+
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
26+
def __init__(self, formatter=None, *args, **kwargs):
27+
super().__init__(*args, **kwargs)
28+
if formatter is not None:
29+
self.setFormatter(formatter)
30+
31+
def emit(self, record):
32+
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
33+
if hasattr(record, "caller_frame"):
34+
frame_info = inspect.getframeinfo(record.caller_frame)
35+
record.pathname = frame_info.filename
36+
record.lineno = frame_info.lineno
37+
super().emit(record)
38+
39+
40+
def no_debug(func):
41+
"""Stops logging non-debug messages when we are in debug mode
42+
It creates a temporary logger and logs the message to the console and file
43+
This is so we don't get both normal output AND debugging output, AND so we get the proper log calling file & line number
44+
"""
45+
@functools.wraps(func)
46+
def wrapper(self, msg, *args, **kwargs):
47+
if self.logger.getEffectiveLevel() >= logging.INFO:
48+
return func(self, msg, *args, **kwargs)
49+
else:
50+
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
51+
caller_frame = inspect.currentframe().f_back
52+
create_temp_logger(caller_frame, formatted_text, args, kwargs)
53+
self.log_console_to_file(formatted_text, *args, **kwargs)
54+
return wrapper
1255

1356

1457
class NXCAdapter(logging.LoggerAdapter):
1558
def __init__(self, extra=None):
1659
logging.basicConfig(
1760
format="%(message)s",
1861
datefmt="[%X]",
19-
handlers=[
20-
RichHandler(
21-
console=nxc_console,
22-
rich_tracebacks=True,
23-
tracebacks_show_locals=False,
24-
)
25-
],
62+
handlers=[RichHandler(
63+
console=nxc_console,
64+
rich_tracebacks=True,
65+
tracebacks_show_locals=False
66+
)],
2667
)
2768
self.logger = logging.getLogger("nxc")
2869
self.extra = extra
@@ -40,52 +81,47 @@ def format(self, msg, *args, **kwargs): # noqa: A003
4081
if self.extra is None:
4182
return f"{msg}", kwargs
4283

43-
if "module_name" in self.extra and len(self.extra["module_name"]) > 8:
84+
if "module_name" in self.extra and len(self.extra["module_name"]) > 11:
4485
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
4586

4687
# If the logger is being called when hooking the 'options' module function
4788
if len(self.extra) == 1 and ("module_name" in self.extra):
48-
return (
49-
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}",
50-
kwargs,
51-
)
89+
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs)
5290

5391
# If the logger is being called from nxcServer
5492
if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra):
55-
return (
56-
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}",
57-
kwargs,
58-
)
93+
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs)
5994

6095
# If the logger is being called from a protocol
6196
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
6297

63-
return (
64-
f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}",
65-
kwargs,
66-
)
98+
return (f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs)
6799

100+
@no_debug
68101
def display(self, msg, *args, **kwargs):
69102
"""Display text to console, formatted for nxc"""
70103
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
71104
text = Text.from_ansi(msg)
72105
nxc_console.print(text, *args, **kwargs)
73106
self.log_console_to_file(text, *args, **kwargs)
74107

108+
@no_debug
75109
def success(self, msg, color="green", *args, **kwargs):
76-
"""Print some sort of success to the user"""
110+
"""Prints some sort of success to the user"""
77111
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
78112
text = Text.from_ansi(msg)
79113
nxc_console.print(text, *args, **kwargs)
80114
self.log_console_to_file(text, *args, **kwargs)
81115

116+
@no_debug
82117
def highlight(self, msg, *args, **kwargs):
83118
"""Prints a completely yellow highlighted message to the user"""
84119
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
85120
text = Text.from_ansi(msg)
86121
nxc_console.print(text, *args, **kwargs)
87122
self.log_console_to_file(text, *args, **kwargs)
88123

124+
@no_debug
89125
def fail(self, msg, color="red", *args, **kwargs):
90126
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
91127
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
@@ -99,26 +135,12 @@ def log_console_to_file(self, text, *args, **kwargs):
99135
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
100136
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
101137
"""
102-
if self.logger.getEffectiveLevel() >= logging.INFO:
103-
# will be 0 if it's just the console output, so only do this if we actually have file loggers
104-
if len(self.logger.handlers):
105-
try:
106-
for handler in self.logger.handlers:
107-
handler.handle(
108-
LogRecord(
109-
"nxc",
110-
20,
111-
"",
112-
kwargs,
113-
msg=text,
114-
args=args,
115-
exc_info=None,
116-
)
117-
)
118-
except Exception as e:
119-
self.logger.fail(f"Issue while trying to custom print handler: {e}")
120-
else:
121-
self.logger.info(text)
138+
if self.logger.getEffectiveLevel() >= logging.INFO and len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
139+
try:
140+
for handler in self.logger.handlers:
141+
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
142+
except Exception as e:
143+
self.logger.fail(f"Issue while trying to custom print handler: {e}")
122144

123145
def add_file_log(self, log_file=None):
124146
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
@@ -152,7 +174,7 @@ def init_log_file():
152174
datetime.now().strftime("%Y-%m-%d"),
153175
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
154176
)
155-
177+
156178

157179
class TermEscapeCodeFormatter(logging.Formatter):
158180
"""A class to strip the escape codes for logging to files"""

0 commit comments

Comments
 (0)