Merge branch 'main' into winlogon-autologon
commit
e728c15c6d
|
@ -1,11 +1,11 @@
|
||||||
name: Lint Python code with ruff
|
name: Lint Python code with ruff
|
||||||
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
|
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Lint Python code with ruff
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if:
|
if:
|
||||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||||
|
|
|
@ -1,32 +1,48 @@
|
||||||
name: NetExec Tests
|
name: NetExec Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
pull_request_review:
|
pull_request_review:
|
||||||
types: [submitted]
|
types: [submitted]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: NetExec Tests for Py${{ matrix.python-version }}
|
name: Test for Py${{ matrix.python-version }}
|
||||||
|
if: github.event.review.state == 'APPROVED'
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
max-parallel: 5
|
max-parallel: 5
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: NetExec set up python on ${{ matrix.os }}
|
- name: Install poetry
|
||||||
uses: actions/setup-python@v4
|
run: |
|
||||||
with:
|
pipx install poetry
|
||||||
python-version: ${{ matrix.python-version }}
|
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||||
- name: Install poetry
|
uses: actions/setup-python@v4
|
||||||
run: |
|
with:
|
||||||
pipx install poetry --python python${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
poetry --version
|
cache: poetry
|
||||||
poetry env info
|
cache-dependency-path: poetry.lock
|
||||||
- name: Install libraries with dev group
|
- name: Install poetry
|
||||||
run: |
|
run: |
|
||||||
poetry install --with dev
|
pipx install poetry --python python${{ matrix.python-version }}
|
||||||
- name: Run the e2e test
|
poetry --version
|
||||||
run: |
|
poetry env info
|
||||||
poetry run pytest tests
|
- name: Install libraries with dev group
|
||||||
|
run: |
|
||||||
|
poetry install --with dev
|
||||||
|
- name: Load every protocol and module
|
||||||
|
run: |
|
||||||
|
poetry run netexec winrm 127.0.0.1
|
||||||
|
poetry run netexec vnc 127.0.0.1
|
||||||
|
poetry run netexec smb 127.0.0.1
|
||||||
|
poetry run netexec ldap 127.0.0.1
|
||||||
|
poetry run netexec wmi 127.0.0.1
|
||||||
|
poetry run netexec rdp 127.0.0.1
|
||||||
|
poetry run netexec mssql 127.0.0.1
|
||||||
|
poetry run netexec ssh 127.0.0.1
|
||||||
|
poetry run netexec ftp 127.0.0.1
|
||||||
|
poetry run netexec smb 127.0.0.1 -M veeam
|
|
@ -163,7 +163,9 @@ class connection:
|
||||||
def proto_flow(self):
|
def proto_flow(self):
|
||||||
self.logger.debug("Kicking off proto_flow")
|
self.logger.debug("Kicking off proto_flow")
|
||||||
self.proto_logger()
|
self.proto_logger()
|
||||||
if self.create_conn_obj():
|
if not self.create_conn_obj():
|
||||||
|
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
|
||||||
|
else:
|
||||||
self.logger.debug("Created connection object")
|
self.logger.debug("Created connection object")
|
||||||
self.enum_host_info()
|
self.enum_host_info()
|
||||||
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
|
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
|
||||||
|
|
|
@ -95,7 +95,7 @@ class ModuleLoader:
|
||||||
module_spec = spec.loader.load_module().NXCModule
|
module_spec = spec.loader.load_module().NXCModule
|
||||||
|
|
||||||
module = {
|
module = {
|
||||||
f"{module_spec.name.lower()}": {
|
f"{module_spec.name}": {
|
||||||
"path": module_path,
|
"path": module_path,
|
||||||
"description": module_spec.description,
|
"description": module_spec.description,
|
||||||
"options": module_spec.options.__doc__,
|
"options": module_spec.options.__doc__,
|
||||||
|
|
|
@ -9,6 +9,49 @@ from termcolor import colored
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
|
||||||
|
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
|
||||||
|
temp_logger = logging.getLogger("temp")
|
||||||
|
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
|
||||||
|
handler = SmartDebugRichHandler(formatter=formatter)
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
class SmartDebugRichHandler(RichHandler):
|
||||||
|
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
|
||||||
|
def __init__(self, formatter=None, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if formatter is not None:
|
||||||
|
self.setFormatter(formatter)
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
|
||||||
|
if hasattr(record, "caller_frame"):
|
||||||
|
frame_info = inspect.getframeinfo(record.caller_frame)
|
||||||
|
record.pathname = frame_info.filename
|
||||||
|
record.lineno = frame_info.lineno
|
||||||
|
super().emit(record)
|
||||||
|
|
||||||
|
|
||||||
|
def no_debug(func):
|
||||||
|
"""Stops logging non-debug messages when we are in debug mode
|
||||||
|
It creates a temporary logger and logs the message to the console and file
|
||||||
|
This is so we don't get both normal output AND debugging output, AND so we get the proper log calling file & line number
|
||||||
|
"""
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(self, msg, *args, **kwargs):
|
||||||
|
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||||
|
return func(self, msg, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
|
||||||
|
caller_frame = inspect.currentframe().f_back
|
||||||
|
create_temp_logger(caller_frame, formatted_text, args, kwargs)
|
||||||
|
self.log_console_to_file(formatted_text, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class NXCAdapter(logging.LoggerAdapter):
|
class NXCAdapter(logging.LoggerAdapter):
|
||||||
|
@ -54,6 +97,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||||
|
|
||||||
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)
|
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)
|
||||||
|
|
||||||
|
@no_debug
|
||||||
def display(self, msg, *args, **kwargs):
|
def display(self, msg, *args, **kwargs):
|
||||||
"""Display text to console, formatted for nxc"""
|
"""Display text to console, formatted for nxc"""
|
||||||
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
|
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
|
||||||
|
@ -61,13 +105,15 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||||
nxc_console.print(text, *args, **kwargs)
|
nxc_console.print(text, *args, **kwargs)
|
||||||
self.log_console_to_file(text, *args, **kwargs)
|
self.log_console_to_file(text, *args, **kwargs)
|
||||||
|
|
||||||
|
@no_debug
|
||||||
def success(self, msg, color="green", *args, **kwargs):
|
def success(self, msg, color="green", *args, **kwargs):
|
||||||
"""Print some sort of success to the user"""
|
"""Prints some sort of success to the user"""
|
||||||
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
|
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
|
||||||
text = Text.from_ansi(msg)
|
text = Text.from_ansi(msg)
|
||||||
nxc_console.print(text, *args, **kwargs)
|
nxc_console.print(text, *args, **kwargs)
|
||||||
self.log_console_to_file(text, *args, **kwargs)
|
self.log_console_to_file(text, *args, **kwargs)
|
||||||
|
|
||||||
|
@no_debug
|
||||||
def highlight(self, msg, *args, **kwargs):
|
def highlight(self, msg, *args, **kwargs):
|
||||||
"""Prints a completely yellow highlighted message to the user"""
|
"""Prints a completely yellow highlighted message to the user"""
|
||||||
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
|
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
|
||||||
|
@ -75,6 +121,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||||
nxc_console.print(text, *args, **kwargs)
|
nxc_console.print(text, *args, **kwargs)
|
||||||
self.log_console_to_file(text, *args, **kwargs)
|
self.log_console_to_file(text, *args, **kwargs)
|
||||||
|
|
||||||
|
@no_debug
|
||||||
def fail(self, msg, color="red", *args, **kwargs):
|
def fail(self, msg, color="red", *args, **kwargs):
|
||||||
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
|
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
|
||||||
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
|
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
|
||||||
|
@ -88,16 +135,12 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||||
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
|
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
|
||||||
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
|
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
|
||||||
"""
|
"""
|
||||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
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
|
||||||
# will be 0 if it's just the console output, so only do this if we actually have file loggers
|
try:
|
||||||
if len(self.logger.handlers):
|
for handler in self.logger.handlers:
|
||||||
try:
|
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
|
||||||
for handler in self.logger.handlers:
|
except Exception as e:
|
||||||
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
|
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||||
except Exception as e:
|
|
||||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
|
||||||
else:
|
|
||||||
self.logger.info(text)
|
|
||||||
|
|
||||||
def add_file_log(self, log_file=None):
|
def add_file_log(self, log_file=None):
|
||||||
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
|
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||||
|
@ -131,7 +174,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||||
datetime.now().strftime("%Y-%m-%d"),
|
datetime.now().strftime("%Y-%m-%d"),
|
||||||
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
|
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TermEscapeCodeFormatter(logging.Formatter):
|
class TermEscapeCodeFormatter(logging.Formatter):
|
||||||
"""A class to strip the escape codes for logging to files"""
|
"""A class to strip the escape codes for logging to files"""
|
||||||
|
|
|
@ -14,7 +14,7 @@ class NXCModule:
|
||||||
def options(self, context, module_options):
|
def options(self, context, module_options):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
name = "MAQ"
|
name = "maq"
|
||||||
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
||||||
supported_protocols = ["ldap"]
|
supported_protocols = ["ldap"]
|
||||||
opsec_safe = True
|
opsec_safe = True
|
|
@ -186,7 +186,7 @@ def main():
|
||||||
exit(0)
|
exit(0)
|
||||||
elif args.module:
|
elif args.module:
|
||||||
nxc_logger.debug(f"Modules to be Loaded: {args.module}, {type(args.module)}")
|
nxc_logger.debug(f"Modules to be Loaded: {args.module}, {type(args.module)}")
|
||||||
for m in map(str.lower, args.module):
|
for m in args.module:
|
||||||
if m not in modules:
|
if m not in modules:
|
||||||
nxc_logger.error(f"Module not found: {m}")
|
nxc_logger.error(f"Module not found: {m}")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* Run `pytest` (or `poetry run pytest`)
|
* Run `pytest` (or `poetry run pytest`)
|
||||||
|
|
||||||
### End to End Tests
|
### End to End Tests
|
||||||
* Install nxc (either in venv or via Poetry)
|
* Install nxc (either in venv or via Poetry): `poetry install --with dev`
|
||||||
* Run `python tests/e2e_tests.py -t $IP -u $USER -p $PASS`, with optional `-k` parameter
|
* Run `python tests/e2e_tests.py -t $IP -u $USER -p $PASS`, with optional `-k` parameter
|
||||||
* Poetry: `poetry run python tests/e2e_tests.py -t $IP -u $USER -p $PASS`
|
* Poetry: `poetry run python tests/e2e_tests.py -t $IP -u $USER -p $PASS`
|
||||||
* To see full errors (that might show real errors not caught by checking the exit code), run with the `--errors` flag
|
* To see full errors (that might show real errors not caught by checking the exit code), run with the `--errors` flag
|
Loading…
Reference in New Issue