Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion capabilities/network-ops/capability.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema: 1
name: network-ops
version: "2.1.10"
version: "2.2.0"
description: >
Network operations and Active Directory exploitation. Multi-agent
pipeline for autonomous red teaming with Nmap scanning, Netexec
Expand Down
103 changes: 103 additions & 0 deletions capabilities/network-ops/tests/test_tool_fixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def _load_module(name: str, filename: str):
cracking_mod = _load_module("cracking", "cracking.py")
bloodyad_mod = _load_module("bloodyad", "bloodyad.py")
krbrelayx_mod = _load_module("krbrelayx", "krbrelayx.py")
netexec_mod = _load_module("netexec", "netexec.py")
# The tool modules above import ``._relay`` relatively, so it is already loaded
# under the package; reuse that instance rather than loading a second copy.
relay_mod = sys.modules.get(f"{_PKG}._relay") or _load_module("_relay", "_relay.py")
Expand Down Expand Up @@ -1043,6 +1044,108 @@ async def test_nonzero_exit_surfaces_instead_of_empty(self, tmp_path):
assert not any("--show" in cmd for cmd in calls)


# ===================================================================
# Netexec: null-session / empty-credential argv construction
# ===================================================================


class TestNetexecNullSession:
"""The base netexec() builder must distinguish None (omit the flag) from
"" (emit the explicit `-u ''` / `-p ''` null-session form)."""

def _make_netexec(self):
nx = netexec_mod.Netexec()
nx.timeout = 60
nx.variant = "all"
return nx

@pytest.mark.asyncio
async def test_empty_strings_emit_null_session(self):
"""username="" and password="" → `-u '' -p ''`, the anonymous null session."""
nx = self._make_netexec()
with patch.object(netexec_mod, "execute", new_callable=AsyncMock) as mock_exec:
mock_exec.return_value = "output"
await nx.netexec("smb", ["10.0.0.1"], args=["--shares"], username="", password="")
cmd = mock_exec.call_args[0][0]
assert "-u" in cmd and "-p" in cmd
assert cmd[cmd.index("-u") + 1] == ""
assert cmd[cmd.index("-p") + 1] == ""

@pytest.mark.asyncio
async def test_none_omits_flags(self):
"""username=None / password=None → no auth flags at all."""
nx = self._make_netexec()
with patch.object(netexec_mod, "execute", new_callable=AsyncMock) as mock_exec:
mock_exec.return_value = "output"
await nx.netexec("smb", ["10.0.0.1"], args=["--shares"])
cmd = mock_exec.call_args[0][0]
assert "-u" not in cmd
assert "-p" not in cmd

@pytest.mark.asyncio
async def test_populated_credentials_unchanged(self):
"""Non-empty credentials still build `-u user -p pass` (regression)."""
nx = self._make_netexec()
with patch.object(netexec_mod, "execute", new_callable=AsyncMock) as mock_exec:
mock_exec.return_value = "output"
await nx.netexec("smb", ["10.0.0.1"], args=[], username="admin", password="Passw0rd")
cmd = mock_exec.call_args[0][0]
assert cmd[cmd.index("-u") + 1] == "admin"
assert cmd[cmd.index("-p") + 1] == "Passw0rd"

@pytest.mark.asyncio
async def test_username_with_empty_password(self):
"""AS-REP-roast style: a real username with password="" → `-u svc -p ''`."""
nx = self._make_netexec()
with patch.object(netexec_mod, "execute", new_callable=AsyncMock) as mock_exec:
mock_exec.return_value = "output"
await nx.netexec("ldap", ["dc.corp.local"], args=[], username="svc", password="")
cmd = mock_exec.call_args[0][0]
assert cmd[cmd.index("-u") + 1] == "svc"
assert cmd[cmd.index("-p") + 1] == ""

@pytest.mark.asyncio
async def test_multi_value_lists_expand(self):
"""A list of usernames/passwords expands to repeated values after each flag."""
nx = self._make_netexec()
with patch.object(netexec_mod, "execute", new_callable=AsyncMock) as mock_exec:
mock_exec.return_value = "output"
await nx.netexec(
"smb",
["10.0.0.1"],
args=[],
username=["u1", "u2"],
password=["p1", "p2"],
)
cmd = mock_exec.call_args[0][0]
u_idx = cmd.index("-u")
assert cmd[u_idx + 1 : u_idx + 3] == ["u1", "u2"]
p_idx = cmd.index("-p")
assert cmd[p_idx + 1 : p_idx + 3] == ["p1", "p2"]

@pytest.mark.asyncio
async def test_empty_list_omits_flag(self):
"""An empty list is "no value supplied", not a null session — no dangling `-u`."""
nx = self._make_netexec()
with patch.object(netexec_mod, "execute", new_callable=AsyncMock) as mock_exec:
mock_exec.return_value = "output"
await nx.netexec("smb", ["10.0.0.1"], args=["--shares"], username=[])
cmd = mock_exec.call_args[0][0]
assert "-u" not in cmd

