mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 02:08:18 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2427fb5423 | ||
|
|
71716634c7 | ||
|
|
e6eb4f740f | ||
|
|
10bc8173f7 |
No files matched your search
@@ -7,10 +7,15 @@
|
||||
|
||||
# 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; 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.
|
||||
- If unsure about anything, ask for clarification before proceeding.
|
||||
|
||||
# NetExec module structure
|
||||
|
||||
|
||||
+275
-292
@@ -5,22 +5,15 @@ from nxc.connection import connection
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.config import host_info_colors
|
||||
from pyNfsClient import (
|
||||
Portmap,
|
||||
Mount,
|
||||
NFSv3,
|
||||
)
|
||||
from pyNfsClient import NFSClient, discover_nfs_versions
|
||||
from pyNfsClient.kerberos import KerberosInitiator
|
||||
from pyNfsClient.const import (
|
||||
NFS_PROGRAM,
|
||||
NFS_V3,
|
||||
ACCESS3_READ,
|
||||
ACCESS3_MODIFY,
|
||||
ACCESS3_EXECUTE,
|
||||
NFSSTAT3,
|
||||
NFS3ERR_NOENT,
|
||||
NF3REG,
|
||||
)
|
||||
import re
|
||||
import uuid
|
||||
import math
|
||||
import os
|
||||
@@ -74,11 +67,14 @@ fsid_lens = {
|
||||
|
||||
class nfs(connection):
|
||||
def __init__(self, args, db, host):
|
||||
if args.nfs_auth in ("krb5", "krb5i", "krb5p"):
|
||||
args.kerberos = True
|
||||
self.protocol = "nfs"
|
||||
self.port = 111
|
||||
self.portmap = None
|
||||
self.mnt_port = None
|
||||
self.mount = None
|
||||
self.port = args.port or 2049
|
||||
self.nfs = None
|
||||
self.nfs_version = args.nfs_version
|
||||
self.nfs_versions = ()
|
||||
self.discovery = None
|
||||
self.auth = {
|
||||
"flavor": 1,
|
||||
"machine_name": uuid.uuid4().hex.upper()[0:6],
|
||||
@@ -86,6 +82,9 @@ class nfs(connection):
|
||||
"gid": 0,
|
||||
"aux_gid": [],
|
||||
}
|
||||
self.rpc_auth = self.auth if args.nfs_auth == "sys" else None
|
||||
self.authenticated = False
|
||||
self.credentials = None
|
||||
self.root_escape = None
|
||||
# If root escape is possible, the escape_share and escape_fh will be populated
|
||||
self.escape_share = None
|
||||
@@ -97,25 +96,59 @@ class nfs(connection):
|
||||
extra={
|
||||
"protocol": "NFS",
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"port": self.port or self.args.port or 2049,
|
||||
"hostname": self.hostname,
|
||||
}
|
||||
)
|
||||
|
||||
def uses_kerberos(self):
|
||||
return self.args.nfs_auth in ("krb5", "krb5i", "krb5p")
|
||||
|
||||
@staticmethod
|
||||
def auth_snapshot(auth):
|
||||
if not isinstance(auth, dict):
|
||||
return auth
|
||||
auth = dict(auth)
|
||||
auth["aux_gid"] = tuple(auth.get("aux_gid", ()))
|
||||
return auth
|
||||
|
||||
def connect_nfs(self):
|
||||
if self.nfs is not None:
|
||||
return self.nfs
|
||||
port = self.args.port
|
||||
if port is None and self.nfs_version == "3" and self.discovery is not None:
|
||||
port = self.discovery.nfs3_port
|
||||
self.nfs = NFSClient(self.host, self.nfs_version, port, self.args.nfs_timeout, self.auth if self.uses_kerberos() and self.rpc_auth is None else self.rpc_auth)
|
||||
self.nfs.connect()
|
||||
self.conn = self.nfs
|
||||
self.port = self.nfs.port
|
||||
return self.nfs
|
||||
|
||||
def selected_root_handle(self):
|
||||
if self.root_escape and not self.args.share:
|
||||
self.logger.success(f"Successful escape on share: {self.escape_share}")
|
||||
return self.escape_fh
|
||||
if not self.args.share and self.nfs_version == "3":
|
||||
self.logger.fail("No root escape possible, please specify a share")
|
||||
return None
|
||||
mount_info = self.nfs.root_handle(self.args.share or "/")
|
||||
if mount_info["status"] != 0:
|
||||
self.logger.fail(f"Could not open share {self.args.share or '/'}: {self.nfs.status_name(mount_info['status'])}")
|
||||
return None
|
||||
return mount_info["mountinfo"]["fhandle"]
|
||||
|
||||
def create_conn_obj(self):
|
||||
"""Initializes and connects to the portmap and mounted folder"""
|
||||
"""Discover supported versions and connect the selected NFS client."""
|
||||
try:
|
||||
# Portmap Initialization
|
||||
self.portmap = Portmap(self.host, timeout=self.args.nfs_timeout)
|
||||
self.portmap.connect()
|
||||
|
||||
# Mount Initialization
|
||||
self.mnt_port = self.portmap.getport(Mount.program, Mount.program_version)
|
||||
self.mount = Mount(host=self.host, port=self.mnt_port, timeout=self.args.nfs_timeout, auth=self.auth)
|
||||
self.mount.connect()
|
||||
|
||||
# Change logging port to the NFS port
|
||||
self.port = self.mnt_port
|
||||
self.discovery = discover_nfs_versions(self.host, self.rpc_auth, self.args.port or 2049, self.args.nfs_timeout, 111)
|
||||
self.nfs_versions = self.discovery.supported
|
||||
if self.nfs_version is not None and self.nfs_version not in self.nfs_versions and not any(label == self.nfs_version or (self.nfs_version == "3" and (label == "rpcbind" or label.startswith("3@"))) for label, _ in self.discovery.inconclusive):
|
||||
raise ConnectionError(f"NFSv{self.nfs_version} is not supported by the target")
|
||||
if self.nfs_version is None:
|
||||
if not self.nfs_versions:
|
||||
raise ConnectionError("No supported NFS version discovered")
|
||||
self.nfs_version = "3" if "3" in self.nfs_versions else self.nfs_versions[-1]
|
||||
self.connect_nfs()
|
||||
self.proto_logger()
|
||||
except Exception as e:
|
||||
self.logger.info(f"Error during Initialization: {e}")
|
||||
@@ -123,46 +156,82 @@ class nfs(connection):
|
||||
return True
|
||||
|
||||
def enum_host_info(self):
|
||||
try:
|
||||
# Dump all registered programs
|
||||
programs = self.portmap.dump()
|
||||
|
||||
self.nfs_versions = set()
|
||||
for program in programs:
|
||||
if program["program"] == NFS_PROGRAM:
|
||||
self.nfs_versions.add(program["version"])
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error checking NFS version: {self.host} {e}")
|
||||
|
||||
# Connect to NFS
|
||||
try:
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
if nfs_port:
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
# Check if root escape is possible
|
||||
if NFS_V3 in self.nfs_versions:
|
||||
self.root_escape = self.try_root_escape()
|
||||
else:
|
||||
self.logger.debug("NFSv3 not supported, skipping root escape check")
|
||||
self.nfs3.disconnect()
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Failed to connect to NFS3 host: {e}")
|
||||
|
||||
if not self.uses_kerberos():
|
||||
try:
|
||||
self.root_escape = self.try_root_escape()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Root escape check failed: {e}")
|
||||
self.root_escape = None
|
||||
self.db.add_host(self.host, self.hostname, self.port, self.nfs_versions, self.root_escape)
|
||||
|
||||
def print_host_info(self):
|
||||
root_escape_str = colored(f"root escape:{self.root_escape}", host_info_colors[1 if self.root_escape else 0], attrs=["bold"])
|
||||
self.logger.display(f"Supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)}) ({root_escape_str})")
|
||||
root_escape_str = colored(f"root escape:{'unknown' if self.root_escape is None else self.root_escape}", host_info_colors[1 if self.root_escape else 0], attrs=["bold"])
|
||||
self.logger.display(f"Supported NFS versions: ({', '.join(self.nfs_versions)}) NFSv{self.nfs_version} {self.args.nfs_auth} ({root_escape_str})")
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect mount and portmap if they are connected"""
|
||||
"""Release the generic NFS client and its version-specific state."""
|
||||
try:
|
||||
self.mount.disconnect()
|
||||
self.portmap.disconnect()
|
||||
self.logger.info(f"Disconnect successful: {self.host}:{self.port}")
|
||||
if self.nfs is not None:
|
||||
self.nfs.disconnect()
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error during disconnect: {e}")
|
||||
else:
|
||||
self.logger.info(f"Disconnect successful: {self.host}:{self.port}")
|
||||
|
||||
def login(self):
|
||||
if not self.uses_kerberos():
|
||||
return True
|
||||
if self.args.use_kcache:
|
||||
return self.kerberos_login(self.args.domain, self.args.username[0] if self.args.username else "", kdcHost=self.args.kdcHost, useCache=True)
|
||||
if not self.args.username:
|
||||
self.logger.fail(f"{self.args.nfs_auth} requires credentials or --use-kcache")
|
||||
return False
|
||||
return super().login() or self.authenticated
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
if not self.uses_kerberos():
|
||||
return True
|
||||
return self.kerberos_login(domain, username, password=password)
|
||||
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
if not self.uses_kerberos():
|
||||
self.logger.fail("NT hashes only apply to Kerberos NFS authentication")
|
||||
return False
|
||||
return self.kerberos_login(domain, username, ntlm_hash=ntlm_hash)
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
lmhash, nthash = ntlm_hash.split(":", 1) if ":" in ntlm_hash else ("", ntlm_hash)
|
||||
self.credentials = {
|
||||
"realm": domain,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"lmhash": lmhash,
|
||||
"nthash": nthash,
|
||||
"aes_key": aesKey,
|
||||
"ccache": os.environ.get("KRB5CCNAME") if useCache else None,
|
||||
"use_cache": useCache,
|
||||
"kdc_host": kdcHost,
|
||||
"service": "nfs",
|
||||
}
|
||||
try:
|
||||
if self.nfs is not None:
|
||||
self.nfs.disconnect()
|
||||
self.nfs = None
|
||||
self.rpc_auth = None
|
||||
self.connect_nfs()
|
||||
self.nfs.establish_gss(KerberosInitiator(self.host, self.credentials), self.args.nfs_auth)
|
||||
self.rpc_auth = self.nfs.auth
|
||||
self.root_escape = self.try_root_escape()
|
||||
except Exception as e:
|
||||
self.logger.fail(f"{domain}\\{username} ({self.args.nfs_auth}): {e}")
|
||||
return False
|
||||
self.domain = domain
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.authenticated = True
|
||||
self.db.add_host(self.host, self.hostname, self.port, self.nfs_versions, self.root_escape)
|
||||
self.logger.success(f"{domain}\\{username} ({self.args.nfs_auth})")
|
||||
return True
|
||||
|
||||
def list_dir(self, file_handle, path, recurse=1):
|
||||
"""Process entries in NFS directory recursively with UID autodection"""
|
||||
@@ -170,6 +239,8 @@ class nfs(connection):
|
||||
try:
|
||||
contents = []
|
||||
for entry in entries:
|
||||
if self.args.nfs_auth == "sys":
|
||||
self.auth.update(directory_auth)
|
||||
if "name" in entry and entry["name"] not in [b".", b".."]:
|
||||
item_path = f'{path}/{entry["name"].decode("utf-8")}' # Constructing file path
|
||||
if entry.get("name_attributes", {}).get("present", False):
|
||||
@@ -178,7 +249,7 @@ class nfs(connection):
|
||||
contents += self.list_dir(dir_handle, item_path, recurse=recurse - 1)
|
||||
else:
|
||||
file_handle = entry["name_handle"]["handle"]["data"]
|
||||
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
|
||||
attrs = self.update_auth(file_handle)
|
||||
file_size = attrs["attributes"]["size"]
|
||||
file_size = convert_size(file_size)
|
||||
uid = attrs["attributes"]["uid"]
|
||||
@@ -189,135 +260,93 @@ class nfs(connection):
|
||||
# Processing next entries recursively
|
||||
contents += process_entries(entry["nextentry"], path, uid, recurse)
|
||||
|
||||
if self.args.nfs_auth == "sys":
|
||||
self.auth.update(directory_auth)
|
||||
return contents
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}")
|
||||
|
||||
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
attrs = self.update_auth(file_handle)
|
||||
directory_auth = dict(self.auth)
|
||||
|
||||
if recurse == 0:
|
||||
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
|
||||
return [{"path": f"{path}/", "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": "-", "uid": self.auth["uid"]}]
|
||||
return [{"path": f"{path}/", "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": "-", "uid": attrs["attributes"]["uid"]}]
|
||||
|
||||
items = self.nfs3.readdirplus(file_handle, auth=self.auth)
|
||||
items = self.nfs.readdirplus(file_handle, auth=self.rpc_auth)
|
||||
if "resfail" in items:
|
||||
raise Exception("Insufficient Permissions")
|
||||
else:
|
||||
entries = items["resok"]["reply"]["entries"]
|
||||
|
||||
return process_entries(entries, path, self.auth["uid"], recurse)
|
||||
|
||||
def export_info(self, export_nodes):
|
||||
"""Enumerates all NFS shares and their access range"""
|
||||
networks = []
|
||||
for node in export_nodes:
|
||||
|
||||
# Collect the names of the groups associated with this export node
|
||||
group_names = self.group_names(node.ex_groups) or ["Everyone"]
|
||||
networks.append(group_names)
|
||||
|
||||
# If there are more export nodes, process them recursively. More than one share.
|
||||
if node.ex_next:
|
||||
networks.extend(self.export_info(node.ex_next))
|
||||
|
||||
return networks
|
||||
|
||||
def group_names(self, groups):
|
||||
"""Enumerates all access range of the share(s)"""
|
||||
result = []
|
||||
for group in groups:
|
||||
result.append(group.gr_name.decode())
|
||||
|
||||
# If there are more IP's, process them recursively.
|
||||
if group.gr_next:
|
||||
result.extend(self.group_names(group.gr_next))
|
||||
|
||||
return result
|
||||
return process_entries(entries, path, attrs["attributes"]["uid"], recurse)
|
||||
|
||||
def shares(self):
|
||||
self.logger.display("Enumerating NFS Shares")
|
||||
try:
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
|
||||
output_export = str(self.mount.export())
|
||||
networks = self.export_info(self.mount.export())
|
||||
|
||||
reg = re.compile(r"ex_dir=b'([^']*)'") # Get share names
|
||||
shares = list(reg.findall(output_export))
|
||||
self.connect_nfs()
|
||||
|
||||
# Mount shares and check permissions
|
||||
self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'Storage Usage':<17}{'Share':<30} {'Access List':<15}")
|
||||
self.logger.highlight(f"{'---':<11}{'-----':<9}{'-------------':<17}{'-----':<30} {'-----------':<15}")
|
||||
for share, network in zip(shares, networks, strict=True):
|
||||
self.logger.highlight(f"{'UID':<11} {'Perms':<9}{'Storage Usage':<17}{'Share':<30} {'Access List':<15}")
|
||||
self.logger.highlight(f"{'---':<11} {'-----':<9}{'-------------':<17}{'-----':<30} {'-----------':<15}")
|
||||
for share, network in self.nfs.exports():
|
||||
try:
|
||||
mnt_info = self.mount.mnt(share, self.auth)
|
||||
mnt_info = self.nfs.root_handle(share)
|
||||
self.logger.debug(f"Mounted {share} - {mnt_info}")
|
||||
if mnt_info["status"] != 0:
|
||||
self.logger.debug(f"Error mounting share {share}: {NFSSTAT3[mnt_info['status']]}")
|
||||
self.logger.debug(f"Error mounting share {share}: {self.nfs.status_name(mnt_info['status'])}")
|
||||
self.logger.highlight(f"{'-':<11}{'---':<9}{'---'}/{'---':<12} {share:<30} {', '.join(network) if network else 'No network':<15}")
|
||||
else:
|
||||
file_handle = mnt_info["mountinfo"]["fhandle"]
|
||||
|
||||
info = self.nfs3.fsstat(file_handle, self.auth)
|
||||
info = self.nfs.fsstat(file_handle, auth=self.rpc_auth)
|
||||
free_space = info["resok"]["fbytes"]
|
||||
total_space = info["resok"]["tbytes"]
|
||||
used_space = total_space - free_space
|
||||
|
||||
# Autodetectting the uid needed for the share
|
||||
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
attrs = self.update_auth(file_handle)
|
||||
|
||||
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
|
||||
self.mount.umnt(self.auth)
|
||||
self.nfs.unmount()
|
||||
self.db.add_share(self.host, (read_perm, write_perm, exec_perm), (convert_size(used_space), "/", convert_size(total_space)), share, network)
|
||||
self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space) + '/' + convert_size(total_space):<16} {share:<30} {', '.join(network) if network else 'No network':<15}")
|
||||
self.logger.highlight(f"{attrs['attributes']['uid']:<11} {'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space) + '/' + convert_size(total_space):<16} {share:<30} {', '.join(network) if network else 'No network':<15}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Failed to list share: {share} - {e}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}")
|
||||
finally:
|
||||
self.nfs3.disconnect()
|
||||
|
||||
def get_permissions(self, file_handle):
|
||||
"""Check permissions for the file handle"""
|
||||
try:
|
||||
read_perm = self.nfs3.access(file_handle, ACCESS3_READ, self.auth).get("resok", {}).get("access", 0) is ACCESS3_READ
|
||||
except Exception:
|
||||
read_perm = self.nfs.access(file_handle, ACCESS3_READ, auth=self.rpc_auth).get("resok", {}).get("access", 0) == ACCESS3_READ
|
||||
except Exception as e:
|
||||
self.logger.debug(f"NFS read access check failed: {e}")
|
||||
read_perm = False
|
||||
try:
|
||||
write_perm = self.nfs3.access(file_handle, ACCESS3_MODIFY, self.auth).get("resok", {}).get("access", 0) is ACCESS3_MODIFY
|
||||
except Exception:
|
||||
write_perm = self.nfs.access(file_handle, ACCESS3_MODIFY, auth=self.rpc_auth).get("resok", {}).get("access", 0) == ACCESS3_MODIFY
|
||||
except Exception as e:
|
||||
self.logger.debug(f"NFS write access check failed: {e}")
|
||||
write_perm = False
|
||||
try:
|
||||
exec_perm = self.nfs3.access(file_handle, ACCESS3_EXECUTE, self.auth).get("resok", {}).get("access", 0) is ACCESS3_EXECUTE
|
||||
except Exception:
|
||||
exec_perm = self.nfs.access(file_handle, ACCESS3_EXECUTE, auth=self.rpc_auth).get("resok", {}).get("access", 0) == ACCESS3_EXECUTE
|
||||
except Exception as e:
|
||||
self.logger.debug(f"NFS execute access check failed: {e}")
|
||||
exec_perm = False
|
||||
return read_perm, write_perm, exec_perm
|
||||
|
||||
def enum_shares(self):
|
||||
try:
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
|
||||
# Mounting NFS Shares
|
||||
output_export = str(self.mount.export())
|
||||
reg = re.compile(r"ex_dir=b'([^']*)'")
|
||||
shares = list(reg.findall(output_export))
|
||||
networks = self.export_info(self.mount.export())
|
||||
self.connect_nfs()
|
||||
|
||||
self.logger.display("Enumerating NFS Shares Directories")
|
||||
for share, network in zip(shares, networks, strict=True):
|
||||
for share, network in self.nfs.exports():
|
||||
try:
|
||||
mount_info = self.mount.mnt(share, self.auth)
|
||||
mount_info = self.nfs.root_handle(share)
|
||||
self.logger.debug(f"Mounted {share} - {mount_info}")
|
||||
if mount_info["status"] != 0:
|
||||
self.logger.fail(f"Error mounting share {share}: {NFSSTAT3[mount_info['status']]}")
|
||||
self.logger.fail(f"Error mounting share {share}: {self.nfs.status_name(mount_info['status'])}")
|
||||
continue
|
||||
|
||||
fhandle = mount_info["mountinfo"]["fhandle"]
|
||||
@@ -325,10 +354,10 @@ class nfs(connection):
|
||||
|
||||
self.logger.success(share)
|
||||
if contents:
|
||||
self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'File Size':<15}{'File Path':<45} {'Access List':<15}")
|
||||
self.logger.highlight(f"{'---':<11}{'-----':<9}{'---------':<15}{'---------':<45} {'-----------':<15}")
|
||||
self.logger.highlight(f"{'UID':<11} {'Perms':<9}{'File Size':<15}{'File Path':<45} {'Access List':<15}")
|
||||
self.logger.highlight(f"{'---':<11} {'-----':<9}{'---------':<15}{'---------':<45} {'-----------':<15}")
|
||||
for content in contents:
|
||||
self.logger.highlight(f"{content['uid']:<11}{'r' if content['read'] else '-'}{'w' if content['write'] else '-'}{'x' if content['execute'] else '-':<7}{content['filesize']:<14} {content['path']:<45} {', '.join(network) if network else 'No network':<15}")
|
||||
self.logger.highlight(f"{content['uid']:<11} {'r' if content['read'] else '-'}{'w' if content['write'] else '-'}{'x' if content['execute'] else '-':<7}{content['filesize']:<14} {content['path']:<45} {', '.join(network) if network else 'No network':<15}")
|
||||
except Exception as e:
|
||||
if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e):
|
||||
self.logger.fail(f"{share} - RPC Access denied")
|
||||
@@ -341,8 +370,6 @@ class nfs(connection):
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error on Listing NFS Shares Directories: {self.host}:{self.port} {e}")
|
||||
self.logger.debug("It is probably unknown format or can not access as anonymously.")
|
||||
finally:
|
||||
self.nfs3.disconnect()
|
||||
|
||||
def get_file(self):
|
||||
"""Downloads a file from the NFS share"""
|
||||
@@ -356,30 +383,18 @@ class nfs(connection):
|
||||
|
||||
self.logger.display(f"Downloading {remote_file_path} to {local_file_path}")
|
||||
try:
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
self.connect_nfs()
|
||||
|
||||
# Mount the NFS share or get the root handle
|
||||
if self.root_escape and not self.args.share:
|
||||
mount_fh = self.escape_fh
|
||||
elif not self.args.share:
|
||||
self.logger.fail("No root escape possible, please specify a share")
|
||||
mount_fh = self.selected_root_handle()
|
||||
if mount_fh is None:
|
||||
return
|
||||
else:
|
||||
mnt_info = self.mount.mnt(self.args.share, self.auth)
|
||||
if mnt_info["status"] != 0:
|
||||
self.logger.fail(f"Error mounting share {self.args.share}: {NFSSTAT3[mnt_info['status']]}")
|
||||
return
|
||||
mount_fh = mnt_info["mountinfo"]["fhandle"]
|
||||
|
||||
# Iterate over the path until we hit the file
|
||||
curr_fh = mount_fh
|
||||
for sub_path in remote_file_path.lstrip("/").split("/"):
|
||||
# Update the UID for the next object and get the handle
|
||||
self.update_auth(curr_fh)
|
||||
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
|
||||
res = self.nfs.lookup(curr_fh, sub_path, auth=self.rpc_auth)
|
||||
|
||||
# Check for a bad path
|
||||
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
|
||||
@@ -401,7 +416,7 @@ class nfs(connection):
|
||||
# Loop until we have read the entire file
|
||||
with open(local_file_path, "wb+") as local_file:
|
||||
while not eof:
|
||||
file_data = self.nfs3.read(curr_fh, offset, auth=self.auth)
|
||||
file_data = self.nfs.read(curr_fh, offset, auth=self.rpc_auth)
|
||||
|
||||
if "resfail" in file_data:
|
||||
raise Exception("Insufficient Permissions")
|
||||
@@ -419,7 +434,7 @@ class nfs(connection):
|
||||
self.logger.highlight(f"File successfully downloaded from {remote_file_path} to {local_file_path}")
|
||||
|
||||
# Unmount the share
|
||||
self.mount.umnt(self.auth)
|
||||
self.nfs.unmount()
|
||||
except Exception as e:
|
||||
self.logger.fail(f'Error retrieving file "{file_name}" from "{remote_dir_path}": {e}')
|
||||
if os.path.exists(local_file_path) and os.path.getsize(local_file_path) == 0:
|
||||
@@ -442,37 +457,25 @@ class nfs(connection):
|
||||
|
||||
self.logger.display(f"Uploading from {local_file_path} to {remote_file_path}")
|
||||
try:
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
self.connect_nfs()
|
||||
|
||||
# Mount the NFS share or get the root handle
|
||||
if self.root_escape and not self.args.share:
|
||||
mount_fh = self.escape_fh
|
||||
elif not self.args.share:
|
||||
self.logger.fail("No root escape possible, please specify a share")
|
||||
mount_fh = self.selected_root_handle()
|
||||
if mount_fh is None:
|
||||
return
|
||||
else:
|
||||
mnt_info = self.mount.mnt(self.args.share, self.auth)
|
||||
if mnt_info["status"] != 0:
|
||||
self.logger.fail(f"Error mounting share {self.args.share}: {NFSSTAT3[mnt_info['status']]}")
|
||||
return
|
||||
mount_fh = mnt_info["mountinfo"]["fhandle"]
|
||||
|
||||
# Iterate over the path
|
||||
curr_fh = mount_fh
|
||||
# If target dir is "" or "/" without filter we would get one item with [""]
|
||||
for sub_path in list(filter(None, remote_dir_path.lstrip("/").split("/"))):
|
||||
self.update_auth(curr_fh)
|
||||
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
|
||||
res = self.nfs.lookup(curr_fh, sub_path, auth=self.rpc_auth)
|
||||
|
||||
# If the path does not exist, create it
|
||||
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
|
||||
self.logger.display(f"Creating directory '/{sub_path}/'")
|
||||
res = self.nfs3.mkdir(curr_fh, sub_path, 0o777, auth=self.auth)
|
||||
res = self.nfs.mkdir(curr_fh, sub_path, 0o777, auth=self.rpc_auth)
|
||||
if res["status"] != 0:
|
||||
self.logger.fail(f"Error creating directory '/{sub_path}/': {NFSSTAT3[res['status']]}")
|
||||
self.logger.fail(f"Error creating directory '/{sub_path}/': {self.nfs.status_name(res['status'])}")
|
||||
return
|
||||
else:
|
||||
curr_fh = res["resok"]["obj"]["handle"]["data"]
|
||||
@@ -484,18 +487,18 @@ class nfs(connection):
|
||||
self.update_auth(curr_fh)
|
||||
|
||||
# Checking if file_name already exists on remote file path
|
||||
lookup_response = self.nfs3.lookup(curr_fh, file_name, auth=self.auth)
|
||||
lookup_response = self.nfs.lookup(curr_fh, file_name, auth=self.rpc_auth)
|
||||
open_auth = self.auth_snapshot(self.rpc_auth)
|
||||
|
||||
# If success, file_name does not exist on remote machine. Else, trying to overwrite it.
|
||||
if lookup_response["resok"] is None:
|
||||
# Create file
|
||||
self.logger.display(f"Trying to create {remote_file_path}")
|
||||
res = self.nfs3.create(curr_fh, file_name, create_mode=1, mode=0o777, auth=self.auth)
|
||||
res = self.nfs.create(curr_fh, file_name, create_mode=1, mode=0o777, auth=open_auth)
|
||||
if res["status"] != 0:
|
||||
raise Exception(NFSSTAT3[res["status"]])
|
||||
raise Exception(self.nfs.status_name(res["status"]))
|
||||
else:
|
||||
file_handle = res["resok"]["obj"]["handle"]["data"]
|
||||
self.update_auth(file_handle)
|
||||
self.logger.success(f"'{file_name}' successfully created")
|
||||
else:
|
||||
# Asking the user if they want to overwrite the file
|
||||
@@ -506,34 +509,39 @@ class nfs(connection):
|
||||
else:
|
||||
return
|
||||
|
||||
# Update the UID and GID for the file
|
||||
self.update_auth(file_handle)
|
||||
|
||||
# Use wtpref as the chunk size
|
||||
res = self.nfs3.fsinfo(file_handle, auth=self.auth)
|
||||
if res["status"] != 0:
|
||||
self.logger.fail(f"Error getting FSINFO for {remote_file_path}: {NFSSTAT3[res['status']]}")
|
||||
return
|
||||
chunk_size = res["resok"]["wtpref"]
|
||||
|
||||
self.logger.display(f"Transferring data from '{local_file_path}' to '{remote_file_path}'")
|
||||
try:
|
||||
offset = 0
|
||||
with open(local_file_path, "rb") as file:
|
||||
while chunk := file.read(chunk_size):
|
||||
# Write the data to the remote file
|
||||
res = self.nfs3.write(file_handle, offset, len(chunk), chunk, 1, auth=self.auth)
|
||||
if res["status"] != 0:
|
||||
self.logger.fail(f"Error writing to '{remote_file_path}': {NFSSTAT3[res['status']]}")
|
||||
return
|
||||
offset += len(chunk)
|
||||
# Update the UID and GID for the file without changing the identity used by NFSv4 OPEN state.
|
||||
self.update_auth(file_handle)
|
||||
|
||||
self.logger.success(f"Data from '{local_file_path}' successfully written to '{remote_file_path}' with permissions 777")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Could not write to '{local_file_path}': {e}")
|
||||
# Use wtpref as the chunk size
|
||||
res = self.nfs.fsinfo(file_handle, auth=self.rpc_auth)
|
||||
if res["status"] != 0:
|
||||
self.logger.fail(f"Error getting FSINFO for {remote_file_path}: {self.nfs.status_name(res['status'])}")
|
||||
return
|
||||
chunk_size = res["resok"]["wtpref"]
|
||||
|
||||
self.logger.display(f"Transferring data from '{local_file_path}' to '{remote_file_path}'")
|
||||
try:
|
||||
offset = 0
|
||||
with open(local_file_path, "rb") as file:
|
||||
while chunk := file.read(chunk_size):
|
||||
# Write the data to the remote file
|
||||
res = self.nfs.write(file_handle, offset, len(chunk), chunk, 1, auth=open_auth)
|
||||
if res["status"] != 0:
|
||||
self.logger.fail(f"Error writing to '{remote_file_path}': {self.nfs.status_name(res['status'])}")
|
||||
return
|
||||
offset += len(chunk)
|
||||
|
||||
self.logger.success(f"Data from '{local_file_path}' successfully written to '{remote_file_path}' with permissions 777")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Could not write to '{local_file_path}': {e}")
|
||||
finally:
|
||||
response = self.nfs.close(file_handle, auth=open_auth)
|
||||
if response["status"] != 0:
|
||||
self.logger.fail(f"Error closing '{remote_file_path}': {self.nfs.status_name(response['status'])}")
|
||||
|
||||
# Unmount the share
|
||||
self.mount.umnt(self.auth)
|
||||
self.nfs.unmount()
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error writing file to share {remote_file_path}: {e}")
|
||||
else:
|
||||
@@ -541,23 +549,11 @@ class nfs(connection):
|
||||
|
||||
def chmod(self):
|
||||
try:
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
self.connect_nfs()
|
||||
|
||||
# Mount the NFS share or get the root handle
|
||||
if self.root_escape and not self.args.share:
|
||||
mount_fh = self.escape_fh
|
||||
elif not self.args.share:
|
||||
self.logger.fail("No root escape possible, please specify a share")
|
||||
mount_fh = self.selected_root_handle()
|
||||
if mount_fh is None:
|
||||
return
|
||||
else:
|
||||
mnt_info = self.mount.mnt(self.args.share, self.auth)
|
||||
if mnt_info["status"] != 0:
|
||||
self.logger.fail(f"Error mounting share {self.args.share}: {NFSSTAT3[mnt_info['status']]}")
|
||||
return
|
||||
mount_fh = mnt_info["mountinfo"]["fhandle"]
|
||||
|
||||
# Iterate over the path
|
||||
curr_fh = mount_fh
|
||||
@@ -568,7 +564,7 @@ class nfs(connection):
|
||||
# If target dir is "" or "/" without filter we would get one item with [""]
|
||||
for sub_path in list(filter(None, file_path.lstrip("/").split("/"))):
|
||||
self.update_auth(curr_fh)
|
||||
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
|
||||
res = self.nfs.lookup(curr_fh, sub_path, auth=self.rpc_auth)
|
||||
|
||||
# If the path does not exist, create it
|
||||
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
|
||||
@@ -580,16 +576,16 @@ class nfs(connection):
|
||||
self.update_auth(curr_fh)
|
||||
|
||||
# Checking if file_name already exists on remote file path
|
||||
lookup_response = self.nfs3.lookup(curr_fh, file_name, auth=self.auth)
|
||||
lookup_response = self.nfs.lookup(curr_fh, file_name, auth=self.rpc_auth)
|
||||
|
||||
if "resfail" in lookup_response and lookup_response["status"] == NFS3ERR_NOENT:
|
||||
self.logger.fail(f"File '{file_name}' does not exist on path '{file_path}/'")
|
||||
return
|
||||
|
||||
current_privs = self.nfs3.getattr(lookup_response["resok"]["object"]["data"], auth=self.auth)["attributes"]["mode"]
|
||||
res = self.nfs3.setattr(lookup_response["resok"]["object"]["data"], mode=privs, auth=self.auth)
|
||||
current_privs = self.nfs.getattr(lookup_response["resok"]["object"]["data"], auth=self.rpc_auth)["attributes"]["mode"]
|
||||
res = self.nfs.setattr(lookup_response["resok"]["object"]["data"], mode=privs, auth=self.rpc_auth)
|
||||
if "resfail" in res:
|
||||
self.logger.fail(f"Failed to change permissions for '{filepath}': {NFSSTAT3[res['status']]}")
|
||||
self.logger.fail(f"Failed to change permissions for '{filepath}': {self.nfs.status_name(res['status'])}")
|
||||
else:
|
||||
self.logger.success(f"Permissions for '{filepath}' successfully changed from {format(current_privs, 'o')} to {format(privs, 'o')}")
|
||||
except Exception as e:
|
||||
@@ -611,12 +607,12 @@ class nfs(connection):
|
||||
"""
|
||||
# First enumerate the directory and try to find a file/dir that contains the fid_type (4th position: handle[3])
|
||||
# See: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25
|
||||
dir_data = self.format_directory(self.nfs3.readdirplus(mount_fh, auth=self.auth))
|
||||
dir_data = self.format_directory(self.nfs.readdirplus(mount_fh, auth=self.rpc_auth))
|
||||
filesystem = FileID.unknown
|
||||
for entry in dir_data:
|
||||
# Check if "." is already the root directory
|
||||
if entry["name"] == b".":
|
||||
if entry["name_handle"]["handle"]["data"][0] in [b"\x02", b"\x80"]:
|
||||
if entry["name_handle"]["handle"]["data"][0] in [2, 0x80]:
|
||||
self.logger.debug("Exported share is already the root directory")
|
||||
return [entry["name_handle"]["handle"]["data"]]
|
||||
elif entry["name"] == b"..":
|
||||
@@ -635,8 +631,12 @@ class nfs(connection):
|
||||
self.logger.debug(f"Filesystem type: {filesystem}")
|
||||
|
||||
# Generate the root handle depending on the filesystem type and preserve the file_id (respect the length)
|
||||
if len(mount_fh) < 4 or mount_fh[2] not in fsid_lens:
|
||||
return []
|
||||
fh_fsid_type = mount_fh[2]
|
||||
fh_fsid_len = fsid_lens[fh_fsid_type]
|
||||
if len(mount_fh) < 4 + fh_fsid_len:
|
||||
return []
|
||||
root_handles = []
|
||||
|
||||
# Generate possible root handles
|
||||
@@ -649,12 +649,12 @@ class nfs(connection):
|
||||
if filesystem in [FileID.btrfs, FileID.unknown]:
|
||||
# Iterate over btrfs subvolumes, use 16 as default similar to the guys from nfs-security-tooling
|
||||
for i in range(16):
|
||||
subvolume = int.to_bytes(i) + b"\x01\x00\x00"
|
||||
subvolume = i.to_bytes(1, "big") + b"\x01\x00\x00"
|
||||
root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # ruff: ignore[missing-whitespace-around-arithmetic-operator]
|
||||
|
||||
return root_handles
|
||||
|
||||
def try_root_escape(self) -> bool:
|
||||
def try_root_escape(self) -> bool | None:
|
||||
"""
|
||||
With an established connection look for a share that can be escaped to the root filesystem.
|
||||
If successfull, self.escape_share and self.escape_fh will be populated.
|
||||
@@ -663,58 +663,44 @@ class nfs(connection):
|
||||
-------
|
||||
bool: True if root escape was successful
|
||||
"""
|
||||
if not self.nfs3:
|
||||
if not self.nfs:
|
||||
raise Exception("NFS connection is not established")
|
||||
|
||||
output_export = str(self.mount.export())
|
||||
reg = re.compile(r"ex_dir=b'([^']*)'") # Get share names
|
||||
shares = list(reg.findall(output_export))
|
||||
|
||||
shares = [share for share, network in self.nfs.exports()]
|
||||
evaluated = False
|
||||
self.logger.debug(f"Trying root escape on shares: {shares}")
|
||||
for share in shares:
|
||||
mount_info = self.mount.mnt(share, self.auth)
|
||||
mount_info = self.nfs.root_handle(share)
|
||||
if mount_info["status"] != 0:
|
||||
self.logger.debug(f"Root escape: can't list directory {share}: {NFSSTAT3[mount_info['status']]}")
|
||||
self.mount.umnt(self.auth)
|
||||
self.logger.debug(f"Root escape: can't list directory {share}: {self.nfs.status_name(mount_info['status'])}")
|
||||
self.nfs.unmount()
|
||||
continue
|
||||
mount_fh = mount_info["mountinfo"]["fhandle"]
|
||||
try:
|
||||
possible_root_fhs = self.get_root_handles(mount_fh)
|
||||
for fh in possible_root_fhs:
|
||||
if "resfail" not in self.nfs3.readdir(fh, auth=self.auth):
|
||||
attributes = self.nfs.getattr(fh, auth=self.rpc_auth)
|
||||
directory = self.nfs.readdir(fh, auth=self.rpc_auth)
|
||||
evaluated = True
|
||||
if attributes["status"] == 0 and directory["status"] == 0:
|
||||
self.logger.info(f"Root escape successful on share '{share}' with handle: {fh.hex()}")
|
||||
self.escape_share = share
|
||||
self.escape_fh = fh
|
||||
self.mount.umnt(self.auth)
|
||||
self.nfs.unmount()
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error trying root escape on share '{share}': {e}")
|
||||
self.mount.umnt(self.auth)
|
||||
return False
|
||||
self.nfs.unmount()
|
||||
return False if evaluated else None
|
||||
|
||||
def ls(self):
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
self.connect_nfs()
|
||||
|
||||
# Remove leading or trailing slashes
|
||||
self.args.ls = self.args.ls.lstrip("/").rstrip("/")
|
||||
|
||||
# NORMAL LS CALL (without root escape)
|
||||
if self.args.share:
|
||||
mount_info = self.mount.mnt(self.args.share, self.auth)
|
||||
if mount_info["status"] != 0:
|
||||
self.logger.fail(f"Could not mount share {self.args.share}: {NFSSTAT3[mount_info['status']]}")
|
||||
return
|
||||
else:
|
||||
mount_fh = mount_info["mountinfo"]["fhandle"]
|
||||
elif self.root_escape:
|
||||
# Interestingly we don't actually have to mount the share if we already got the handle
|
||||
self.logger.success(f"Successful escape on share: {self.escape_share}")
|
||||
mount_fh = self.escape_fh
|
||||
else:
|
||||
self.logger.fail("No root escape possible, please specify a share")
|
||||
mount_fh = self.selected_root_handle()
|
||||
if mount_fh is None:
|
||||
return
|
||||
|
||||
# We got a path to look up
|
||||
@@ -725,13 +711,13 @@ class nfs(connection):
|
||||
for sub_path in list(filter(None, self.args.ls.split("/"))):
|
||||
# Update UID and GID for the path
|
||||
self.update_auth(curr_fh)
|
||||
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
|
||||
res = self.nfs.lookup(curr_fh, sub_path, auth=self.rpc_auth)
|
||||
|
||||
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
|
||||
self.logger.fail(f"Unknown path: {self.args.ls!r}")
|
||||
return
|
||||
elif "resfail" in res:
|
||||
self.logger.fail(f"Error on looking up path '{sub_path}': {NFSSTAT3[res['status']]}")
|
||||
self.logger.fail(f"Error on looking up path '{sub_path}': {self.nfs.status_name(res['status'])}")
|
||||
return
|
||||
# If file then break and only display file
|
||||
if res["resok"]["obj_attributes"]["attributes"]["type"] == NF3REG:
|
||||
@@ -742,9 +728,9 @@ class nfs(connection):
|
||||
# Update the UID and GID for the file/dir
|
||||
self.update_auth(curr_fh)
|
||||
|
||||
dir_listing = self.nfs3.readdirplus(curr_fh, auth=self.auth)
|
||||
dir_listing = self.nfs.readdirplus(curr_fh, auth=self.rpc_auth)
|
||||
if dir_listing["status"] != 0:
|
||||
self.logger.fail(f"Error on listing directory: {NFSSTAT3[dir_listing['status']]}")
|
||||
self.logger.fail(f"Error on listing directory: {self.nfs.status_name(dir_listing['status'])}")
|
||||
return
|
||||
content = self.format_directory(dir_listing)
|
||||
|
||||
@@ -752,7 +738,7 @@ class nfs(connection):
|
||||
while not dir_listing["resok"]["reply"]["eof"]:
|
||||
cookie_verf = dir_listing["resok"]["cookieverf"]
|
||||
cookie = content[-1]["cookie"]
|
||||
dir_listing = self.nfs3.readdirplus(curr_fh, cookie=cookie, cookie_verf=cookie_verf, auth=self.auth)
|
||||
dir_listing = self.nfs.readdirplus(curr_fh, cookie=cookie, cookie_verf=cookie_verf, auth=self.rpc_auth)
|
||||
more_content = self.format_directory(dir_listing)
|
||||
content.extend(more_content)
|
||||
|
||||
@@ -761,7 +747,7 @@ class nfs(connection):
|
||||
for item in content:
|
||||
if not item["name_attributes"]["present"]:
|
||||
try:
|
||||
res = self.nfs3.lookup(curr_fh, item["name"].decode(), auth=self.auth)
|
||||
res = self.nfs.lookup(curr_fh, item["name"].decode(), auth=self.rpc_auth)
|
||||
item["name_attributes"]["attributes"] = res["resok"]["obj_attributes"]["attributes"]
|
||||
item["name_attributes"]["present"] = True
|
||||
item["name_handle"]["handle"] = res["resok"]["object"]
|
||||
@@ -770,35 +756,20 @@ class nfs(connection):
|
||||
self.logger.debug(f"Error on getting attributes for {item['name'].decode()}: {e}")
|
||||
|
||||
# If the requested path is a file, we filter out all other files
|
||||
path = f"{self.args.share if self.args.share else ''}/{self.args.ls}"
|
||||
path = f"{(self.args.share or '').rstrip('/')}/{self.args.ls}"
|
||||
if is_file:
|
||||
content = [x for x in content if x["name"].decode() == sub_path]
|
||||
path = path.rsplit("/", 1)[0] # Remove the file from the path
|
||||
self.print_directory(content, path)
|
||||
|
||||
def cat(self):
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
self.connect_nfs()
|
||||
|
||||
# Remove leading or trailing slashes
|
||||
self.args.cat = self.args.cat.lstrip("/").rstrip("/")
|
||||
|
||||
# NORMAL LS CALL (without root escape)
|
||||
if self.args.share:
|
||||
mount_info = self.mount.mnt(self.args.share, self.auth)
|
||||
if mount_info["status"] != 0:
|
||||
self.logger.fail(f"Could not mount share {self.args.share}: {NFSSTAT3[mount_info['status']]}")
|
||||
return
|
||||
else:
|
||||
mount_fh = mount_info["mountinfo"]["fhandle"]
|
||||
elif self.root_escape:
|
||||
# Interestingly we don't actually have to mount the share if we already got the handle
|
||||
self.logger.success(f"Successful escape on share: {self.escape_share}")
|
||||
mount_fh = self.escape_fh
|
||||
else:
|
||||
self.logger.fail("No root escape possible, please specify a share")
|
||||
mount_fh = self.selected_root_handle()
|
||||
if mount_fh is None:
|
||||
return
|
||||
|
||||
# We got a path to look up
|
||||
@@ -809,13 +780,13 @@ class nfs(connection):
|
||||
for sub_path in list(filter(None, self.args.cat.split("/"))):
|
||||
# Update UID and GID for the path
|
||||
self.update_auth(curr_fh)
|
||||
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
|
||||
res = self.nfs.lookup(curr_fh, sub_path, auth=self.rpc_auth)
|
||||
|
||||
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
|
||||
self.logger.fail(f"Unknown path: {self.args.cat!r}")
|
||||
return
|
||||
elif "resfail" in res:
|
||||
self.logger.fail(f"Error on looking up path '{sub_path}': {NFSSTAT3[res['status']]}")
|
||||
self.logger.fail(f"Error on looking up path '{sub_path}': {self.nfs.status_name(res['status'])}")
|
||||
return
|
||||
|
||||
curr_fh = res["resok"]["object"]["data"]
|
||||
@@ -837,10 +808,10 @@ class nfs(connection):
|
||||
offset = 0
|
||||
eof = False
|
||||
while not eof:
|
||||
file_data = self.nfs3.read(curr_fh, offset, auth=self.auth)
|
||||
file_data = self.nfs.read(curr_fh, offset, auth=self.rpc_auth)
|
||||
|
||||
if "resfail" in file_data:
|
||||
self.logger.fail(f"Failed to retrieve data for '{self.args.cat}': {NFSSTAT3[file_data['status']]}")
|
||||
self.logger.fail(f"Failed to retrieve data for '{self.args.cat}': {self.nfs.status_name(file_data['status'])}")
|
||||
return
|
||||
|
||||
else:
|
||||
@@ -866,9 +837,12 @@ class nfs(connection):
|
||||
"""
|
||||
# Sort items linux-like by name
|
||||
content = sorted(content, key=lambda x: x["name"].lower())
|
||||
self.logger.highlight(f"{'UID':<11}{'Perms':<7}{'File Size':<14}{'File Path'}")
|
||||
self.logger.highlight(f"{'---':<11}{'-----':<7}{'---------':<14}{'---------'}")
|
||||
self.logger.highlight(f"{'UID':<11} {'Perms':<7}{'File Size':<14}{'File Path'}")
|
||||
self.logger.highlight(f"{'---':<11} {'-----':<7}{'---------':<14}{'---------'}")
|
||||
directory_auth = dict(self.auth)
|
||||
for item in content:
|
||||
if self.args.nfs_auth == "sys":
|
||||
self.auth.update(directory_auth)
|
||||
if not item["name_attributes"]["present"] or not item["name_handle"]["present"]:
|
||||
uid = "-"
|
||||
perms = "----"
|
||||
@@ -876,19 +850,25 @@ class nfs(connection):
|
||||
else:
|
||||
uid = item["name_attributes"]["attributes"]["uid"]
|
||||
is_dir = "d" if item["name_attributes"]["attributes"]["type"] == 2 else "-"
|
||||
self.update_auth(item["name_handle"]["handle"]["data"])
|
||||
read_perm, write_perm, exec_perm = self.get_permissions(item["name_handle"]["handle"]["data"])
|
||||
perms = f"{is_dir}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{'x' if exec_perm else '-'}"
|
||||
file_size = convert_size(item["name_attributes"]["attributes"]["size"])
|
||||
try:
|
||||
self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode()}")
|
||||
except UnicodeDecodeError:
|
||||
self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode('CP437')}")
|
||||
self.logger.highlight(f"{uid:<11} {perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode()}")
|
||||
except UnicodeDecodeError as e:
|
||||
self.logger.debug(f"NFS filename is not UTF-8: {e}")
|
||||
self.logger.highlight(f"{uid:<11} {perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode('CP437')}")
|
||||
if self.args.nfs_auth == "sys":
|
||||
self.auth.update(directory_auth)
|
||||
|
||||
def format_directory(self, raw_directory):
|
||||
"""Convert the chained directory entries to a list of the entries"""
|
||||
if "resfail" in raw_directory:
|
||||
self.logger.debug("Insufficient Permissions, NFS returned 'resfail'")
|
||||
return {}
|
||||
return []
|
||||
if not raw_directory["resok"]["reply"]["entries"]:
|
||||
return []
|
||||
items = []
|
||||
nextentry = raw_directory["resok"]["reply"]["entries"][0]
|
||||
while nextentry:
|
||||
@@ -900,10 +880,13 @@ class nfs(connection):
|
||||
|
||||
def update_auth(self, file_handle):
|
||||
"""Update the UID and GID for the file handle"""
|
||||
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
|
||||
attrs = self.nfs.getattr(file_handle, auth=self.rpc_auth)
|
||||
self.logger.debug(f"Updating auth with UID: {attrs['attributes']['uid']} and GID: {attrs['attributes']['gid']}")
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
self.auth["gid"] = attrs["attributes"]["gid"]
|
||||
if self.args.nfs_auth == "sys" and isinstance(attrs["attributes"]["uid"], int):
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
if self.args.nfs_auth == "sys" and isinstance(attrs["attributes"]["gid"], int):
|
||||
self.auth["gid"] = attrs["attributes"]["gid"]
|
||||
return attrs
|
||||
|
||||
|
||||
def convert_size(size_bytes):
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
def proto_args(parser, parents):
|
||||
nfs_parser = parser.add_parser("nfs", help="own stuff using NFS", parents=parents)
|
||||
nfs_parser.add_argument("--port", type=int, default=111, help="NFS portmapper port (default: %(default)s)")
|
||||
nfs_parser.add_argument("--nfs-timeout", type=int, default=5, help="NFS connection timeout (default: %(default)ss)")
|
||||
nfs_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NT hash(es) to use for Kerberos authentication")
|
||||
nfs_parser.add_argument("-d", "--domain", metavar="DOMAIN", dest="domain", help="Kerberos realm/domain")
|
||||
nfs_parser.add_argument("--port", type=int, help="NFS port to probe and connect to (default: 2049)")
|
||||
nfs_parser.add_argument("--nfs-version", choices=("3", "4.0", "4.1", "4.2"), help="NFS protocol version (default: prefer 3, otherwise latest supported NFSv4)")
|
||||
nfs_parser.add_argument("--nfs-auth", choices=("none", "sys", "krb5", "krb5i", "krb5p"), default="sys", help="NFS security flavor (default: %(default)s)")
|
||||
nfs_parser.add_argument("--nfs-timeout", type=int, default=2, help="NFS connection timeout (default: %(default)ss)")
|
||||
|
||||
dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration")
|
||||
dgroup.add_argument("--share", help="Specify a share, e.g. for --ls, --get-file, --put-file")
|
||||
|
||||
+27
-5
@@ -334,8 +334,30 @@ netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
|
||||
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce --continue-on-success
|
||||
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE
|
||||
##### NFS
|
||||
netexec nfs TARGET_HOST -u "" -p "" --shares
|
||||
netexec nfs TARGET_HOST -u "" -p "" --enum-shares
|
||||
netexec nfs TARGET_HOST -u "" -p "" --get-file /NFStest/test/test.txt ../test.txt
|
||||
netexec nfs TARGET_HOST -u "" -p "" --put-file ../test.txt /NFStest/test
|
||||
netexec nfs TARGET_HOST -u "" -p "" --chmod 666 /NFStest/test/test.txt
|
||||
netexec nfs TARGET_HOST --nfs-version 4.0 --nfs-auth none --shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.0 --nfs-auth sys --enum-shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.1 --nfs-auth sys --enum-shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.2 --nfs-auth sys --enum-shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.0 --nfs-auth krb5 --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.1 --nfs-auth krb5 --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.2 --nfs-auth krb5 --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.0 --nfs-auth krb5i --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.1 --nfs-auth krb5i --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.2 --nfs-auth krb5i --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.0 --nfs-auth krb5p --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.1 --nfs-auth krb5p --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-version 4.2 --nfs-auth krb5p --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-auth sys --shares
|
||||
netexec nfs TARGET_HOST --nfs-auth sys --enum-shares
|
||||
netexec nfs TARGET_HOST --nfs-auth krb5 --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-auth krb5i --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-auth krb5p --use-kcache --shares
|
||||
netexec nfs TARGET_HOST --nfs-version 3 --nfs-auth sys --get-file /NFStest/test/test.txt ../test.txt
|
||||
netexec nfs TARGET_HOST --nfs-version 3 --nfs-auth sys --put-file ../test.txt /NFStest/test
|
||||
netexec nfs TARGET_HOST --nfs-version 3 --nfs-auth sys --chmod 666 /NFStest/test/test.txt
|
||||
netexec nfs TARGET_HOST --nfs-version 4.0 --nfs-auth sys --share / --ls /
|
||||
netexec nfs TARGET_HOST --nfs-version 4.1 --nfs-auth sys --share / --ls /
|
||||
netexec nfs TARGET_HOST --nfs-version 4.2 --nfs-auth sys --share / --ls /
|
||||
netexec nfs TARGET_HOST --nfs-version 4.1 --nfs-auth sys --share / --put-file ../test.txt /pynfsclient/test.txt
|
||||
netexec nfs TARGET_HOST --nfs-version 4.0 --nfs-auth sys --share / --get-file /pynfsclient/test.txt ../test.txt
|
||||
netexec nfs TARGET_HOST --nfs-version 4.2 --nfs-auth sys --share / --chmod 666 /pynfsclient/test.txt
|
||||
@@ -0,0 +1,456 @@
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
|
||||
|
||||
PROTOCOL_PATH = Path(__file__).parents[1] / "nxc" / "protocols" / "nfs.py"
|
||||
PROTOCOL_ARGS_PATH = Path(__file__).parents[1] / "nxc" / "protocols" / "nfs" / "proto_args.py"
|
||||
PROTOCOL_MODULE = ProtocolLoader().load_protocol(str(PROTOCOL_PATH))
|
||||
proto_args = ProtocolLoader().load_protocol(str(PROTOCOL_ARGS_PATH)).proto_args
|
||||
nfs = PROTOCOL_MODULE.nfs
|
||||
|
||||
|
||||
class Logger:
|
||||
def __init__(self):
|
||||
self.messages = []
|
||||
|
||||
def debug(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
def info(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
def fail(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
def success(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
def display(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
def highlight(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self):
|
||||
self.hosts = []
|
||||
|
||||
def add_host(self, *host):
|
||||
self.hosts.append(host)
|
||||
|
||||
|
||||
def make_protocol(version="4.0", auth="sys", port=None, share=None):
|
||||
protocol = object.__new__(nfs)
|
||||
protocol.args = SimpleNamespace(nfs_version=version, nfs_auth=auth, nfs_timeout=2, port=port, share=share, use_kcache=False, domain=None, username=[], kdcHost=None)
|
||||
protocol.host = "192.0.2.1"
|
||||
protocol.hostname = "server.example"
|
||||
protocol.port = port or 2049
|
||||
protocol.nfs = None
|
||||
protocol.nfs_version = version
|
||||
protocol.nfs_versions = () if version is None else (version,)
|
||||
protocol.discovery = None
|
||||
protocol.auth = {
|
||||
"flavor": 1,
|
||||
"machine_name": "NXC123",
|
||||
"uid": 0,
|
||||
"gid": 0,
|
||||
"aux_gid": [],
|
||||
}
|
||||
protocol.rpc_auth = protocol.auth if auth == "sys" else None
|
||||
protocol.logger = Logger()
|
||||
protocol.db = Database()
|
||||
protocol.root_escape = None
|
||||
protocol.escape_share = None
|
||||
protocol.escape_fh = b""
|
||||
protocol.authenticated = False
|
||||
protocol.credentials = None
|
||||
protocol.proto_logger = lambda: None
|
||||
return protocol
|
||||
|
||||
|
||||
def discovery(*versions, nfs3_port=None, inconclusive=()):
|
||||
return SimpleNamespace(supported=versions, nfs3_port=nfs3_port, inconclusive=inconclusive)
|
||||
|
||||
|
||||
def client_class(calls):
|
||||
class Client:
|
||||
def __init__(self, host, version, port, timeout, auth):
|
||||
calls.append(("init", host, version, port, timeout, auth))
|
||||
self.version = version
|
||||
self.port = port or 2049
|
||||
self.auth = auth
|
||||
self.connected = False
|
||||
|
||||
def connect(self):
|
||||
self.connected = True
|
||||
calls.append(("connect", self.version))
|
||||
return self
|
||||
|
||||
def disconnect(self):
|
||||
self.connected = False
|
||||
calls.append(("disconnect", self.version))
|
||||
|
||||
return Client
|
||||
|
||||
|
||||
def test_argument_defaults_use_auto_discovery():
|
||||
parser = ArgumentParser()
|
||||
proto_args(parser.add_subparsers(dest="protocol"), [])
|
||||
args = parser.parse_args(["nfs", "--shares"])
|
||||
assert args.nfs_version is None
|
||||
assert args.nfs_auth == "sys"
|
||||
assert args.port is None
|
||||
assert args.nfs_timeout == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["3", "4.0", "4.1", "4.2"])
|
||||
def test_argument_parser_accepts_exact_versions(version):
|
||||
parser = ArgumentParser()
|
||||
proto_args(parser.add_subparsers(dest="protocol"), [])
|
||||
assert parser.parse_args(["nfs", "--nfs-version", version]).nfs_version == version
|
||||
|
||||
|
||||
def test_argument_parser_rejects_legacy_nfsv4_value():
|
||||
parser = ArgumentParser()
|
||||
proto_args(parser.add_subparsers(dest="protocol"), [])
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args(["nfs", "--nfs-version", "4"])
|
||||
|
||||
|
||||
def test_auto_discovery_prefers_nfsv3(monkeypatch):
|
||||
calls = []
|
||||
discovered = discovery("3", "4.0", "4.1", "4.2", nfs3_port=2050)
|
||||
|
||||
def discover(*args):
|
||||
calls.append(("discover", args))
|
||||
return discovered
|
||||
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "discover_nfs_versions", discover)
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "NFSClient", client_class(calls))
|
||||
protocol = make_protocol(None)
|
||||
|
||||
assert protocol.create_conn_obj()
|
||||
assert protocol.nfs_version == "3"
|
||||
assert protocol.nfs_versions == discovered.supported
|
||||
assert calls[0] == ("discover", ("192.0.2.1", protocol.rpc_auth, 2049, 2, 111))
|
||||
assert calls[1] == ("init", "192.0.2.1", "3", 2050, 2, protocol.rpc_auth)
|
||||
|
||||
|
||||
def test_auto_discovery_uses_latest_supported_nfsv4(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "discover_nfs_versions", lambda *args: discovery("4.0", "4.2"))
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "NFSClient", client_class(calls))
|
||||
protocol = make_protocol(None)
|
||||
|
||||
assert protocol.create_conn_obj()
|
||||
assert protocol.nfs_version == "4.2"
|
||||
assert calls[0] == ("init", "192.0.2.1", "4.2", None, 2, protocol.rpc_auth)
|
||||
|
||||
|
||||
def test_conclusively_unsupported_explicit_version_fails_without_fallback(monkeypatch):
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "discover_nfs_versions", lambda *args: discovery("4.1", "4.2"))
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "NFSClient", lambda *args: (_ for _ in ()).throw(AssertionError("client constructed")))
|
||||
protocol = make_protocol("3")
|
||||
|
||||
assert not protocol.create_conn_obj()
|
||||
assert protocol.nfs_version == "3"
|
||||
assert "NFSv3 is not supported by the target" in protocol.logger.messages[-1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("version", "label"), [("3", "rpcbind"), ("3", "3@2049"), ("4.0", "4.0")])
|
||||
def test_inconclusive_explicit_version_is_still_attempted(monkeypatch, version, label):
|
||||
calls = []
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "discover_nfs_versions", lambda *args: discovery("4.2", inconclusive=((label, TimeoutError()),)))
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "NFSClient", client_class(calls))
|
||||
protocol = make_protocol(version)
|
||||
|
||||
assert protocol.create_conn_obj()
|
||||
assert protocol.nfs_version == version
|
||||
assert calls[0][2] == version
|
||||
|
||||
|
||||
def test_custom_port_is_used_for_discovery_and_client(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def discover(*args):
|
||||
calls.append(("discover", args))
|
||||
return discovery("4.1")
|
||||
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "discover_nfs_versions", discover)
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "NFSClient", client_class(calls))
|
||||
protocol = make_protocol("4.1", port=3049)
|
||||
|
||||
assert protocol.create_conn_obj()
|
||||
assert calls[0] == ("discover", ("192.0.2.1", protocol.rpc_auth, 3049, 2, 111))
|
||||
assert calls[1] == ("init", "192.0.2.1", "4.1", 3049, 2, protocol.rpc_auth)
|
||||
|
||||
|
||||
def test_auto_discovery_rejects_hosts_without_a_supported_version(monkeypatch):
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "discover_nfs_versions", lambda *args: discovery())
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "NFSClient", lambda *args: (_ for _ in ()).throw(AssertionError("client constructed")))
|
||||
protocol = make_protocol(None)
|
||||
assert not protocol.create_conn_obj()
|
||||
assert "No supported NFS version discovered" in protocol.logger.messages[-1]
|
||||
|
||||
|
||||
def test_host_output_includes_discovered_and_selected_versions():
|
||||
protocol = make_protocol("3")
|
||||
protocol.nfs_versions = ("3", "4.0", "4.1", "4.2")
|
||||
protocol.print_host_info()
|
||||
assert "Supported NFS versions: (3, 4.0, 4.1, 4.2) NFSv3 sys" in protocol.logger.messages[-1]
|
||||
|
||||
|
||||
def test_enum_host_info_uses_the_selected_generic_client():
|
||||
protocol = make_protocol("4.2")
|
||||
protocol.nfs_versions = ("4.1", "4.2")
|
||||
protocol.nfs = object()
|
||||
protocol.try_root_escape = lambda: True
|
||||
protocol.enum_host_info()
|
||||
assert protocol.root_escape is True
|
||||
assert protocol.db.hosts[-1][2:] == (2049, ("4.1", "4.2"), True)
|
||||
|
||||
|
||||
def test_kerberos_rebuilds_the_selected_generic_client(monkeypatch):
|
||||
events = []
|
||||
|
||||
class Client:
|
||||
def __init__(self, host, version, port, timeout, auth):
|
||||
self.port = port or 2049
|
||||
self.auth = auth
|
||||
events.append(("init", version, auth))
|
||||
|
||||
def connect(self):
|
||||
events.append(("connect",))
|
||||
|
||||
def disconnect(self):
|
||||
events.append(("disconnect",))
|
||||
|
||||
def establish_gss(self, initiator, service):
|
||||
events.append(("gss", initiator.host, service))
|
||||
self.auth = "rpcsec-gss"
|
||||
|
||||
class Initiator:
|
||||
def __init__(self, host, credentials):
|
||||
self.host = host
|
||||
self.credentials = credentials
|
||||
|
||||
monkeypatch.setattr(PROTOCOL_MODULE, "NFSClient", Client)
|
||||
monkeypatch.setitem(sys.modules, "pyNfsClient.kerberos", SimpleNamespace(KerberosInitiator=Initiator))
|
||||
protocol = make_protocol("4.1", auth="krb5p")
|
||||
protocol.discovery = discovery("4.1")
|
||||
protocol.try_root_escape = lambda: False
|
||||
|
||||
assert protocol.kerberos_login("NFS.TEST", "nfsclient", password="secret", kdcHost="192.0.2.2")
|
||||
assert ("init", "4.1", protocol.auth) in events
|
||||
assert ("gss", "192.0.2.1", "krb5p") in events
|
||||
assert protocol.rpc_auth == "rpcsec-gss"
|
||||
|
||||
|
||||
def test_selected_root_handle_delegates_to_generic_client():
|
||||
protocol = make_protocol("4.2", share="/export")
|
||||
protocol.nfs = SimpleNamespace(
|
||||
root_handle=lambda share: {"status": 0, "mountinfo": {"fhandle": b"root"}},
|
||||
status_name=lambda status: str(status),
|
||||
)
|
||||
assert protocol.selected_root_handle() == b"root"
|
||||
|
||||
|
||||
def test_nfsv3_without_share_still_requires_root_escape():
|
||||
protocol = make_protocol("3")
|
||||
protocol.nfs = SimpleNamespace()
|
||||
assert protocol.selected_root_handle() is None
|
||||
assert "please specify a share" in protocol.logger.messages[-1]
|
||||
|
||||
|
||||
def test_root_escape_validation_uses_the_generic_client():
|
||||
protocol = make_protocol()
|
||||
protocol.nfs = SimpleNamespace(
|
||||
getattr=lambda file_handle, auth=None: {"status": 0, "attributes": {"type": 2}},
|
||||
readdir=lambda file_handle, auth=None: {"status": 0, "resok": {}},
|
||||
exports=lambda: [("/", ["Everyone"])],
|
||||
root_handle=lambda share: {"status": 0, "mountinfo": {"fhandle": b"export"}},
|
||||
unmount=lambda: None,
|
||||
)
|
||||
protocol.get_root_handles = lambda file_handle: [b"forged"]
|
||||
assert protocol.try_root_escape() is True
|
||||
assert protocol.escape_share == "/"
|
||||
assert protocol.escape_fh == b"forged"
|
||||
|
||||
|
||||
def test_named_owner_does_not_replace_auth_sys_numeric_uid():
|
||||
protocol = make_protocol()
|
||||
protocol.nfs = SimpleNamespace(
|
||||
getattr=lambda file_handle, auth=None: {"status": 0, "attributes": {"uid": "[email protected]", "gid": "[email protected]"}},
|
||||
access=lambda file_handle, access, auth=None: {"status": 0, "resok": {"access": access}},
|
||||
)
|
||||
assert protocol.list_dir(b"root", "/", recurse=0)[0]["uid"] == "[email protected]"
|
||||
assert protocol.auth["uid"] == 0
|
||||
assert protocol.auth["gid"] == 0
|
||||
|
||||
|
||||
def test_print_directory_restores_directory_identity_between_entries():
|
||||
calls = []
|
||||
owners = {b"one": (1000, 100), b"two": (2000, 200)}
|
||||
protocol = make_protocol()
|
||||
protocol.auth.update({"uid": 500, "gid": 50})
|
||||
|
||||
def get_attributes(file_handle, auth=None):
|
||||
calls.append(("getattr", file_handle, auth["uid"], auth["gid"]))
|
||||
uid, gid = owners[file_handle]
|
||||
return {"status": 0, "attributes": {"uid": uid, "gid": gid}}
|
||||
|
||||
def access(file_handle, requested, auth=None):
|
||||
calls.append(("access", file_handle, auth["uid"], auth["gid"]))
|
||||
return {"status": 0, "resok": {"access": requested}}
|
||||
|
||||
protocol.nfs = SimpleNamespace(getattr=get_attributes, access=access)
|
||||
protocol.print_directory(
|
||||
[
|
||||
{
|
||||
"name": b"one",
|
||||
"name_attributes": {"present": True, "attributes": {"uid": 1000, "type": 1, "size": 0}},
|
||||
"name_handle": {"present": True, "handle": {"data": b"one"}},
|
||||
},
|
||||
{
|
||||
"name": b"two",
|
||||
"name_attributes": {"present": True, "attributes": {"uid": 2000, "type": 1, "size": 0}},
|
||||
"name_handle": {"present": True, "handle": {"data": b"two"}},
|
||||
},
|
||||
],
|
||||
"/",
|
||||
)
|
||||
|
||||
assert calls == [
|
||||
("getattr", b"one", 500, 50),
|
||||
("access", b"one", 1000, 100),
|
||||
("access", b"one", 1000, 100),
|
||||
("access", b"one", 1000, 100),
|
||||
("getattr", b"two", 500, 50),
|
||||
("access", b"two", 2000, 200),
|
||||
("access", b"two", 2000, 200),
|
||||
("access", b"two", 2000, 200),
|
||||
]
|
||||
assert protocol.auth["uid"] == 500
|
||||
assert protocol.auth["gid"] == 50
|
||||
|
||||
|
||||
def test_btrfs_root_escape_handles_are_python_310_compatible(monkeypatch):
|
||||
protocol = make_protocol()
|
||||
protocol.nfs = SimpleNamespace(readdirplus=lambda file_handle, auth=None: {})
|
||||
monkeypatch.setattr(protocol, "format_directory", lambda response: [])
|
||||
handles = protocol.get_root_handles(b"\x01\x00\x00\x4d" + b"12345678")
|
||||
assert len(handles) == 18
|
||||
assert handles[2][20:24] == b"\x00\x01\x00\x00"
|
||||
assert handles[-1][20:24] == b"\x0f\x01\x00\x00"
|
||||
|
||||
|
||||
def test_netexec_uses_only_the_generic_client_api():
|
||||
source = PROTOCOL_PATH.read_text()
|
||||
assert "NFSClient" in source
|
||||
assert ".compound(" not in source
|
||||
assert "NFSv40" not in source
|
||||
assert "NFSv41" not in source
|
||||
assert "NFSv42" not in source
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["3", "4.0", "4.1", "4.2"])
|
||||
def test_uid_and_gid_changes_are_passed_to_each_generic_call(version):
|
||||
calls = []
|
||||
protocol = make_protocol(version=version)
|
||||
protocol.args.cat = "file"
|
||||
protocol.connect_nfs = lambda: protocol.nfs
|
||||
protocol.selected_root_handle = lambda: b"root"
|
||||
|
||||
def get_attributes(file_handle, auth=None):
|
||||
calls.append(("getattr", file_handle, auth["uid"], auth["gid"]))
|
||||
uid, gid = (1000, 100) if file_handle == b"root" else (2000, 200)
|
||||
return {"status": 0, "attributes": {"uid": uid, "gid": gid}}
|
||||
|
||||
def lookup(file_handle, name, auth=None):
|
||||
calls.append(("lookup", file_handle, auth["uid"], auth["gid"]))
|
||||
return {"status": 0, "resok": {"object": {"data": b"file"}, "obj_attributes": {"attributes": {"type": PROTOCOL_MODULE.NF3REG}}}}
|
||||
|
||||
def read(file_handle, offset, auth=None):
|
||||
calls.append(("read", file_handle, auth["uid"], auth["gid"]))
|
||||
return {"status": 0, "resok": {"data": b"data", "eof": True}}
|
||||
|
||||
protocol.nfs = SimpleNamespace(getattr=get_attributes, lookup=lookup, read=read)
|
||||
protocol.cat()
|
||||
|
||||
assert calls == [
|
||||
("getattr", b"root", 0, 0),
|
||||
("lookup", b"root", 1000, 100),
|
||||
("getattr", b"file", 1000, 100),
|
||||
("read", b"file", 2000, 200),
|
||||
]
|
||||
|
||||
|
||||
def test_put_file_reuses_open_auth_snapshot_after_uid_detection(tmp_path):
|
||||
local_file = tmp_path / "upload.txt"
|
||||
local_file.write_bytes(b"payload")
|
||||
protocol = make_protocol("4.0")
|
||||
protocol.args.put_file = [str(local_file), "upload.txt"]
|
||||
protocol.selected_root_handle = lambda: b"root"
|
||||
events = []
|
||||
|
||||
def update_auth(file_handle):
|
||||
if file_handle == b"root":
|
||||
protocol.auth.update({"uid": 1000, "gid": 100})
|
||||
else:
|
||||
protocol.auth.update({"uid": 2000, "gid": 200})
|
||||
return {"status": 0, "attributes": {"uid": protocol.auth["uid"], "gid": protocol.auth["gid"]}}
|
||||
|
||||
class Client:
|
||||
def lookup(self, *args, **kwargs):
|
||||
return {"status": PROTOCOL_MODULE.NFS3ERR_NOENT, "resok": None, "resfail": {}}
|
||||
|
||||
def create(self, dir_handle, name, create_mode, mode=None, auth=None):
|
||||
events.append(("create", auth))
|
||||
return {"status": 0, "resok": {"obj": {"handle": {"data": b"file"}}}}
|
||||
|
||||
def fsinfo(self, *args, **kwargs):
|
||||
return {"status": 0, "resok": {"wtpref": 1024}}
|
||||
|
||||
def write(self, file_handle, offset, count, content, stable_how, auth=None):
|
||||
events.append(("write", auth, content))
|
||||
return {"status": 0, "resok": {"count": count}}
|
||||
|
||||
def close(self, file_handle, auth=None):
|
||||
events.append(("close", auth))
|
||||
return {"status": 0}
|
||||
|
||||
def unmount(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def status_name(status):
|
||||
return str(status)
|
||||
|
||||
protocol.nfs = Client()
|
||||
protocol.update_auth = update_auth
|
||||
protocol.put_file()
|
||||
|
||||
assert protocol.auth["uid"] == 2000
|
||||
assert events[0][1]["uid"] == 1000
|
||||
assert events[1][1] is events[0][1]
|
||||
assert events[2][1] is events[0][1]
|
||||
assert events[1][2] == b"payload"
|
||||
|
||||
|
||||
def test_disconnect_delegates_all_cleanup_to_generic_client():
|
||||
class Client:
|
||||
def disconnect(self):
|
||||
self.disconnected = True
|
||||
|
||||
protocol = make_protocol()
|
||||
protocol.nfs = Client()
|
||||
protocol.nfs.disconnected = False
|
||||
protocol.disconnect()
|
||||
assert protocol.nfs.disconnected
|
||||
Reference in new issue
Block a user