Compare commits

..
2 changed files with 37 additions and 73 deletions

No files matched your search

+9 -12
View File
@@ -7,18 +7,10 @@
# Coding style
- Never add a comment or docstring at the top of a Python file unless that file already has a top-of-file comment or docstring.
- In parenthesized Python import blocks, group multiple imported names on each line instead of placing every name on its own line.
- Keep function and method call arguments on one line when practical, including `raise` and log calls; format object literals for readability.
- Keep comments short and precise. Use block form for docstrings and other long text.
- Assume ordinary LDAP searches and LDAP response parsing do not raise exceptions; do not wrap them in `try`/`except`.
- Always bind caught exceptions as `e`, not `error` or another name.
- Do not prefix function or method names with underscores; preserve required Python special methods such as `__init__`.
- Do not assume the host runs Linux; keep code and paths cross-platform unless the task explicitly targets one operating system.
- Keep implementations as short and concise as practical. Prefer direct control flow and shared error handling over wrappers, redundant branches, or defensive boilerplate.
- Avoid stub variables that merely hold a value for one immediate use. Pass data directly unless a local name improves clarity, prevents repeated work, or represents meaningful state.
- Do not use `.warning()` logging. Use `.fail()` when logic or an operation fails.
- If unsure about anything, ask for clarification before proceeding.
# NetExec protocol structure
@@ -31,21 +23,26 @@
- In files under `nxc/modules/`, do not define variables, constants, or helper functions at module scope.
- Keep all module-specific state, constants, and helper methods inside the `NXCModule` class.
- Module scope should contain only imports and the `NXCModule` class definition.
- When module options are invalid, log the reason with `context.log.fail()` and `return False` from `options()`; do not call `exit()`.
- When a module must terminate for invalid options, import `exit` with `from sys import exit` and call `exit(...)`; do not use `import sys` with `sys.exit(...)`.
- Do not over-validate module options. Check only that required arguments are present, mutually exclusive arguments are not combined, or a value would otherwise cause the module to fail before performing its normal lookup or operation.
- Do not validate whether user-supplied object names, identifiers, or similar values are semantically correct when the underlying protocol lookup can handle them; let a misspecified value produce the normal "not found" result.
- Avoid stub variables that merely hold a value for one immediate use. Pass data directly unless a local name improves clarity, prevents repeated work, or represents meaningful state.
- Keep module implementations as short and concise as practical. Prefer direct control flow and shared error handling over wrappers, redundant branches, or defensive boilerplate.
- Do not optimize module code for line length; long lines are acceptable when they keep straightforward logic together.
- Do not use `.warning()` logging in NetExec modules. Use `.fail()` when logic or an operation fails.
- Run `ruff check .` after every coding session.
# Development and validation
- To verify that a module works, run the module's actual NetExec command instead of pytest or compilation checks.
- Record every module-verification command in `tests/e2e_commands.txt` using the file's placeholders (`TARGET_HOST`, `LOGIN_USERNAME`, `LOGIN_PASSWORD`, `KERBEROS`), never real targets or credentials.
- Record every module-verification command in `tests/e2e_commands.txt`.
- Do not run compilation checks.
- Run Ruff after completing a coding session.
# Filesystem and source conventions
- Assume that the underlying operating system and its storage are fully secure. Files and other data produced by NetExec may intentionally contain readable plaintext credentials; do not add encryption, redaction, restrictive permission handling, or permission warnings unless explicitly requested.
- Before opening, creating, writing, renaming, moving, or deleting a file or directory, sanitize every path component derived from untrusted data with `sanitize_path_component()` from `nxc/helpers/path.py` and verify that the resulting path cannot traverse outside its intended base directory.
- Before opening, creating, writing, renaming, moving, or deleting a file or directory, sanitize every path component derived from untrusted data and verify that the resulting path cannot traverse outside its intended base directory.
- Place every file and directory produced by NetExec under `NXC_PATH` by default, using `TMP_PATH` for temporary artifacts. An explicit user-provided output path or task requirement may override this default.
- `NXC_PATH` defaults to `~/.nxc` but may be overridden by the environment. Import and use `NXC_PATH` or its derived path constants instead of hardcoding `~/.nxc`.
- Place imports at the top of files unless explicitly instructed otherwise; only a heavy, rarely used dependency on a hot path, such as protocol loading, may be imported where it is used.
- Place imports at the top of files unless explicitly instructed otherwise.
+28 -61
View File
@@ -26,13 +26,10 @@ from aardwolf.commons.iosettings import RDPIOSettings
from aardwolf.commons.target import RDPTarget
from aardwolf.keyboard.layoutmanager import KeyboardLayoutManager
from aardwolf.protocol.x224.constants import SUPP_PROTOCOLS
from aardwolf.network.x224 import X224Network
from aardwolf.network.tpkt import TPKTPacketizer
from asyauth.common.credentials.ntlm import NTLMCredential
from asyauth.common.credentials.kerberos import KerberosCredential
from asyauth.common.constants import asyauthSecret
from asysocks.unicomm.common.target import UniTarget, UniProto
from asysocks.unicomm.client import UniClient
class rdp(connection):
@@ -121,36 +118,40 @@ class rdp(connection):
except Exception as e:
self.logger.debug(f"Error adding host {self.host} into db: {e!s}")
def _create_rdp_connection(self, credentials, supported_protocols=None):
def create_conn_obj(self, credentials=None, supported_protocols=SUPP_PROTOCOLS.SSL):
"""Create self.conn. The call from proto_flow() has no credentials: it is the null-auth probe, which returns False when the host doesn't answer."""
if credentials is None:
self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout, unsafe_ssl=True) # old servers only offer ciphers OpenSSL rejects at its default security level
self.auth = NTLMCredential(secret="pass", username="user", domain="FAKE", stype=asyauthSecret.PASS)
iosettings = self.iosettings.clone_for_connection()
# Explicit protocols are used by discovery and screenshot fallbacks.
# Authenticated connections leave this unset so aardwolf selects the
# X224 flags appropriate for the credential type.
# the probe, the enumeration and the nla screenshot pin the protocols, the login passes None so aardwolf picks the X224 flags for the credential type
iosettings.supported_protocols = supported_protocols
return RDPConnection(
iosettings=iosettings,
target=copy.deepcopy(self.target),
credentials=copy.deepcopy(credentials),
)
self.conn = RDPConnection(iosettings=iosettings, target=copy.deepcopy(self.target), credentials=copy.deepcopy(credentials))
if credentials is not None:
return True
def create_conn_obj(self):
self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout, unsafe_ssl=True) # old servers only offer ciphers OpenSSL rejects at its default security level
self.auth = NTLMCredential(secret="pass", username="user", domain="FAKE", stype=asyauthSecret.PASS)
self.runner.run(self.check_nla())
self.logger.debug(f"Checking NLA for {self.host}")
try:
self.runner.run(self.connect_rdp(auth_only=True))
except OSError as e:
self.logger.debug(f"No RDP answer from {self.host}:{self.port}: {e!r}")
return False
except Exception as e:
# a server that requires NLA refuses this SSL-only offer, else auth_only stops with a RuntimeError before any session opens
self.nla = not isinstance(e, RuntimeError) and "SSL_NOT_ALLOWED_BY_SERVER" not in str(e)
return True
def enum_host_info(self):
for proto in reversed(self.protoflags):
try:
self.conn = self._create_rdp_connection(
self.auth, supported_protocols=proto
)
self.create_conn_obj(self.auth, supported_protocols=proto)
self.runner.run(self.connect_rdp_with_cleanup())
except OSError as e:
if "Errno 104" not in str(e):
return False
break
except Exception as e:
if "TCPSocket" in str(e):
return False
break
if "Reason:" not in str(e):
try:
info_domain = self.conn.get_extra_info()
@@ -174,21 +175,11 @@ class rdp(connection):
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
self.target = RDPTarget(
ip=self.host,
hostname=self.hostname,
port=self.port,
domain=self.domain,
dc_ip=self.domain,
timeout=self.args.rdp_timeout,
unsafe_ssl=True,
)
self.target = RDPTarget(ip=self.host, hostname=self.hostname, port=self.port, domain=self.domain, dc_ip=self.domain, timeout=self.args.rdp_timeout, unsafe_ssl=True)
# Reset so aardwolf negotiates on its own (restricted admin + HYBRID_EX), the loop above pinned it
self.iosettings.supported_protocols = None
return True
def guess_os(self, info_domain):
build = info_domain.get("os_build")
if build in WIN_VERSIONS:
@@ -197,33 +188,9 @@ class rdp(connection):
return f"Windows 10.0 Build {build}"
return f"{info_domain.get('os_guess') or 'Unknown'} Build {build or 'Unknown'}"
async def check_nla(self):
self.logger.debug(f"Checking NLA for {self.host}")
probe_connection = None
try:
self.conn = self._create_rdp_connection(
None, supported_protocols=SUPP_PROTOCOLS.SSL
)
packetizer = TPKTPacketizer()
client = UniClient(self.target, packetizer)
probe_connection = await asyncio.wait_for(client.connect(), timeout=self.args.rdp_timeout)
self.conn._connection = probe_connection
self.conn._x224net = X224Network(self.conn._connection)
_, err = await asyncio.wait_for(self.conn._x224net.client_negotiate(0, SUPP_PROTOCOLS.SSL), timeout=self.args.rdp_timeout)
# If no error SSL supported if SSL_NOT_ALLOWED_BY_SERVER error, plain RDP supported
if err is None or "SSL_NOT_ALLOWED_BY_SERVER" in str(err):
self.nla = False
return
except Exception:
pass
finally:
# this probe socket is not owned by aardwolf, terminate() would not close it
if probe_connection is not None:
await probe_connection.close()
async def connect_rdp(self):
async def connect_rdp(self, auth_only=False):
"""Connect to the RDP server. Does NOT clean up on exit."""
_, err = await asyncio.wait_for(self.conn.connect(), timeout=self.args.rdp_timeout)
_, err = await asyncio.wait_for(self.conn.connect(auth_only=auth_only), timeout=self.args.rdp_timeout)
if err is not None:
raise err
self.logger.debug(f"RDP negotiated protocol: {self.conn.x224_protocol}, authz_result: {self.conn.authz_result}")
@@ -250,7 +217,7 @@ class rdp(connection):
That PDU never comes when access is authorized but the logon is refused (restricted admin).
"""
await self.terminate_conn() # a session kept by an earlier success (--continue-on-success) would take a session slot this logon needs
self.conn = self._create_rdp_connection(self.auth)
self.create_conn_obj(self.auth, supported_protocols=None)
await self.connect_rdp()
try:
await asyncio.wait_for(self.conn.logon_info_received.wait(), timeout=self.args.rdp_timeout)
@@ -739,7 +706,7 @@ Add-Type -AssemblyName System.Windows.Forms
connected = False
for proto in self.protoflags_nla:
try:
self.conn = self._create_rdp_connection(self.auth, supported_protocols=proto)
self.create_conn_obj(self.auth, supported_protocols=proto)
await self.connect_rdp()
except Exception as e:
self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}")