Merge branch 'main' into winlogon-autologon

winlogon-autologon
Alex 2024-03-31 13:59:44 -04:00 committed by GitHub
commit e728c15c6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 98 additions and 37 deletions

View File

@ -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

View File

@ -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

View File

@ -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 == "")):

View File

@ -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__,

View File

@ -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")

View File

@ -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

View File

@ -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)

View File

@ -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