@pytest.mark.asyncio
async def test_enum_shares_wrapper_passes_null_session(self):
"""The specialized wrapper threads "" through to the null-session form."""
nx = self._make_netexec()
with patch.object(netexec_mod, "execute", new_callable=AsyncMock) as mock_exec:
mock_exec.return_value = "output"
await nx.netexec_smb_enum_shares(["10.0.0.1"], username="", password="")
cmd = mock_exec.call_args[0][0]
assert cmd[cmd.index("-u") + 1] == ""
assert cmd[cmd.index("-p") + 1] == ""
assert "--shares" in cmd


# ===================================================================
# Shared relay listener: stream_listener
#
Expand Down
82 changes: 43 additions & 39 deletions capabilities/network-ops/tools/netexec.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
from loguru import logger


def _emit_auth_flag(value: str | list[str] | None) -> bool:
"""Whether an auth value should produce its command-line flag.

``None`` and an empty list mean no value was supplied, so the flag is
omitted. An empty string is the explicit null-session form (``-u ''`` /
``-p ''``) and must still emit the flag with its empty value.
"""
return value is not None and value != []


class Netexec(Toolset):
"""
A toolset for network operations using the Netexec (nxc) utility.
Expand Down Expand Up @@ -40,8 +50,8 @@ async def netexec(
ssh, wmi, mssql, vnc, ldap, nfs, smb, winrm, ftp, or rdp.
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
args: A list of additional arguments and flags for netexec (e.g., ['--help']).
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand All @@ -50,9 +60,7 @@ async def netexec(
# Fix netexec first-run setup issue if .nxc exists as a file instead of directory
nxc_path = Path.home() / ".nxc"
if nxc_path.exists() and nxc_path.is_file():
logger.warning(
f"Found {nxc_path} as a file instead of directory, removing to allow netexec setup"
)
logger.warning(f"Found {nxc_path} as a file instead of directory, removing to allow netexec setup")
nxc_path.unlink()

cmd = ["netexec", protocol, *targets]
Expand All @@ -62,14 +70,10 @@ async def netexec(
elif domain:
cmd.extend(["-d", domain])

if username:
cmd.extend(
["-u", *(username if isinstance(username, list) else [username])]
)
if password:
cmd.extend(
["-p", *(password if isinstance(password, list) else [password])]
)
if _emit_auth_flag(username):
cmd.extend(["-u", *(username if isinstance(username, list) else [username])])
if _emit_auth_flag(password):
cmd.extend(["-p", *(password if isinstance(password, list) else [password])])
if hash:
cmd.extend(["-H", *(hash if isinstance(hash, list) else [hash])])

Expand Down Expand Up @@ -106,8 +110,8 @@ async def netexec_smb_enum(

Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down Expand Up @@ -144,8 +148,8 @@ async def netexec_smb_auth(

Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down Expand Up @@ -182,8 +186,8 @@ async def netexec_smb_enum_group_members(
Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
groups: Group names to query members for.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down Expand Up @@ -228,8 +232,8 @@ async def netexec_smb_enum_shares(

Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down Expand Up @@ -263,8 +267,8 @@ async def netexec_ldap_enum(

Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down Expand Up @@ -296,8 +300,8 @@ async def netexec_ldap_auth(

Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down Expand Up @@ -331,8 +335,8 @@ async def netexec_ldap_enum_group_members(
Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
groups: Group names to query members for.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down Expand Up @@ -376,8 +380,8 @@ async def netexec_asreproast(

Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
"""
Expand Down Expand Up @@ -410,8 +414,8 @@ async def netexec_kerberoast(

Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
"""
Expand Down Expand Up @@ -759,8 +763,8 @@ async def netexec_smb(
Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
args: Additional command line arguments for the command.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down Expand Up @@ -985,8 +989,8 @@ async def netexec_ldap(
Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
args: Additional command line arguments for the command.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down Expand Up @@ -1195,8 +1199,8 @@ async def netexec_mssql(
Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
args: Additional command line arguments for the command.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down Expand Up @@ -1389,8 +1393,8 @@ async def netexec_wmi(
Args:
targets: A list of IPs, ranges, CIDRs, hostnames, or FQDNs.
args: Additional command line arguments for the command.
username: Username, list of usernames, or a path to a user file (empty for null session).
password: Password, list of passwords, or a path to a password file (empty for null session).
username: Username, list of usernames, or a path to a user file. Pass "" to emit `-u ''` (pair with password="" for an anonymous null session).
password: Password, list of passwords, or a path to a password file. Pass "" to emit `-p ''` (pair with username="" for an anonymous null session).
domain: Domain for authentication.
hash: A single NTLM hash, a list of hashes, or a path to a hash file.
kerberos: Kerberos ccache file path or AES key for authentication.
Expand Down
Loading