diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c61f73f9..52407e93 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,11 +1,11 @@ name: Lint Python code with ruff # Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233 -on: [push, pull_request] +on: + push: jobs: lint: - name: Lint Python code with ruff runs-on: ubuntu-latest if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e0a8444..582f8292 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,32 +1,48 @@ name: NetExec Tests on: + workflow_dispatch: pull_request_review: types: [submitted] jobs: 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 }} strategy: max-parallel: 5 matrix: 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: - - uses: actions/checkout@v3 - - name: NetExec set up python on ${{ matrix.os }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install poetry - run: | - pipx install poetry --python python${{ matrix.python-version }} - poetry --version - poetry env info - - name: Install libraries with dev group - run: | - poetry install --with dev - - name: Run the e2e test - run: | - poetry run pytest tests + - uses: actions/checkout@v3 + - name: Install poetry + run: | + pipx install poetry + - name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: poetry + cache-dependency-path: poetry.lock + - name: Install poetry + run: | + pipx install poetry --python python${{ matrix.python-version }} + poetry --version + poetry env info + - 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 \ No newline at end of file diff --git a/nxc/connection.py b/nxc/connection.py index cc2d68bb..6165d399 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -163,7 +163,9 @@ class connection: def proto_flow(self): self.logger.debug("Kicking off proto_flow") 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.enum_host_info() if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")): diff --git a/nxc/loaders/moduleloader.py b/nxc/loaders/moduleloader.py index 0bed7408..d7e45034 100755 --- a/nxc/loaders/moduleloader.py +++ b/nxc/loaders/moduleloader.py @@ -95,7 +95,7 @@ class ModuleLoader: module_spec = spec.loader.load_module().NXCModule module = { - f"{module_spec.name.lower()}": { + f"{module_spec.name}": { "path": module_path, "description": module_spec.description, "options": module_spec.options.__doc__, diff --git a/nxc/logger.py b/nxc/logger.py index 5c0f40d6..51862a69 100755 --- a/nxc/logger.py +++ b/nxc/logger.py @@ -9,6 +9,49 @@ from termcolor import colored from datetime import datetime from rich.text import Text 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): @@ -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) + @no_debug def display(self, msg, *args, **kwargs): """Display text to console, formatted for nxc""" 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) self.log_console_to_file(text, *args, **kwargs) + @no_debug 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) text = Text.from_ansi(msg) nxc_console.print(text, *args, **kwargs) self.log_console_to_file(text, *args, **kwargs) + @no_debug def highlight(self, msg, *args, **kwargs): """Prints a completely yellow highlighted message to the user""" 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) self.log_console_to_file(text, *args, **kwargs) + @no_debug 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""" 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, 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: - # will be 0 if it's just the console output, so only do this if we actually have file loggers - if len(self.logger.handlers): - try: - for handler in self.logger.handlers: - handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None)) - except Exception as e: - self.logger.fail(f"Issue while trying to custom print handler: {e}") - else: - self.logger.info(text) + 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 + try: + for handler in self.logger.handlers: + handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None)) + except Exception as e: + self.logger.fail(f"Issue while trying to custom print handler: {e}") def add_file_log(self, log_file=None): file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s") @@ -131,7 +174,7 @@ class NXCAdapter(logging.LoggerAdapter): datetime.now().strftime("%Y-%m-%d"), f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log", ) - + class TermEscapeCodeFormatter(logging.Formatter): """A class to strip the escape codes for logging to files""" diff --git a/nxc/modules/MAQ.py b/nxc/modules/maq.py similarity index 98% rename from nxc/modules/MAQ.py rename to nxc/modules/maq.py index 921793c1..6ea5d44e 100644 --- a/nxc/modules/MAQ.py +++ b/nxc/modules/maq.py @@ -14,7 +14,7 @@ class NXCModule: def options(self, context, module_options): pass - name = "MAQ" + name = "maq" description = "Retrieves the MachineAccountQuota domain-level attribute" supported_protocols = ["ldap"] opsec_safe = True diff --git a/nxc/netexec.py b/nxc/netexec.py index a63ca1f3..7df05841 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -186,7 +186,7 @@ def main(): exit(0) elif 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: nxc_logger.error(f"Module not found: {m}") exit(1) diff --git a/tests/README.md b/tests/README.md index 44dd85e6..46bf7175 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,7 +5,7 @@ * Run `pytest` (or `poetry run pytest`) ### 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 * 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 \ No newline at end of file diff --git a/tests/e2e_test.py b/tests/e2e_tests.py similarity index 100% rename from tests/e2e_test.py rename to tests/e2e_tests.py