From 80839d850e0927fd94931c966dba3e906a08f6f8 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Tue, 4 Jun 2024 10:04:14 +0300 Subject: [PATCH 01/52] New NFS Protocol Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs.py | 224 ++++++++++++++++++ nxc/protocols/nfs/__init__.py | 0 .../nfs/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 136 bytes .../nfs/__pycache__/database.cpython-310.pyc | Bin 0 -> 2141 bytes .../__pycache__/proto_args.cpython-310.pyc | Bin 0 -> 669 bytes nxc/protocols/nfs/database.py | 62 +++++ nxc/protocols/nfs/db_navigator.py | 15 ++ nxc/protocols/nfs/proto_args.py | 10 + 8 files changed, 311 insertions(+) create mode 100644 nxc/protocols/nfs.py create mode 100644 nxc/protocols/nfs/__init__.py create mode 100644 nxc/protocols/nfs/__pycache__/__init__.cpython-310.pyc create mode 100644 nxc/protocols/nfs/__pycache__/database.cpython-310.pyc create mode 100644 nxc/protocols/nfs/__pycache__/proto_args.cpython-310.pyc create mode 100644 nxc/protocols/nfs/database.py create mode 100644 nxc/protocols/nfs/db_navigator.py create mode 100644 nxc/protocols/nfs/proto_args.py diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py new file mode 100644 index 000000000..86aaf1504 --- /dev/null +++ b/nxc/protocols/nfs.py @@ -0,0 +1,224 @@ +from nxc.connection import connection +from nxc.logger import NXCAdapter +from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3 +import socket +import re + + +class nfs(connection): + def __init__(self, args, db, host): + # Setting up NFS protocol attributes + self.protocol = "nfs" + self.port = 111 + self.portmap = None + self.mnt_port = None + self.mount = None + self.auth = {"flavor": 1, + "machine_name": "host1", + "uid": 0, + "gid": 0, + "aux_gid": [], + } + + connection.__init__(self, args, db, host) + + + def proto_logger(self): + # Initializing logger for NFS protocol + self.logger = NXCAdapter( + extra={ + "protocol": "NFS", + "host": self.host, + "port": self.port, + "hostname": self.hostname, + } + ) + + def plaintext_login(self, username, password): + # Doing as Anonymously now. + try: + if self.initialization(): + self.logger.success("Initialization is successfull!") + except Exception as e: + self.logger.fail("Initialization is failed.") + self.logger.debug(f"Error Plaintext login: {self.host}:{self.port} {e}") + finally: + self.disconnection() + + def create_conn_obj(self): + # Creating and connecting socket to NFS host + try: + self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client.connect((self.host, self.port)) + self.logger.info(f"Connection target successful: {self.host}:{self.port}") + except Exception as e: + self.logger.debug(f"Error connecting to NFS host: {self.host}:{self.port} {e}") + return False + return True + + def enum_host_info(self): + self.initialization() + # Enumerating host information + 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"]) + + return self.nfs_versions + + except Exception as e: + self.logger.debug(f"Error checking NFS version: {self.host} {e}") + finally: + self.disconnection() + + def print_host_info(self): + # Printing host banner information + self.logger.display(f"Target supported NFS versions {self.nfs_versions}") + return True + + def disconnection(self): + try: + # Disconnecting from NFS host + if self.mount(): + self.mount.disconnect() + if self.portmap(): + self.portmap.disconnect() + self.logger.info(f"Disconnection successful: {self.host}:{self.port}") + except Exception as e: + self.logger.debug(f"Error during disconnection: {e}") + + def initialization(self): + try: + # Portmap Initialization + self.portmap = Portmap(self.host, timeout=3600) + 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=3600, auth=self.auth) + self.mount.connect() + + return self.portmap, self.mnt_port, self.mount + except Exception as e: + self.logger.debug(f"Error during Initialization: {e}") + + def listdir(self, nfs, file_handle, path, recurse=1): + # Process entries in NFS directory recursively. + def process_entries(entries, path, recurse): + + try: + contents = [] + for entry in entries: + 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): + entry_type = entry["name_attributes"]["attributes"].get("type") + if entry_type == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. + dir_handle = entry["name_handle"]["handle"]["data"] + contents += self.listdir(nfs, dir_handle, item_path, recurse=recurse - 1) + else: + contents.append(item_path) + + if entry["nextentry"]: + # Processing next entries recursively + recurse += 1 + contents += process_entries(entry["nextentry"], path, recurse) + + return contents + + except Exception as e: + self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}") + + try: + if recurse == 0: + return [path + "/"] + + items = self.nfs3.readdirplus(file_handle, auth=self.auth) + entries = items["resok"]["reply"]["entries"] + + return process_entries(entries, path, recurse) + except Exception as e: + self.logger.debug(f"Error on Listing NFS Shares Directories: {self.host}:{self.port} {e}") + + def export_info(self, export_nodes): + # This func for finding filtering all NFS shares and their access range. Using for shares func. + + result = [] + for node in export_nodes: + ex_dir = node.ex_dir.decode() + # Collect the names of the groups associated with this export node + group_names = self.group_names(node.ex_groups) + result.append(f"{ex_dir} {', '.join(group_names)}") + + # If there are more export nodes, process them recursively. More than one share. + if node.ex_next: + result.extend(self.export_info(node.ex_next)) + + return result + + def group_names(self, groups): + # This func for finding all access range of the share(s). Using for shares func. + + 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 + + def shares(self): + try: + # Initializing NFS services + self.initialization() + + # Exporting NFS Shares + for mount in self.export_info(self.mount.export()): + self.logger.highlight(mount) + + except Exception as e: + self.logger.debug(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}") + finally: + self.disconnection() + + def shares_list(self): + try: + self.initialization() + + # NFS Initialization + nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) + self.nfs3 = NFSv3(self.host, nfs_port, 3600, self.auth) + self.nfs3.connect() + + contents = [] + # Mounting NFS Shares + output_export = str(self.mount.export()) + pattern_name = re.compile(r"ex_dir=b'([^']*)'") + matches_name = pattern_name.findall(output_export) + output_name = list(matches_name) + + # Searching files and folders as recursivly on every shares. + for export in output_name: + try: + mount_info = self.mount.mnt(export, self.auth) + contents += self.listdir(self.nfs3, mount_info["mountinfo"]["fhandle"], export) + except Exception as e: + self.logger.fail(f"Can not reaching file(s) on {export}") + self.logger.debug(f"Error on Enum NFS Share {export}: {self.host}:{self.port} {e}") + self.logger.debug("It is probably unknown format or can not access as anonymously.") + continue + + for shares in contents: + self.logger.highlight(shares) + + except Exception as e: + self.logger.debug(f"Error on Listing NFS Shares: {self.host}:{self.port} {e}") + finally: + self.nfs3.disconnect() + self.disconnection() diff --git a/nxc/protocols/nfs/__init__.py b/nxc/protocols/nfs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nxc/protocols/nfs/__pycache__/__init__.cpython-310.pyc b/nxc/protocols/nfs/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9809df110c67474e593d1130ac49711a3130e68a GIT binary patch literal 136 zcmd1j<>g`k0_SS&G!Xq5L?8o3AjbiSi&=m~3PUi1CZpd}^lAoNPQ>>qpl31W0AD@|*SrQ+wS5SG2!zMRBr8Fni4rE9%6OdqG F008Ro9A5wc literal 0 HcmV?d00001 diff --git a/nxc/protocols/nfs/__pycache__/database.cpython-310.pyc b/nxc/protocols/nfs/__pycache__/database.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15c9e877f659d24c4c5b5ca906c8aaa10336daad GIT binary patch literal 2141 zcmbVNPj4GV6rY*hwbzdAn9x%GYY9TI2sKE(AXFhj8qo@BRZXPQmC$OvGqKmq?z%H$ zl2{HxN-rGv00)d}zxPQ!|RbJa6VdF2*MM7G+4-zvBS=G`)>#QvDn`5-h{c&SHRR(<#WK@Pqawc!u2lj_-?_g`#?CEk$6t&i~`SQ zWW+|!$OYarHJEwFbW}g_Pf@aAn#cHEnURy$DWSx)Q6BOA%#N7p%-AtKa%PT|TIB?= zIK5r)rQ6R69(BdRI8!1ni#RRwJTqZ8>TW5)MPaf;YU`({dAHPN&Q}fIk3})a3Q;)| zCDjUmuIjKjh8QZet#Y*ydF4&UV~C`4eu3~rG^D|EC% zUsP^d7Ded7JwooOHY`P}=SdLnC8;UZVO8Imr^+*;UIl@_KinwCW}_#}_JK$@AfOu; zdi6%Jr#CLVe{+0T`Ei^TnTg{LoYdEV)F?cxMc1egPlx)f#ilP^p1F`uDac+7RK~@+6*%&-{ zc<=t^!^go_+mAn5mYQgxiX<27fe{Dh%H}xHdcRa~5pFIW45z;=Lm6Ud|0jrS{IBZq z%KyezDsySzGBhZ~x`6Oaj0h=C@0yH!gK7C-IUZl*OiN z72O49>O*ak6N+{E$f|XTVTbQhas;U|T{X^$%c1IjCBNC^c|;zA)Diswdd|){w@>#; z$a>TFZSxBDhnW;XDodRegW$YK0^SeG{s?O8(;)QJbx>V-C?gcKexeXm<#ExU3_@n@ zt*E$SaBIf;DaKJj6(Jt+{Z&UwAb~e3s>4XK*7d(JfZ#}RM4B9`UcgVNR zBj{}-cH+$FiF-=0RGzqKrNW>B=$GMBwKXIQ`~l881RA2=z>OLgQb3lRqxY=9RSP<) z6p4y2P~L`vx&{Qx+&x=};j&yWY+#4L1dijX8OM3aCldKq96y;Pa=xQpM<6W?|D7y_ zrZz0OdJ~pxuBdmAdk+bAh6REtYl zKaq*Vq(8h_s(hhWHlOd<2CLA*C3$g>&cp7b>IM+|!@D{E;mwo5(TCbSB3;OYYqaJ0bj|q(vCII& literal 0 HcmV?d00001 diff --git a/nxc/protocols/nfs/__pycache__/proto_args.cpython-310.pyc b/nxc/protocols/nfs/__pycache__/proto_args.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..506e7c493978f64b51edf5d3b34396325594eb62 GIT binary patch literal 669 zcmZWn&5qMB5O(a4mTbzFUJy4+dm$1f>J=dbLdyw?#NPHmWo6u-RW`P4N7_c&8`r)A z9C-$=JcX~Ecm+<3(^jg4r5XF1AAjG-R-;i$aQ*uEWqD2s`5nON!WemjW;1j^Ao@zK z3D7H&vohq4E5<{D_72T{M%R%WtOnXez;^7OcJwBK*im=@2@G~|L^}G7!4MvPr}wO5 zm`&jfG8pX`<^q0%`22tgGLGk-)k}NL=I^g^>f^Pnv>#|;WbJI5Os85I_eUXl`vgmP zfczQArD!VmlFw$dJZC<3TP^+I5H?|mefdYB^`>4GmvwV1jc^-Pw-=vu*ezcw!~ZL& zR~&iT;dE-(sKWZxI%VX-nMSr3ZyUEpWt&ns$tzWgs@ig=xD@4@J7HGR;S`A|1H)d& z=y=n#+E~{<{eT`Nae0s=f$y2}*rx(ufx}yAd=}1lBMPsbXB%{r)6imNRHLye**6%5 zQ~R?Dgg#!8KwtBNm@(%5J};DZ#az0}J6RU>U0G&3wCK#RPQNQ;DC+>03GJJKbpsD2RzCS!lM*ab`K)bF0 literal 0 HcmV?d00001 diff --git a/nxc/protocols/nfs/database.py b/nxc/protocols/nfs/database.py new file mode 100644 index 000000000..8f10d151b --- /dev/null +++ b/nxc/protocols/nfs/database.py @@ -0,0 +1,62 @@ +from pathlib import Path +from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy import MetaData, Table +from sqlalchemy.exc import ( + IllegalStateChangeError, + NoInspectionAvailable, + NoSuchTableError, +) +from nxc.logger import nxc_logger +import sys + + +class database: + def __init__(self, db_engine): + self.CredentialsTable = None + self.HostsTable = None + + self.db_engine = db_engine + self.db_path = self.db_engine.url.database + self.protocol = Path(self.db_path).stem.upper() + self.metadata = MetaData() + self.reflect_tables() + session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True) + + Session = scoped_session(session_factory) + # this is still named "conn" when it is the session object; TODO: rename + self.conn = Session() + + @staticmethod + def db_schema(db_conn): + db_conn.execute( + """CREATE TABLE "credentials" ( + "id" integer PRIMARY KEY, + "username" text, + "password" text + )""" + ) + + db_conn.execute( + """CREATE TABLE "hosts" ( + "id" integer PRIMARY KEY, + "ip" text, + "hostname" text, + "port" integer + )""" + ) + + def reflect_tables(self): + pass + + def shutdown_db(self): + try: + self.conn.close() + # due to the async nature of nxc, sometimes session state is a bit messy and this will throw: + # Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and + # this would cause an unexpected state change to + except IllegalStateChangeError as e: + nxc_logger.debug(f"Error while closing session db object: {e}") + + def clear_database(self): + for table in self.metadata.sorted_tables: + self.conn.execute(table.delete()) diff --git a/nxc/protocols/nfs/db_navigator.py b/nxc/protocols/nfs/db_navigator.py new file mode 100644 index 000000000..c712309b9 --- /dev/null +++ b/nxc/protocols/nfs/db_navigator.py @@ -0,0 +1,15 @@ +from nxc.nxcdb import DatabaseNavigator, print_help + + +class navigator(DatabaseNavigator): + def do_clear_database(self, line): + if input("This will destroy all data in the current database, are you SURE you want to run this? (y/n): ") == "y": + self.db.clear_database() + + def help_clear_database(self): + help_string = """ + clear_database + THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE + YOU CANNOT UNDO THIS COMMAND + """ + print_help(help_string) diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py new file mode 100644 index 000000000..f09e5afff --- /dev/null +++ b/nxc/protocols/nfs/proto_args.py @@ -0,0 +1,10 @@ +def proto_args(parser, parents): + ldap_parser = parser.add_parser("nfs", help="NFS", parents=parents) + ldap_parser.add_argument("--port", type=int, default=111, help="NFS port (default: 111)") + + dgroup = ldap_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") + dgroup.add_argument("--shares", action="store_true", help="Authenticate locally to each target") + dgroup.add_argument("--shares-list", action="store_true", help="Listing enumerated shares") + + return parser + From fbfef1b4b06d8367a00c9e451750af8e134ab285 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:24:01 +0300 Subject: [PATCH 02/52] Update e2e_commands.txt Added NFS Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- tests/e2e_commands.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 14ee63d3b..8032ad2a5 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -242,3 +242,6 @@ netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --get test_file.txt 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 +##### RDP +netexec nfs TARGETHOST -u "" -p "" --shares +netexec nfs TARGETHOST -u "" -p "" --shares-list From 9cf6d983e205f376e982c15b4a76ea88fb5dd403 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:26:15 +0300 Subject: [PATCH 03/52] Update e2e_commands.txt Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- tests/e2e_commands.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 8032ad2a5..e70291600 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -242,6 +242,6 @@ netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --get test_file.txt 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 -##### RDP +##### NFS netexec nfs TARGETHOST -u "" -p "" --shares netexec nfs TARGETHOST -u "" -p "" --shares-list From f1894b2c6be62653e8e70a1da3c6ea66dd552e9e Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Wed, 5 Jun 2024 09:01:18 -0400 Subject: [PATCH 04/52] remove commited pyc files --- .../nfs/__pycache__/__init__.cpython-310.pyc | Bin 136 -> 0 bytes .../nfs/__pycache__/database.cpython-310.pyc | Bin 2141 -> 0 bytes .../nfs/__pycache__/proto_args.cpython-310.pyc | Bin 669 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 nxc/protocols/nfs/__pycache__/__init__.cpython-310.pyc delete mode 100644 nxc/protocols/nfs/__pycache__/database.cpython-310.pyc delete mode 100644 nxc/protocols/nfs/__pycache__/proto_args.cpython-310.pyc diff --git a/nxc/protocols/nfs/__pycache__/__init__.cpython-310.pyc b/nxc/protocols/nfs/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 9809df110c67474e593d1130ac49711a3130e68a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 136 zcmd1j<>g`k0_SS&G!Xq5L?8o3AjbiSi&=m~3PUi1CZpd}^lAoNPQ>>qpl31W0AD@|*SrQ+wS5SG2!zMRBr8Fni4rE9%6OdqG F008Ro9A5wc diff --git a/nxc/protocols/nfs/__pycache__/database.cpython-310.pyc b/nxc/protocols/nfs/__pycache__/database.cpython-310.pyc deleted file mode 100644 index 15c9e877f659d24c4c5b5ca906c8aaa10336daad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2141 zcmbVNPj4GV6rY*hwbzdAn9x%GYY9TI2sKE(AXFhj8qo@BRZXPQmC$OvGqKmq?z%H$ zl2{HxN-rGv00)d}zxPQ!|RbJa6VdF2*MM7G+4-zvBS=G`)>#QvDn`5-h{c&SHRR(<#WK@Pqawc!u2lj_-?_g`#?CEk$6t&i~`SQ zWW+|!$OYarHJEwFbW}g_Pf@aAn#cHEnURy$DWSx)Q6BOA%#N7p%-AtKa%PT|TIB?= zIK5r)rQ6R69(BdRI8!1ni#RRwJTqZ8>TW5)MPaf;YU`({dAHPN&Q}fIk3})a3Q;)| zCDjUmuIjKjh8QZet#Y*ydF4&UV~C`4eu3~rG^D|EC% zUsP^d7Ded7JwooOHY`P}=SdLnC8;UZVO8Imr^+*;UIl@_KinwCW}_#}_JK$@AfOu; zdi6%Jr#CLVe{+0T`Ei^TnTg{LoYdEV)F?cxMc1egPlx)f#ilP^p1F`uDac+7RK~@+6*%&-{ zc<=t^!^go_+mAn5mYQgxiX<27fe{Dh%H}xHdcRa~5pFIW45z;=Lm6Ud|0jrS{IBZq z%KyezDsySzGBhZ~x`6Oaj0h=C@0yH!gK7C-IUZl*OiN z72O49>O*ak6N+{E$f|XTVTbQhas;U|T{X^$%c1IjCBNC^c|;zA)Diswdd|){w@>#; z$a>TFZSxBDhnW;XDodRegW$YK0^SeG{s?O8(;)QJbx>V-C?gcKexeXm<#ExU3_@n@ zt*E$SaBIf;DaKJj6(Jt+{Z&UwAb~e3s>4XK*7d(JfZ#}RM4B9`UcgVNR zBj{}-cH+$FiF-=0RGzqKrNW>B=$GMBwKXIQ`~l881RA2=z>OLgQb3lRqxY=9RSP<) z6p4y2P~L`vx&{Qx+&x=};j&yWY+#4L1dijX8OM3aCldKq96y;Pa=xQpM<6W?|D7y_ zrZz0OdJ~pxuBdmAdk+bAh6REtYl zKaq*Vq(8h_s(hhWHlOd<2CLA*C3$g>&cp7b>IM+|!@D{E;mwo5(TCbSB3;OYYqaJ0bj|q(vCII& diff --git a/nxc/protocols/nfs/__pycache__/proto_args.cpython-310.pyc b/nxc/protocols/nfs/__pycache__/proto_args.cpython-310.pyc deleted file mode 100644 index 506e7c493978f64b51edf5d3b34396325594eb62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 669 zcmZWn&5qMB5O(a4mTbzFUJy4+dm$1f>J=dbLdyw?#NPHmWo6u-RW`P4N7_c&8`r)A z9C-$=JcX~Ecm+<3(^jg4r5XF1AAjG-R-;i$aQ*uEWqD2s`5nON!WemjW;1j^Ao@zK z3D7H&vohq4E5<{D_72T{M%R%WtOnXez;^7OcJwBK*im=@2@G~|L^}G7!4MvPr}wO5 zm`&jfG8pX`<^q0%`22tgGLGk-)k}NL=I^g^>f^Pnv>#|;WbJI5Os85I_eUXl`vgmP zfczQArD!VmlFw$dJZC<3TP^+I5H?|mefdYB^`>4GmvwV1jc^-Pw-=vu*ezcw!~ZL& zR~&iT;dE-(sKWZxI%VX-nMSr3ZyUEpWt&ns$tzWgs@ig=xD@4@J7HGR;S`A|1H)d& z=y=n#+E~{<{eT`Nae0s=f$y2}*rx(ufx}yAd=}1lBMPsbXB%{r)6imNRHLye**6%5 zQ~R?Dgg#!8KwtBNm@(%5J};DZ#az0}J6RU>U0G&3wCK#RPQNQ;DC+>03GJJKbpsD2RzCS!lM*ab`K)bF0 From 27b52e2da4e6acdc535f24ede06c437342a6d5d1 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Thu, 13 Jun 2024 11:02:20 -0400 Subject: [PATCH 05/52] deps: add pynfsclient --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 191a1f77e..64bd63fa7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ sqlalchemy = "^2.0.4" termcolor = ">=2.4.0" terminaltables = "^3.1.0" xmltodict = "^0.13.0" +pynfsclient = "^0.1.5" [tool.poetry.group.dev.dependencies] flake8 = "*" From 4c5844820fb95423040fdd41e4d4db8683dba0c0 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Thu, 13 Jun 2024 11:07:26 -0400 Subject: [PATCH 06/52] nfs(db): add LIR and shares table and reflection; rename conn to sess --- nxc/protocols/nfs/database.py | 39 +++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/nfs/database.py b/nxc/protocols/nfs/database.py index 8f10d151b..6ecd9c969 100644 --- a/nxc/protocols/nfs/database.py +++ b/nxc/protocols/nfs/database.py @@ -24,7 +24,7 @@ def __init__(self, db_engine): Session = scoped_session(session_factory) # this is still named "conn" when it is the session object; TODO: rename - self.conn = Session() + self.sess = Session() @staticmethod def db_schema(db_conn): @@ -44,13 +44,44 @@ def db_schema(db_conn): "port" integer )""" ) + db_conn.execute( + """CREATE TABLE "loggedin_relations" ( + "id" integer PRIMARY KEY, + "cred_id" integer, + "host_id" integer, + FOREIGN KEY(cred_id) REFERENCES credentials(id), + FOREIGN KEY(host_id) REFERENCES hosts(id) + )""" + ) + db_conn.execute( + """CREATE TABLE "shares" ( + "id" integer PRIMARY KEY, + "lir_id" integer, + "data" text, + FOREIGN KEY(lir_id) REFERENCES loggedin_relations(id) + )""" + ) def reflect_tables(self): - pass + with self.db_engine.connect(): + try: + self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine) + self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine) + self.LoggedinRelationsTable = Table("loggedin_relations", self.metadata, autoload_with=self.db_engine) + self.SharesTable = Table("shares", self.metadata, autoload_with=self.db_engine) + except (NoInspectionAvailable, NoSuchTableError): + print( + f""" + [-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch + [-] This is probably because a newer version of nxc is being run on an old DB schema + [-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`) + [-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB""" + ) + sys.exit() def shutdown_db(self): try: - self.conn.close() + self.sess.close() # due to the async nature of nxc, sometimes session state is a bit messy and this will throw: # Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and # this would cause an unexpected state change to @@ -59,4 +90,4 @@ def shutdown_db(self): def clear_database(self): for table in self.metadata.sorted_tables: - self.conn.execute(table.delete()) + self.sess.execute(table.delete()) From 7b37015b62194260af87649218971426a8aebc75 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Thu, 13 Jun 2024 11:14:17 -0400 Subject: [PATCH 07/52] nfs: formatting, commenting, and small fixes --- nxc/protocols/nfs.py | 47 +++++++++++++------------------------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 86aaf1504..6df59d0cf 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -7,24 +7,20 @@ class nfs(connection): def __init__(self, args, db, host): - # Setting up NFS protocol attributes self.protocol = "nfs" self.port = 111 self.portmap = None self.mnt_port = None self.mount = None self.auth = {"flavor": 1, - "machine_name": "host1", - "uid": 0, - "gid": 0, - "aux_gid": [], + "machine_name": "host1", + "uid": 0, + "gid": 0, + "aux_gid": [], } - connection.__init__(self, args, db, host) - def proto_logger(self): - # Initializing logger for NFS protocol self.logger = NXCAdapter( extra={ "protocol": "NFS", @@ -35,7 +31,7 @@ def proto_logger(self): ) def plaintext_login(self, username, password): - # Doing as Anonymously now. + # Uses Anonymous access for now try: if self.initialization(): self.logger.success("Initialization is successfull!") @@ -46,7 +42,7 @@ def plaintext_login(self, username, password): self.disconnection() def create_conn_obj(self): - # Creating and connecting socket to NFS host + """Creates and connects a socket to the NFS host""" try: self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.client.connect((self.host, self.port)) @@ -58,7 +54,6 @@ def create_conn_obj(self): def enum_host_info(self): self.initialization() - # Enumerating host information try: # Dump all registered programs programs = self.portmap.dump() @@ -76,13 +71,12 @@ def enum_host_info(self): self.disconnection() def print_host_info(self): - # Printing host banner information self.logger.display(f"Target supported NFS versions {self.nfs_versions}") return True def disconnection(self): + """Disconnect mount and portmap if they are connected""" try: - # Disconnecting from NFS host if self.mount(): self.mount.disconnect() if self.portmap(): @@ -92,6 +86,7 @@ def disconnection(self): self.logger.debug(f"Error during disconnection: {e}") def initialization(self): + """Initializes and connects to the portmap and mounted folder""" try: # Portmap Initialization self.portmap = Portmap(self.host, timeout=3600) @@ -106,10 +101,9 @@ def initialization(self): except Exception as e: self.logger.debug(f"Error during Initialization: {e}") - def listdir(self, nfs, file_handle, path, recurse=1): - # Process entries in NFS directory recursively. + def list_dir(self, nfs, file_handle, path, recurse=1): + """Process entries in NFS directory recursively""" def process_entries(entries, path, recurse): - try: contents = [] for entry in entries: @@ -119,7 +113,7 @@ def process_entries(entries, path, recurse): entry_type = entry["name_attributes"]["attributes"].get("type") if entry_type == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. dir_handle = entry["name_handle"]["handle"]["data"] - contents += self.listdir(nfs, dir_handle, item_path, recurse=recurse - 1) + contents += self.list_dir(nfs, dir_handle, item_path, recurse=recurse - 1) else: contents.append(item_path) @@ -129,10 +123,8 @@ def process_entries(entries, path, recurse): contents += process_entries(entry["nextentry"], path, recurse) return contents - except Exception as e: self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}") - try: if recurse == 0: return [path + "/"] @@ -145,8 +137,7 @@ def process_entries(entries, path, recurse): self.logger.debug(f"Error on Listing NFS Shares Directories: {self.host}:{self.port} {e}") def export_info(self, export_nodes): - # This func for finding filtering all NFS shares and their access range. Using for shares func. - + """Filters all NFS shares and their access range""" result = [] for node in export_nodes: ex_dir = node.ex_dir.decode() @@ -157,12 +148,10 @@ def export_info(self, export_nodes): # If there are more export nodes, process them recursively. More than one share. if node.ex_next: result.extend(self.export_info(node.ex_next)) - return result def group_names(self, groups): - # This func for finding all access range of the share(s). Using for shares func. - + """Findings all access range of the share(s)""" result = [] for group in groups: result.append(group.gr_name.decode()) @@ -175,13 +164,9 @@ def group_names(self, groups): def shares(self): try: - # Initializing NFS services self.initialization() - - # Exporting NFS Shares for mount in self.export_info(self.mount.export()): self.logger.highlight(mount) - except Exception as e: self.logger.debug(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}") finally: @@ -190,8 +175,6 @@ def shares(self): def shares_list(self): try: self.initialization() - - # NFS Initialization nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) self.nfs3 = NFSv3(self.host, nfs_port, 3600, self.auth) self.nfs3.connect() @@ -207,16 +190,14 @@ def shares_list(self): for export in output_name: try: mount_info = self.mount.mnt(export, self.auth) - contents += self.listdir(self.nfs3, mount_info["mountinfo"]["fhandle"], export) + contents += self.list_dir(self.nfs3, mount_info["mountinfo"]["fhandle"], export) except Exception as e: self.logger.fail(f"Can not reaching file(s) on {export}") self.logger.debug(f"Error on Enum NFS Share {export}: {self.host}:{self.port} {e}") self.logger.debug("It is probably unknown format or can not access as anonymously.") continue - for shares in contents: self.logger.highlight(shares) - except Exception as e: self.logger.debug(f"Error on Listing NFS Shares: {self.host}:{self.port} {e}") finally: From 65fc94f8ddeecdbd887bd70764924a87284d3ccb Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Thu, 13 Jun 2024 12:37:54 -0400 Subject: [PATCH 08/52] nfs: properly initialize table variables to None --- nxc/protocols/nfs/database.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/nfs/database.py b/nxc/protocols/nfs/database.py index 6ecd9c969..2f9a9a549 100644 --- a/nxc/protocols/nfs/database.py +++ b/nxc/protocols/nfs/database.py @@ -14,16 +14,17 @@ class database: def __init__(self, db_engine): self.CredentialsTable = None self.HostsTable = None + self.LoggedinRelationsTable = None + self.SharesTable = None self.db_engine = db_engine self.db_path = self.db_engine.url.database self.protocol = Path(self.db_path).stem.upper() self.metadata = MetaData() self.reflect_tables() + session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True) - Session = scoped_session(session_factory) - # this is still named "conn" when it is the session object; TODO: rename self.sess = Session() @staticmethod From 0b5bad9ba0f326a022ee541ff1f8dbff031311a6 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:42:35 +0300 Subject: [PATCH 09/52] Update nfs.py UID Brute Force added (To Do, Brute number will be taken by user) disconnection debug bug fixed To Do Kerberos Auth Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs.py | 384 ++++++++++++++++++++++--------------------- 1 file changed, 196 insertions(+), 188 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 6df59d0cf..4cb60304e 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -6,200 +6,208 @@ class nfs(connection): - def __init__(self, args, db, host): - self.protocol = "nfs" - self.port = 111 - self.portmap = None - self.mnt_port = None - self.mount = None - self.auth = {"flavor": 1, - "machine_name": "host1", - "uid": 0, - "gid": 0, - "aux_gid": [], - } - connection.__init__(self, args, db, host) + def __init__(self, args, db, host): + self.protocol = "nfs" + self.port = 111 + self.portmap = None + self.mnt_port = None + self.mount = None + self.auth = {"flavor": 1, + "machine_name": "host1", + "uid": 0, + "gid": 0, + "aux_gid": [], + } + connection.__init__(self, args, db, host) - def proto_logger(self): - self.logger = NXCAdapter( - extra={ - "protocol": "NFS", - "host": self.host, - "port": self.port, - "hostname": self.hostname, - } - ) - - def plaintext_login(self, username, password): - # Uses Anonymous access for now - try: - if self.initialization(): - self.logger.success("Initialization is successfull!") - except Exception as e: - self.logger.fail("Initialization is failed.") - self.logger.debug(f"Error Plaintext login: {self.host}:{self.port} {e}") - finally: - self.disconnection() - - def create_conn_obj(self): - """Creates and connects a socket to the NFS host""" - try: - self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.client.connect((self.host, self.port)) - self.logger.info(f"Connection target successful: {self.host}:{self.port}") - except Exception as e: - self.logger.debug(f"Error connecting to NFS host: {self.host}:{self.port} {e}") - return False - return True - - def enum_host_info(self): - self.initialization() - try: - # Dump all registered programs - programs = self.portmap.dump() + def proto_logger(self): + self.logger = NXCAdapter( + extra={ + "protocol": "NFS", + "host": self.host, + "port": self.port, + "hostname": self.hostname, + } + ) + + def plaintext_login(self, username, password): + # Uses Anonymous access for now + try: + if self.initialization(): + self.logger.success("Initialization is successfull!") + except Exception as e: + self.logger.fail("Initialization is failed.") + self.logger.debug(f"Error Plaintext login: {self.host}:{self.port} {e}") + finally: + self.disconnection() + + def create_conn_obj(self): + """Creates and connects a socket to the NFS host""" + try: + self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client.connect((self.host, self.port)) + self.logger.info(f"Connection target successful: {self.host}:{self.port}") + except Exception as e: + self.logger.debug(f"Error connecting to NFS host: {self.host}:{self.port} {e}") + return False + return True + + def enum_host_info(self): + self.initialization() + 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"]) + self.nfs_versions = set() + for program in programs: + if program["program"] == NFS_PROGRAM: + self.nfs_versions.add(program["version"]) - return self.nfs_versions + return self.nfs_versions - except Exception as e: - self.logger.debug(f"Error checking NFS version: {self.host} {e}") - finally: - self.disconnection() - - def print_host_info(self): - self.logger.display(f"Target supported NFS versions {self.nfs_versions}") - return True - - def disconnection(self): - """Disconnect mount and portmap if they are connected""" - try: - if self.mount(): - self.mount.disconnect() - if self.portmap(): - self.portmap.disconnect() - self.logger.info(f"Disconnection successful: {self.host}:{self.port}") - except Exception as e: - self.logger.debug(f"Error during disconnection: {e}") - - def initialization(self): - """Initializes and connects to the portmap and mounted folder""" - try: - # Portmap Initialization - self.portmap = Portmap(self.host, timeout=3600) - 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=3600, auth=self.auth) - self.mount.connect() - - return self.portmap, self.mnt_port, self.mount - except Exception as e: - self.logger.debug(f"Error during Initialization: {e}") - - def list_dir(self, nfs, file_handle, path, recurse=1): - """Process entries in NFS directory recursively""" - def process_entries(entries, path, recurse): - try: - contents = [] - for entry in entries: - 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): - entry_type = entry["name_attributes"]["attributes"].get("type") - if entry_type == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. - dir_handle = entry["name_handle"]["handle"]["data"] - contents += self.list_dir(nfs, dir_handle, item_path, recurse=recurse - 1) - else: - contents.append(item_path) - - if entry["nextentry"]: - # Processing next entries recursively - recurse += 1 - contents += process_entries(entry["nextentry"], path, recurse) - - return contents - except Exception as e: - self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}") - try: - if recurse == 0: - return [path + "/"] + except Exception as e: + self.logger.debug(f"Error checking NFS version: {self.host} {e}") + finally: + self.disconnection() + + def print_host_info(self): + self.logger.display(f"Target supported NFS versions {self.nfs_versions}") + return True + + def disconnection(self): + """Disconnect mount and portmap if they are connected""" + try: + self.mount.disconnect() + self.portmap.disconnect() + self.logger.info(f"Disconnection successful: {self.host}:{self.port}") + except Exception as e: + self.logger.debug(f"Error during disconnection: {e}") + + def initialization(self): + """Initializes and connects to the portmap and mounted folder""" + try: + # Portmap Initialization + self.portmap = Portmap(self.host, timeout=3600) + 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=3600, auth=self.auth) + self.mount.connect() + + return self.portmap, self.mnt_port, self.mount + except Exception as e: + self.logger.debug(f"Error during Initialization: {e}") + + def list_dir(self, nfs, file_handle, path, recurse=1): + """Process entries in NFS directory recursively""" + def process_entries(entries, path, recurse): + try: + contents = [] + for entry in entries: + 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): + entry_type = entry["name_attributes"]["attributes"].get("type") + if entry_type == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. + dir_handle = entry["name_handle"]["handle"]["data"] + contents += self.list_dir(nfs, dir_handle, item_path, recurse=recurse - 1) + else: + contents.append(item_path) + + if entry["nextentry"]: + # Processing next entries recursively + recurse += 1 + contents += process_entries(entry["nextentry"], path, recurse) + + return contents + except Exception as e: + self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}") + try: + if recurse == 0: + return [path + "/"] - items = self.nfs3.readdirplus(file_handle, auth=self.auth) - entries = items["resok"]["reply"]["entries"] + items = self.nfs3.readdirplus(file_handle, auth=self.auth) + entries = items["resok"]["reply"]["entries"] - return process_entries(entries, path, recurse) - except Exception as e: - self.logger.debug(f"Error on Listing NFS Shares Directories: {self.host}:{self.port} {e}") + return process_entries(entries, path, recurse) + except Exception as e: + pass - def export_info(self, export_nodes): - """Filters all NFS shares and their access range""" - result = [] - for node in export_nodes: - ex_dir = node.ex_dir.decode() - # Collect the names of the groups associated with this export node - group_names = self.group_names(node.ex_groups) - result.append(f"{ex_dir} {', '.join(group_names)}") - - # If there are more export nodes, process them recursively. More than one share. - if node.ex_next: - result.extend(self.export_info(node.ex_next)) - return result + def export_info(self, export_nodes): + """Filters all NFS shares and their access range""" + result = [] + for node in export_nodes: + ex_dir = node.ex_dir.decode() + # Collect the names of the groups associated with this export node + group_names = self.group_names(node.ex_groups) + result.append(f"{ex_dir} {', '.join(group_names)}") + + # If there are more export nodes, process them recursively. More than one share. + if node.ex_next: + result.extend(self.export_info(node.ex_next)) + return result - def group_names(self, groups): - """Findings 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)) + def group_names(self, groups): + """Findings 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 - - def shares(self): - try: - self.initialization() - for mount in self.export_info(self.mount.export()): - self.logger.highlight(mount) - except Exception as e: - self.logger.debug(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}") - finally: - self.disconnection() - - def shares_list(self): - try: - self.initialization() - nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) - self.nfs3 = NFSv3(self.host, nfs_port, 3600, self.auth) - self.nfs3.connect() - - contents = [] - # Mounting NFS Shares - output_export = str(self.mount.export()) - pattern_name = re.compile(r"ex_dir=b'([^']*)'") - matches_name = pattern_name.findall(output_export) - output_name = list(matches_name) - - # Searching files and folders as recursivly on every shares. - for export in output_name: - try: - mount_info = self.mount.mnt(export, self.auth) - contents += self.list_dir(self.nfs3, mount_info["mountinfo"]["fhandle"], export) - except Exception as e: - self.logger.fail(f"Can not reaching file(s) on {export}") - self.logger.debug(f"Error on Enum NFS Share {export}: {self.host}:{self.port} {e}") - self.logger.debug("It is probably unknown format or can not access as anonymously.") - continue - for shares in contents: - self.logger.highlight(shares) - except Exception as e: - self.logger.debug(f"Error on Listing NFS Shares: {self.host}:{self.port} {e}") - finally: - self.nfs3.disconnect() - self.disconnection() + return result + + def shares(self): + try: + self.initialization() + for mount in self.export_info(self.mount.export()): + self.logger.highlight(mount) + except Exception as e: + self.logger.debug(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}") + finally: + self.disconnection() + + def shares_list(self): + try: + self.initialization() + nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) + self.nfs3 = NFSv3(self.host, nfs_port, 3600, self.auth) + self.nfs3.connect() + + contents = [] + # Mounting NFS Shares + output_export = str(self.mount.export()) + pattern_name = re.compile(r"ex_dir=b'([^']*)'") + matches_name = pattern_name.findall(output_export) + output_name = list(matches_name) + + white_list = [] + + for uid in range(1001): + self.auth["uid"] = uid + + # Searching files and folders as recursivly on every shares. + for export in output_name: + try: + if export in white_list: + continue + else: + mount_info = self.mount.mnt(export, self.auth) + contents += self.list_dir(self.nfs3, mount_info["mountinfo"]["fhandle"], export) + white_list.append(export) + for shares in contents: + self.logger.highlight(f"UID: {self.auth['uid']} {shares}") + contents = [] + except Exception as e: + pass + + except Exception as e: + self.logger.fail(f"Can not reaching file(s) on {export}") + 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() + self.disconnection() From ea5426b468959a51dab8115d72d0912bcb0d5428 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Mon, 17 Jun 2024 20:55:02 +0300 Subject: [PATCH 10/52] Update nfs.py Bruteforce-UID added. Kerberos auth left. Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs.py | 396 ++++++++++++++++++++++--------------------- 1 file changed, 201 insertions(+), 195 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 4cb60304e..ecf833cdb 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -4,210 +4,216 @@ import socket import re - class nfs(connection): - def __init__(self, args, db, host): - self.protocol = "nfs" - self.port = 111 - self.portmap = None - self.mnt_port = None - self.mount = None - self.auth = {"flavor": 1, - "machine_name": "host1", - "uid": 0, - "gid": 0, - "aux_gid": [], - } - connection.__init__(self, args, db, host) + def __init__(self, args, db, host): + self.protocol = "nfs" + self.port = 111 + self.portmap = None + self.mnt_port = None + self.mount = None + self.auth = {"flavor": 1, + "machine_name": "host1", + "uid": 0, + "gid": 0, + "aux_gid": [], + } + connection.__init__(self, args, db, host) + + def proto_logger(self): + self.logger = NXCAdapter( + extra={ + "protocol": "NFS", + "host": self.host, + "port": self.port, + "hostname": self.hostname, + } + ) + + def plaintext_login(self, username, password): + # Uses Anonymous access for now + try: + if self.initialization(): + self.logger.success("Initialization is successfull!") + except Exception as e: + self.logger.fail("Initialization is failed.") + self.logger.debug(f"Error Plaintext login: {self.host}:{self.port} {e}") + finally: + self.disconnection() + + def create_conn_obj(self): + """Creates and connects a socket to the NFS host""" + try: + self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client.connect((self.host, self.port)) + self.logger.info(f"Connection target successful: {self.host}:{self.port}") + except Exception as e: + self.logger.debug(f"Error connecting to NFS host: {self.host}:{self.port} {e}") + return False + return True + + def enum_host_info(self): + self.initialization() + try: + # Dump all registered programs + programs = self.portmap.dump() - def proto_logger(self): - self.logger = NXCAdapter( - extra={ - "protocol": "NFS", - "host": self.host, - "port": self.port, - "hostname": self.hostname, - } - ) - - def plaintext_login(self, username, password): - # Uses Anonymous access for now - try: - if self.initialization(): - self.logger.success("Initialization is successfull!") - except Exception as e: - self.logger.fail("Initialization is failed.") - self.logger.debug(f"Error Plaintext login: {self.host}:{self.port} {e}") - finally: - self.disconnection() - - def create_conn_obj(self): - """Creates and connects a socket to the NFS host""" - try: - self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.client.connect((self.host, self.port)) - self.logger.info(f"Connection target successful: {self.host}:{self.port}") - except Exception as e: - self.logger.debug(f"Error connecting to NFS host: {self.host}:{self.port} {e}") - return False - return True - - def enum_host_info(self): - self.initialization() - 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"]) - self.nfs_versions = set() - for program in programs: - if program["program"] == NFS_PROGRAM: - self.nfs_versions.add(program["version"]) + return self.nfs_versions - return self.nfs_versions + except Exception as e: + self.logger.debug(f"Error checking NFS version: {self.host} {e}") + finally: + self.disconnection() + + def print_host_info(self): + self.logger.display(f"Target supported NFS versions {self.nfs_versions}") + return True + + def disconnection(self): + """Disconnect mount and portmap if they are connected""" + try: + self.mount.disconnect() + self.portmap.disconnect() + self.logger.info(f"Disconnection successful: {self.host}:{self.port}") + except Exception as e: + self.logger.debug(f"Error during disconnection: {e}") + + def initialization(self): + """Initializes and connects to the portmap and mounted folder""" + try: + # Portmap Initialization + self.portmap = Portmap(self.host, timeout=3600) + 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=3600, auth=self.auth) + self.mount.connect() + + return self.portmap, self.mnt_port, self.mount + except Exception as e: + self.logger.debug(f"Error during Initialization: {e}") + + def list_dir(self, nfs, file_handle, path, recurse=1): + """Process entries in NFS directory recursively""" + def process_entries(entries, path, recurse): + try: + contents = [] + for entry in entries: + 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): + entry_type = entry["name_attributes"]["attributes"].get("type") + if entry_type == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. + dir_handle = entry["name_handle"]["handle"]["data"] + contents += self.list_dir(nfs, dir_handle, item_path, recurse=recurse - 1) + else: + contents.append(item_path) + + if entry["nextentry"]: + # Processing next entries recursively + recurse += 1 + contents += process_entries(entry["nextentry"], path, recurse) + + return contents + except Exception as e: + self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}") + try: + if recurse == 0: + return [path + "/"] - except Exception as e: - self.logger.debug(f"Error checking NFS version: {self.host} {e}") - finally: - self.disconnection() - - def print_host_info(self): - self.logger.display(f"Target supported NFS versions {self.nfs_versions}") - return True - - def disconnection(self): - """Disconnect mount and portmap if they are connected""" - try: - self.mount.disconnect() - self.portmap.disconnect() - self.logger.info(f"Disconnection successful: {self.host}:{self.port}") - except Exception as e: - self.logger.debug(f"Error during disconnection: {e}") - - def initialization(self): - """Initializes and connects to the portmap and mounted folder""" - try: - # Portmap Initialization - self.portmap = Portmap(self.host, timeout=3600) - 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=3600, auth=self.auth) - self.mount.connect() - - return self.portmap, self.mnt_port, self.mount - except Exception as e: - self.logger.debug(f"Error during Initialization: {e}") - - def list_dir(self, nfs, file_handle, path, recurse=1): - """Process entries in NFS directory recursively""" - def process_entries(entries, path, recurse): - try: - contents = [] - for entry in entries: - 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): - entry_type = entry["name_attributes"]["attributes"].get("type") - if entry_type == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. - dir_handle = entry["name_handle"]["handle"]["data"] - contents += self.list_dir(nfs, dir_handle, item_path, recurse=recurse - 1) - else: - contents.append(item_path) - - if entry["nextentry"]: - # Processing next entries recursively - recurse += 1 - contents += process_entries(entry["nextentry"], path, recurse) - - return contents - except Exception as e: - self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}") - try: - if recurse == 0: - return [path + "/"] + items = self.nfs3.readdirplus(file_handle, auth=self.auth) + entries = items["resok"]["reply"]["entries"] - items = self.nfs3.readdirplus(file_handle, auth=self.auth) - entries = items["resok"]["reply"]["entries"] + return process_entries(entries, path, recurse) + except Exception: + pass # To avoid mess in the debug logs - return process_entries(entries, path, recurse) - except Exception as e: - pass + def export_info(self, export_nodes): + """Filters all NFS shares and their access range""" + result = [] + for node in export_nodes: + ex_dir = node.ex_dir.decode() + # Collect the names of the groups associated with this export node + group_names = self.group_names(node.ex_groups) + result.append(f"{ex_dir} {', '.join(group_names)}") + + # If there are more export nodes, process them recursively. More than one share. + if node.ex_next: + result.extend(self.export_info(node.ex_next)) + return result - def export_info(self, export_nodes): - """Filters all NFS shares and their access range""" - result = [] - for node in export_nodes: - ex_dir = node.ex_dir.decode() - # Collect the names of the groups associated with this export node - group_names = self.group_names(node.ex_groups) - result.append(f"{ex_dir} {', '.join(group_names)}") - - # If there are more export nodes, process them recursively. More than one share. - if node.ex_next: - result.extend(self.export_info(node.ex_next)) - return result + def group_names(self, groups): + """Findings 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)) - def group_names(self, groups): - """Findings 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 + + def shares(self): + try: + self.initialization() + for mount in self.export_info(self.mount.export()): + self.logger.highlight(mount) + except Exception as e: + self.logger.debug(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}") + finally: + self.disconnection() - return result - - def shares(self): - try: - self.initialization() - for mount in self.export_info(self.mount.export()): - self.logger.highlight(mount) - except Exception as e: - self.logger.debug(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}") - finally: - self.disconnection() - - def shares_list(self): - try: - self.initialization() - nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) - self.nfs3 = NFSv3(self.host, nfs_port, 3600, self.auth) - self.nfs3.connect() - - contents = [] - # Mounting NFS Shares - output_export = str(self.mount.export()) - pattern_name = re.compile(r"ex_dir=b'([^']*)'") - matches_name = pattern_name.findall(output_export) - output_name = list(matches_name) + def shares_list(self, max_uid=0): + def export_list(max_uid): + white_list = [] + for uid in range(max_uid + 1): + self.auth["uid"] = uid + for export in output_name: + try: + if export in white_list: + continue + else: + mount_info = self.mount.mnt(export, self.auth) + nonlocal contents # The nonlocal keyword allows a variable defined in an outer (non-global) scope. To be referenced and modified in an inner scope. + contents += self.list_dir(self.nfs3, mount_info["mountinfo"]["fhandle"], export) + white_list.append(export) + for shares in contents: + self.logger.highlight(f"UID: {uid} {shares}") + contents = [] + except Exception: + if not max_uid: # To avoid mess in the debug logs + self.logger.fail(f"Can not reach file(s) on {export} with UID: {uid}") + + try: + self.initialization() + nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) + self.nfs3 = NFSv3(self.host, nfs_port, 3600, self.auth) + self.nfs3.connect() + + contents = [] + # Mounting NFS Shares + output_export = str(self.mount.export()) + pattern_name = re.compile(r"ex_dir=b'([^']*)'") + matches_name = pattern_name.findall(output_export) + output_name = list(matches_name) + + export_list(max_uid) - white_list = [] + 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() + self.disconnection() - for uid in range(1001): - self.auth["uid"] = uid - - # Searching files and folders as recursivly on every shares. - for export in output_name: - try: - if export in white_list: - continue - else: - mount_info = self.mount.mnt(export, self.auth) - contents += self.list_dir(self.nfs3, mount_info["mountinfo"]["fhandle"], export) - white_list.append(export) - for shares in contents: - self.logger.highlight(f"UID: {self.auth['uid']} {shares}") - contents = [] - except Exception as e: - pass - - except Exception as e: - self.logger.fail(f"Can not reaching file(s) on {export}") - 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() - self.disconnection() + def bruteforce_uid(self, max_uid=None): + if not max_uid: + max_uid = int(self.args.bruteforce_uid) + self.shares_list(max_uid) From 59b6bf8f7691ad407bf0cd5b7650bdd17c551327 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:13:15 +0300 Subject: [PATCH 11/52] Update proto_args.py Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs/proto_args.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index f09e5afff..11962d714 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -5,6 +5,7 @@ def proto_args(parser, parents): dgroup = ldap_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") dgroup.add_argument("--shares", action="store_true", help="Authenticate locally to each target") dgroup.add_argument("--shares-list", action="store_true", help="Listing enumerated shares") + dgroup.add_argument("--bruteforce-uid", nargs="?", type=int, const=4000, metavar="MAX_UID", help="Enumerate shares by bruteforcing UIDs") return parser From d071631c1ab833f5534f63375023ccb3f0cc460d Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:26:49 +0300 Subject: [PATCH 12/52] Update e2e_commands.txt - bruteforceuid added Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- tests/e2e_commands.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index c2b175dc1..552974eb1 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -250,3 +250,4 @@ netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE ##### NFS netexec nfs TARGETHOST -u "" -p "" --shares netexec nfs TARGETHOST -u "" -p "" --shares-list +netexec nfs TARGETHOST -u "" -p "" --bruteforce-uid 2000 From e0faaf3dbf41d2270f52702b28b9b59bc635e9a6 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Tue, 9 Jul 2024 09:35:09 +0300 Subject: [PATCH 13/52] Changed uid-brute for to keep consistency with SMB --- nxc/protocols/nfs.py | 4 ++-- nxc/protocols/nfs/proto_args.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index ecf833cdb..5c8d4748b 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -213,7 +213,7 @@ def export_list(max_uid): self.nfs3.disconnect() self.disconnection() - def bruteforce_uid(self, max_uid=None): + def uid_brute(self, max_uid=None): if not max_uid: - max_uid = int(self.args.bruteforce_uid) + max_uid = int(self.args.uid_brute) self.shares_list(max_uid) diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 11962d714..cda1a0848 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -5,7 +5,7 @@ def proto_args(parser, parents): dgroup = ldap_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") dgroup.add_argument("--shares", action="store_true", help="Authenticate locally to each target") dgroup.add_argument("--shares-list", action="store_true", help="Listing enumerated shares") - dgroup.add_argument("--bruteforce-uid", nargs="?", type=int, const=4000, metavar="MAX_UID", help="Enumerate shares by bruteforcing UIDs") + dgroup.add_argument("--uid-brute", nargs="?", type=int, const=4000, metavar="MAX_UID", help="Enumerate shares by bruteforcing UIDs") return parser From 4e855f811f266167ffc628add134a1397efdbc32 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 21 Sep 2024 12:04:39 -0400 Subject: [PATCH 14/52] Properly add pynfsclient dependency --- poetry.lock | 15 +++++++++++++-- pyproject.toml | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1f2fc7d7d..44a7840d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aardwolf" @@ -1734,6 +1734,17 @@ cffi = ">=1.4.1" docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] +[[package]] +name = "pynfsclient" +version = "0.1.5" +description = "Pure python NFS client" +optional = false +python-versions = ">=2.7" +files = [ + {file = "pyNfsClient-0.1.5-py2.py3-none-any.whl", hash = "sha256:d988029d0b86ae2897bfb17162e47a81943a72508ccb37566ef23615c533cc59"}, + {file = "pyNfsClient-0.1.5.tar.gz", hash = "sha256:c6064bd3c365302a529002d0c24961ed7ab5e9b2b64a6da10329dbac85ac81a5"}, +] + [[package]] name = "pyopenssl" version = "23.2.0" @@ -2351,4 +2362,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8.0" -content-hash = "7bde962ed54681c55a1249f2f8272ebf64109417f2a28293eab3eea90ce05c6f" +content-hash = "8e5309a710ccf009d98d60b25f99c6866e5821804601bde027cfee4753739807" diff --git a/pyproject.toml b/pyproject.toml index 64bd63fa7..b0c78c80e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ paramiko = "^3.3.1" poetry-dynamic-versioning = "^1.2.0" pyasn1-modules = "^0.3.0" pylnk3 = "^0.4.2" +pynfsclient = "^0.1.5" pypsrp = "^0.8.1" pypykatz = "^0.6.8" pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23) @@ -65,7 +66,6 @@ sqlalchemy = "^2.0.4" termcolor = ">=2.4.0" terminaltables = "^3.1.0" xmltodict = "^0.13.0" -pynfsclient = "^0.1.5" [tool.poetry.group.dev.dependencies] flake8 = "*" From e1ecf90a7849fa4ca6b124836312de86e498df07 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 21 Sep 2024 12:04:55 -0400 Subject: [PATCH 15/52] Formating --- nxc/protocols/nfs.py | 17 +++++++++-------- nxc/protocols/nfs/proto_args.py | 7 +++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 5c8d4748b..3f718fe2d 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -11,7 +11,8 @@ def __init__(self, args, db, host): self.portmap = None self.mnt_port = None self.mount = None - self.auth = {"flavor": 1, + self.auth = { + "flavor": 1, "machine_name": "host1", "uid": 0, "gid": 0, @@ -38,7 +39,7 @@ def plaintext_login(self, username, password): self.logger.fail("Initialization is failed.") self.logger.debug(f"Error Plaintext login: {self.host}:{self.port} {e}") finally: - self.disconnection() + self.disconnect() def create_conn_obj(self): """Creates and connects a socket to the NFS host""" @@ -67,20 +68,20 @@ def enum_host_info(self): except Exception as e: self.logger.debug(f"Error checking NFS version: {self.host} {e}") finally: - self.disconnection() + self.disconnect() def print_host_info(self): self.logger.display(f"Target supported NFS versions {self.nfs_versions}") return True - def disconnection(self): + def disconnect(self): """Disconnect mount and portmap if they are connected""" try: self.mount.disconnect() self.portmap.disconnect() - self.logger.info(f"Disconnection successful: {self.host}:{self.port}") + self.logger.info(f"Disconnect successful: {self.host}:{self.port}") except Exception as e: - self.logger.debug(f"Error during disconnection: {e}") + self.logger.debug(f"Error during disconnect: {e}") def initialization(self): """Initializes and connects to the portmap and mounted folder""" @@ -167,7 +168,7 @@ def shares(self): except Exception as e: self.logger.debug(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}") finally: - self.disconnection() + self.disconnect() def shares_list(self, max_uid=0): def export_list(max_uid): @@ -211,7 +212,7 @@ def export_list(max_uid): self.logger.debug("It is probably unknown format or can not access as anonymously.") finally: self.nfs3.disconnect() - self.disconnection() + self.disconnect() def uid_brute(self, max_uid=None): if not max_uid: diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index cda1a0848..55a312ef6 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -1,11 +1,10 @@ def proto_args(parser, parents): - ldap_parser = parser.add_parser("nfs", help="NFS", parents=parents) - ldap_parser.add_argument("--port", type=int, default=111, help="NFS port (default: 111)") + nfs_parser = parser.add_parser("nfs", help="NFS", parents=parents) + nfs_parser.add_argument("--port", type=int, default=111, help="NFS port (default: %(default)s)") - dgroup = ldap_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") + dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") dgroup.add_argument("--shares", action="store_true", help="Authenticate locally to each target") dgroup.add_argument("--shares-list", action="store_true", help="Listing enumerated shares") dgroup.add_argument("--uid-brute", nargs="?", type=int, const=4000, metavar="MAX_UID", help="Enumerate shares by bruteforcing UIDs") return parser - From ee549ccae71bca6f8b21859668719d4d0d10f189 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 21 Sep 2024 12:46:59 -0400 Subject: [PATCH 16/52] Add ipv6 message and formating --- nxc/protocols/nfs.py | 55 ++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 3f718fe2d..b4e867d6d 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -3,6 +3,8 @@ from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3 import socket import re +import uuid + class nfs(connection): def __init__(self, args, db, host): @@ -13,7 +15,7 @@ def __init__(self, args, db, host): self.mount = None self.auth = { "flavor": 1, - "machine_name": "host1", + "machine_name": uuid.uuid4().hex.upper()[0:6], "uid": 0, "gid": 0, "aux_gid": [], @@ -29,29 +31,31 @@ def proto_logger(self): "hostname": self.hostname, } ) - + def plaintext_login(self, username, password): # Uses Anonymous access for now try: if self.initialization(): - self.logger.success("Initialization is successfull!") + self.logger.success("Initialization successfull!") except Exception as e: - self.logger.fail("Initialization is failed.") + self.logger.fail("Initialization failed.") self.logger.debug(f"Error Plaintext login: {self.host}:{self.port} {e}") finally: self.disconnect() - + def create_conn_obj(self): - """Creates and connects a socket to the NFS host""" + """Creates a socket and connects to the NFS host""" + if self.is_ipv6: + self.logger.error("IPv6 is not supported for NFS") try: self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.client.connect((self.host, self.port)) - self.logger.info(f"Connection target successful: {self.host}:{self.port}") + self.logger.info(f"Connection to target successful: {self.host}:{self.port}") except Exception as e: self.logger.debug(f"Error connecting to NFS host: {self.host}:{self.port} {e}") return False return True - + def enum_host_info(self): self.initialization() try: @@ -62,18 +66,16 @@ def enum_host_info(self): for program in programs: if program["program"] == NFS_PROGRAM: self.nfs_versions.add(program["version"]) - return self.nfs_versions - except Exception as e: self.logger.debug(f"Error checking NFS version: {self.host} {e}") finally: self.disconnect() - + def print_host_info(self): self.logger.display(f"Target supported NFS versions {self.nfs_versions}") return True - + def disconnect(self): """Disconnect mount and portmap if they are connected""" try: @@ -82,23 +84,23 @@ def disconnect(self): self.logger.info(f"Disconnect successful: {self.host}:{self.port}") except Exception as e: self.logger.debug(f"Error during disconnect: {e}") - + def initialization(self): """Initializes and connects to the portmap and mounted folder""" try: # Portmap Initialization self.portmap = Portmap(self.host, timeout=3600) 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=3600, auth=self.auth) self.mount.connect() - + return self.portmap, self.mnt_port, self.mount except Exception as e: self.logger.debug(f"Error during Initialization: {e}") - + def list_dir(self, nfs, file_handle, path, recurse=1): """Process entries in NFS directory recursively""" def process_entries(entries, path, recurse): @@ -108,18 +110,18 @@ def process_entries(entries, path, recurse): 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): - entry_type = entry["name_attributes"]["attributes"].get("type") + entry_type = entry["name_attributes"]["attributes"].get("type") if entry_type == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. dir_handle = entry["name_handle"]["handle"]["data"] contents += self.list_dir(nfs, dir_handle, item_path, recurse=recurse - 1) else: contents.append(item_path) - + if entry["nextentry"]: # Processing next entries recursively recurse += 1 contents += process_entries(entry["nextentry"], path, recurse) - + return contents except Exception as e: self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}") @@ -142,7 +144,7 @@ def export_info(self, export_nodes): # Collect the names of the groups associated with this export node group_names = self.group_names(node.ex_groups) result.append(f"{ex_dir} {', '.join(group_names)}") - + # If there are more export nodes, process them recursively. More than one share. if node.ex_next: result.extend(self.export_info(node.ex_next)) @@ -153,13 +155,13 @@ def group_names(self, groups): 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 - + def shares(self): try: self.initialization() @@ -190,24 +192,23 @@ def export_list(max_uid): except Exception: if not max_uid: # To avoid mess in the debug logs self.logger.fail(f"Can not reach file(s) on {export} with UID: {uid}") - + try: self.initialization() nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) self.nfs3 = NFSv3(self.host, nfs_port, 3600, self.auth) self.nfs3.connect() - + contents = [] # Mounting NFS Shares output_export = str(self.mount.export()) pattern_name = re.compile(r"ex_dir=b'([^']*)'") - matches_name = pattern_name.findall(output_export) + matches_name = pattern_name.findall(output_export) output_name = list(matches_name) - + export_list(max_uid) 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: From 78a270b57bf7baa35896fb5ae992341da6e5fee9 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 22 Sep 2024 16:37:55 -0400 Subject: [PATCH 17/52] Switch pynfsclient to our fork --- poetry.lock | 16 ++++++++++------ pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 44a7840d5..873ed0316 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1735,15 +1735,19 @@ docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] -name = "pynfsclient" +name = "pyNfsClient" version = "0.1.5" description = "Pure python NFS client" optional = false python-versions = ">=2.7" -files = [ - {file = "pyNfsClient-0.1.5-py2.py3-none-any.whl", hash = "sha256:d988029d0b86ae2897bfb17162e47a81943a72508ccb37566ef23615c533cc59"}, - {file = "pyNfsClient-0.1.5.tar.gz", hash = "sha256:c6064bd3c365302a529002d0c24961ed7ab5e9b2b64a6da10329dbac85ac81a5"}, -] +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/Pennyw0rth/NfsClient" +reference = "HEAD" +resolved_reference = "bb29a22433882677ad68cb5bd9eb7ae32c18e972" [[package]] name = "pyopenssl" @@ -2362,4 +2366,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8.0" -content-hash = "8e5309a710ccf009d98d60b25f99c6866e5821804601bde027cfee4753739807" +content-hash = "1cf32ce29adfdbe31288e1343d4856e5c465ea99a6dc960f6f3b3fe495518b48" diff --git a/pyproject.toml b/pyproject.toml index b0c78c80e..e107fbee3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ paramiko = "^3.3.1" poetry-dynamic-versioning = "^1.2.0" pyasn1-modules = "^0.3.0" pylnk3 = "^0.4.2" -pynfsclient = "^0.1.5" +pynfsclient = { git = "https://github.com/Pennyw0rth/NfsClient" } pypsrp = "^0.8.1" pypykatz = "^0.6.8" pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23) From f191f780a32e4c47283abf1b194803f797f86f8c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 22 Sep 2024 19:27:59 -0400 Subject: [PATCH 18/52] Renaming, simplifying code and adad recursion depth to share listing --- nxc/protocols/nfs.py | 79 ++++++++++++++++++--------------- nxc/protocols/nfs/proto_args.py | 4 +- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index b4e867d6d..bfbfd4ef3 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -35,7 +35,7 @@ def proto_logger(self): def plaintext_login(self, username, password): # Uses Anonymous access for now try: - if self.initialization(): + if self.init(): self.logger.success("Initialization successfull!") except Exception as e: self.logger.fail("Initialization failed.") @@ -57,7 +57,7 @@ def create_conn_obj(self): return True def enum_host_info(self): - self.initialization() + self.init() try: # Dump all registered programs programs = self.portmap.dump() @@ -85,7 +85,7 @@ def disconnect(self): except Exception as e: self.logger.debug(f"Error during disconnect: {e}") - def initialization(self): + def init(self): """Initializes and connects to the portmap and mounted folder""" try: # Portmap Initialization @@ -125,16 +125,17 @@ def process_entries(entries, path, recurse): return contents except Exception as e: self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}") - try: - if recurse == 0: - return [path + "/"] - items = self.nfs3.readdirplus(file_handle, auth=self.auth) + if recurse == 0: + return [path + "/"] + + items = self.nfs3.readdirplus(file_handle, auth=self.auth) + if "resfail" in items: + raise Exception("Insufficient Permissions") + else: entries = items["resok"]["reply"]["entries"] - return process_entries(entries, path, recurse) - except Exception: - pass # To avoid mess in the debug logs + return process_entries(entries, path, recurse) def export_info(self, export_nodes): """Filters all NFS shares and their access range""" @@ -164,7 +165,7 @@ def group_names(self, groups): def shares(self): try: - self.initialization() + self.init() for mount in self.export_info(self.mount.export()): self.logger.highlight(mount) except Exception as e: @@ -172,29 +173,9 @@ def shares(self): finally: self.disconnect() - def shares_list(self, max_uid=0): - def export_list(max_uid): - white_list = [] - for uid in range(max_uid + 1): - self.auth["uid"] = uid - for export in output_name: - try: - if export in white_list: - continue - else: - mount_info = self.mount.mnt(export, self.auth) - nonlocal contents # The nonlocal keyword allows a variable defined in an outer (non-global) scope. To be referenced and modified in an inner scope. - contents += self.list_dir(self.nfs3, mount_info["mountinfo"]["fhandle"], export) - white_list.append(export) - for shares in contents: - self.logger.highlight(f"UID: {uid} {shares}") - contents = [] - except Exception: - if not max_uid: # To avoid mess in the debug logs - self.logger.fail(f"Can not reach file(s) on {export} with UID: {uid}") - + def enum_shares(self, max_uid=0): try: - self.initialization() + self.init() nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) self.nfs3 = NFSv3(self.host, nfs_port, 3600, self.auth) self.nfs3.connect() @@ -206,8 +187,7 @@ def export_list(max_uid): matches_name = pattern_name.findall(output_export) output_name = list(matches_name) - export_list(max_uid) - + self.list_exported_shares(max_uid, contents, output_name, recurse_depth=self.args.enum_shares) 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.") @@ -215,7 +195,34 @@ def export_list(max_uid): self.nfs3.disconnect() self.disconnect() + def list_exported_shares(self, max_uid, contents, output_name, recurse_depth): + self.logger.display(f"Enumerating NFS Shares with UID {max_uid}") + white_list = [] + for uid in range(max_uid + 1): + self.auth["uid"] = uid + for export in output_name: + try: + if export in white_list: + self.logger.debug(f"Skipping {export} as it is already listed.") + continue + else: + mount_info = self.mount.mnt(export, self.auth) + contents = self.list_dir(self.nfs3, mount_info["mountinfo"]["fhandle"], export, recurse_depth) + white_list.append(export) + self.logger.success(export) + for content in contents: + self.logger.highlight(f"\t{content}") + except Exception as e: + if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e): + self.logger.fail(f"{export} - RPC Access denied") + elif "RPC_AUTH_ERROR: AUTH_TOOWEAK" in str(e): + self.logger.fail(f"{export} - Kerberos authentication required") + elif "Insufficient Permissions" in str(e): + self.logger.fail(f"{export} - Insufficient Permissions for share listing") + else: + self.logger.exception(f"{export} - {e}") + def uid_brute(self, max_uid=None): if not max_uid: max_uid = int(self.args.uid_brute) - self.shares_list(max_uid) + self.enum_shares(max_uid) diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 55a312ef6..157396fbf 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -3,8 +3,8 @@ def proto_args(parser, parents): nfs_parser.add_argument("--port", type=int, default=111, help="NFS port (default: %(default)s)") dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") - dgroup.add_argument("--shares", action="store_true", help="Authenticate locally to each target") - dgroup.add_argument("--shares-list", action="store_true", help="Listing enumerated shares") + dgroup.add_argument("--shares", action="store_true", help="List NFS shares with UID 0") + dgroup.add_argument("--enum-shares", nargs="?", type=int, const=1, help="Authenticate and enumerate exposed shares recursively (default depth: %(default)s)") dgroup.add_argument("--uid-brute", nargs="?", type=int, const=4000, metavar="MAX_UID", help="Enumerate shares by bruteforcing UIDs") return parser From 045f2c291b63b3c5f7943a4c9ebaa44e8da5cccb Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 22 Sep 2024 19:28:28 -0400 Subject: [PATCH 19/52] Fix recursion depth --- nxc/protocols/nfs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index bfbfd4ef3..8bcdfd356 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -73,7 +73,7 @@ def enum_host_info(self): self.disconnect() def print_host_info(self): - self.logger.display(f"Target supported NFS versions {self.nfs_versions}") + self.logger.display(f"Target supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)})") return True def disconnect(self): @@ -119,7 +119,6 @@ def process_entries(entries, path, recurse): if entry["nextentry"]: # Processing next entries recursively - recurse += 1 contents += process_entries(entry["nextentry"], path, recurse) return contents From d89709feaca4bd7d9caf6c81ddb0ddf835eea64a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 22 Sep 2024 19:34:35 -0400 Subject: [PATCH 20/52] Update PyNfsClient --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 873ed0316..3f87cfdd2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1747,7 +1747,7 @@ develop = false type = "git" url = "https://github.com/Pennyw0rth/NfsClient" reference = "HEAD" -resolved_reference = "bb29a22433882677ad68cb5bd9eb7ae32c18e972" +resolved_reference = "12afb1eb8f39e0c9932e4c3e62a9e1a23577bb88" [[package]] name = "pyopenssl" From ad1b421422db52a4421478ccc5941bcaa8f0a01f Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:57:54 +0300 Subject: [PATCH 21/52] Fixed spam output while uid bruteforce Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 8bcdfd356..f45b0b8c5 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -195,7 +195,10 @@ def enum_shares(self, max_uid=0): self.disconnect() def list_exported_shares(self, max_uid, contents, output_name, recurse_depth): - self.logger.display(f"Enumerating NFS Shares with UID {max_uid}") + if max_uid: + self.logger.display(f"Enumerating NFS Shares to UID {max_uid}") + else: + self.logger.display(f"Enumerating NFS Shares with UID {max_uid}") white_list = [] for uid in range(max_uid + 1): self.auth["uid"] = uid @@ -210,16 +213,17 @@ def list_exported_shares(self, max_uid, contents, output_name, recurse_depth): white_list.append(export) self.logger.success(export) for content in contents: - self.logger.highlight(f"\t{content}") + self.logger.highlight(f"\tUID: {self.auth['uid']} {content}") except Exception as e: - if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e): - self.logger.fail(f"{export} - RPC Access denied") - elif "RPC_AUTH_ERROR: AUTH_TOOWEAK" in str(e): - self.logger.fail(f"{export} - Kerberos authentication required") - elif "Insufficient Permissions" in str(e): - self.logger.fail(f"{export} - Insufficient Permissions for share listing") - else: - self.logger.exception(f"{export} - {e}") + if not max_uid: # To avoid mess in the debug logs + if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e): + self.logger.fail(f"{export} - RPC Access denied") + elif "RPC_AUTH_ERROR: AUTH_TOOWEAK" in str(e): + self.logger.fail(f"{export} - Kerberos authentication required") + elif "Insufficient Permissions" in str(e): + self.logger.fail(f"{export} - Insufficient Permissions for share listing") + else: + self.logger.exception(f"{export} - {e}") def uid_brute(self, max_uid=None): if not max_uid: From 8c39962dba5e9c781ec149f9de24ab8ae7e9a951 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 28 Sep 2024 11:47:11 -0400 Subject: [PATCH 22/52] Add generel disconnect instruction to be implemented by protocols --- nxc/connection.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nxc/connection.py b/nxc/connection.py index 2f97e86be..e78fcd5b4 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -203,6 +203,9 @@ def print_host_info(self): def create_conn_obj(self): return + def disconnect(self): + return + def check_if_admin(self): return @@ -231,6 +234,7 @@ def proto_flow(self): else: self.logger.debug("Calling command arguments") self.call_cmd_args() + self.disconnect() def call_cmd_args(self): """Calls all the methods specified by the command line arguments From f05601f6458205bc5db998a6514939d390fc9aef Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 28 Sep 2024 11:56:46 -0400 Subject: [PATCH 23/52] Use init as create_conn_obj function and improve code structure --- nxc/protocols/nfs.py | 54 ++++++++++++++------------------------------ poetry.lock | 2 +- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index f45b0b8c5..e25e7c179 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -1,7 +1,6 @@ from nxc.connection import connection from nxc.logger import NXCAdapter from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3 -import socket import re import uuid @@ -35,29 +34,33 @@ def proto_logger(self): def plaintext_login(self, username, password): # Uses Anonymous access for now try: - if self.init(): + if self.create_conn_obj(): self.logger.success("Initialization successfull!") except Exception as e: self.logger.fail("Initialization failed.") self.logger.debug(f"Error Plaintext login: {self.host}:{self.port} {e}") - finally: - self.disconnect() def create_conn_obj(self): - """Creates a socket and connects to the NFS host""" - if self.is_ipv6: - self.logger.error("IPv6 is not supported for NFS") + """Initializes and connects to the portmap and mounted folder""" try: - self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.client.connect((self.host, self.port)) - self.logger.info(f"Connection to target successful: {self.host}:{self.port}") + # Portmap Initialization + self.portmap = Portmap(self.host, timeout=3600) + 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=3600, auth=self.auth) + self.mount.connect() + + # Change logging port to the NFS port + self.port = self.mnt_port + self.proto_logger() except Exception as e: - self.logger.debug(f"Error connecting to NFS host: {self.host}:{self.port} {e}") + self.logger.fail(f"Error during Initialization: {e}") return False return True def enum_host_info(self): - self.init() try: # Dump all registered programs programs = self.portmap.dump() @@ -69,8 +72,6 @@ def enum_host_info(self): return self.nfs_versions except Exception as e: self.logger.debug(f"Error checking NFS version: {self.host} {e}") - finally: - self.disconnect() def print_host_info(self): self.logger.display(f"Target supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)})") @@ -83,23 +84,7 @@ def disconnect(self): self.portmap.disconnect() self.logger.info(f"Disconnect successful: {self.host}:{self.port}") except Exception as e: - self.logger.debug(f"Error during disconnect: {e}") - - def init(self): - """Initializes and connects to the portmap and mounted folder""" - try: - # Portmap Initialization - self.portmap = Portmap(self.host, timeout=3600) - 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=3600, auth=self.auth) - self.mount.connect() - - return self.portmap, self.mnt_port, self.mount - except Exception as e: - self.logger.debug(f"Error during Initialization: {e}") + self.logger.fail(f"Error during disconnect: {e}") def list_dir(self, nfs, file_handle, path, recurse=1): """Process entries in NFS directory recursively""" @@ -164,17 +149,13 @@ def group_names(self, groups): def shares(self): try: - self.init() for mount in self.export_info(self.mount.export()): self.logger.highlight(mount) except Exception as e: - self.logger.debug(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}") - finally: - self.disconnect() + self.logger.fail(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}") def enum_shares(self, max_uid=0): try: - self.init() nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) self.nfs3 = NFSv3(self.host, nfs_port, 3600, self.auth) self.nfs3.connect() @@ -192,7 +173,6 @@ def enum_shares(self, max_uid=0): self.logger.debug("It is probably unknown format or can not access as anonymously.") finally: self.nfs3.disconnect() - self.disconnect() def list_exported_shares(self, max_uid, contents, output_name, recurse_depth): if max_uid: diff --git a/poetry.lock b/poetry.lock index 3f87cfdd2..1740c7551 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1747,7 +1747,7 @@ develop = false type = "git" url = "https://github.com/Pennyw0rth/NfsClient" reference = "HEAD" -resolved_reference = "12afb1eb8f39e0c9932e4c3e62a9e1a23577bb88" +resolved_reference = "a94a3254b279dc49395caecf27ec097a71eea91b" [[package]] name = "pyopenssl" From e9ed46b6e0394f7d14caf71acbb46daa6a11118b Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 28 Sep 2024 11:57:01 -0400 Subject: [PATCH 24/52] Correct port description --- nxc/protocols/nfs/proto_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 157396fbf..a8489be4e 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -1,6 +1,6 @@ def proto_args(parser, parents): nfs_parser = parser.add_parser("nfs", help="NFS", parents=parents) - nfs_parser.add_argument("--port", type=int, default=111, help="NFS port (default: %(default)s)") + nfs_parser.add_argument("--port", type=int, default=111, help="NFS portmapper port (default: %(default)s)") dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") dgroup.add_argument("--shares", action="store_true", help="List NFS shares with UID 0") From 6b5eb1a240c09a1608d3363ea302b73841650072 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 28 Sep 2024 11:57:28 -0400 Subject: [PATCH 25/52] Remove plaintext_login as there is no such thing in NFS3 --- nxc/protocols/nfs.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index e25e7c179..690876bc8 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -31,15 +31,6 @@ def proto_logger(self): } ) - def plaintext_login(self, username, password): - # Uses Anonymous access for now - try: - if self.create_conn_obj(): - self.logger.success("Initialization successfull!") - except Exception as e: - self.logger.fail("Initialization failed.") - self.logger.debug(f"Error Plaintext login: {self.host}:{self.port} {e}") - def create_conn_obj(self): """Initializes and connects to the portmap and mounted folder""" try: From c23db63b97df55adbc97e7031f8a1d8dbafd3a93 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 28 Sep 2024 12:17:09 -0400 Subject: [PATCH 26/52] Add connection timeout option --- nxc/protocols/nfs.py | 10 +++++----- nxc/protocols/nfs/proto_args.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 690876bc8..786c3487e 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -35,12 +35,12 @@ def create_conn_obj(self): """Initializes and connects to the portmap and mounted folder""" try: # Portmap Initialization - self.portmap = Portmap(self.host, timeout=3600) + 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=3600, auth=self.auth) + 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 @@ -113,7 +113,7 @@ def process_entries(entries, path, recurse): return process_entries(entries, path, recurse) def export_info(self, export_nodes): - """Filters all NFS shares and their access range""" + """Enumerates all NFS shares and their access range""" result = [] for node in export_nodes: ex_dir = node.ex_dir.decode() @@ -127,7 +127,7 @@ def export_info(self, export_nodes): return result def group_names(self, groups): - """Findings all access range of the share(s)""" + """Enumerates all access range of the share(s)""" result = [] for group in groups: result.append(group.gr_name.decode()) @@ -148,7 +148,7 @@ def shares(self): def enum_shares(self, max_uid=0): try: nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) - self.nfs3 = NFSv3(self.host, nfs_port, 3600, self.auth) + self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) self.nfs3.connect() contents = [] diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index a8489be4e..4500be7e5 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -1,6 +1,7 @@ def proto_args(parser, parents): nfs_parser = parser.add_parser("nfs", help="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=30, help="NFS connection timeout (default: %(default)ss)") dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") dgroup.add_argument("--shares", action="store_true", help="List NFS shares with UID 0") From 1e624d86069262cd4c33ac747d60569371aa8256 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 28 Sep 2024 15:40:28 -0400 Subject: [PATCH 27/52] Add share permission enumeration --- nxc/protocols/nfs.py | 48 ++++++++++++++++++++++++++++++--- nxc/protocols/nfs/proto_args.py | 2 +- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 786c3487e..48dee5e0b 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -1,6 +1,6 @@ from nxc.connection import connection from nxc.logger import NXCAdapter -from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3 +from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3, ACCESS3_READ, ACCESS3_MODIFY, ACCESS3_EXECUTE import re import uuid @@ -139,11 +139,53 @@ def group_names(self, groups): return result def shares(self): + self.auth["uid"] = self.args.shares + self.logger.display(f"Enumerating NFS Shares with UID {self.args.shares}") try: - for mount in self.export_info(self.mount.export()): - self.logger.highlight(mount) + # 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()) + reg = re.compile(r"ex_dir=b'([^']*)'") + shares = list(reg.findall(output_export)) + + # Mount shares and check permissions + self.logger.highlight(f"{'Permissions':<15}{'Share':<15}") + self.logger.highlight(f"{'-----------':<15}{'-----':<15}") + for share in shares: + try: + mnt_info = self.mount.mnt(share, self.auth) + file_handle = mnt_info["mountinfo"]["fhandle"] + + read_perm, write_perm, exec_perm = self.get_permissions(file_handle) + self.mount.umnt(self.auth) + self.logger.highlight(f"{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<12} {share:<15}") + except Exception as e: + self.logger.fail(f"{share} - {e}") + self.logger.highlight(f"{'---':<15}{share:<15}") + 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 = 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 = 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 = False + return read_perm, write_perm, exec_perm def enum_shares(self, max_uid=0): try: diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 4500be7e5..24d92a52e 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -4,7 +4,7 @@ def proto_args(parser, parents): nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)") dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") - dgroup.add_argument("--shares", action="store_true", help="List NFS shares with UID 0") + dgroup.add_argument("--shares", nargs="?", type=int, const=0, help="List NFS shares (default with UID: %(default)s)") dgroup.add_argument("--enum-shares", nargs="?", type=int, const=1, help="Authenticate and enumerate exposed shares recursively (default depth: %(default)s)") dgroup.add_argument("--uid-brute", nargs="?", type=int, const=4000, metavar="MAX_UID", help="Enumerate shares by bruteforcing UIDs") From fadce581ef3e4331dc0a0865573cb22768d402ae Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 28 Sep 2024 15:40:41 -0400 Subject: [PATCH 28/52] Improve code --- nxc/protocols/nfs.py | 46 ++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 48dee5e0b..1ec1181b9 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -77,7 +77,7 @@ def disconnect(self): except Exception as e: self.logger.fail(f"Error during disconnect: {e}") - def list_dir(self, nfs, file_handle, path, recurse=1): + def list_dir(self, file_handle, path, recurse=1): """Process entries in NFS directory recursively""" def process_entries(entries, path, recurse): try: @@ -89,7 +89,7 @@ def process_entries(entries, path, recurse): entry_type = entry["name_attributes"]["attributes"].get("type") if entry_type == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. dir_handle = entry["name_handle"]["handle"]["data"] - contents += self.list_dir(nfs, dir_handle, item_path, recurse=recurse - 1) + contents += self.list_dir(dir_handle, item_path, recurse=recurse - 1) else: contents.append(item_path) @@ -193,52 +193,48 @@ def enum_shares(self, max_uid=0): self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) self.nfs3.connect() - contents = [] # Mounting NFS Shares output_export = str(self.mount.export()) - pattern_name = re.compile(r"ex_dir=b'([^']*)'") - matches_name = pattern_name.findall(output_export) - output_name = list(matches_name) + reg = re.compile(r"ex_dir=b'([^']*)'") + shares = list(reg.findall(output_export)) - self.list_exported_shares(max_uid, contents, output_name, recurse_depth=self.args.enum_shares) + self.list_exported_shares(max_uid, shares, recurse_depth=self.args.enum_shares) 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 list_exported_shares(self, max_uid, contents, output_name, recurse_depth): - if max_uid: - self.logger.display(f"Enumerating NFS Shares to UID {max_uid}") + def list_exported_shares(self, max_uid, shares, recurse_depth): + if self.args.uid_brute: + self.logger.display(f"Enumerating NFS Shares up to UID {max_uid}") else: self.logger.display(f"Enumerating NFS Shares with UID {max_uid}") white_list = [] for uid in range(max_uid + 1): self.auth["uid"] = uid - for export in output_name: + for share in shares: try: - if export in white_list: - self.logger.debug(f"Skipping {export} as it is already listed.") + if share in white_list: + self.logger.debug(f"Skipping {share} as it is already listed.") continue else: - mount_info = self.mount.mnt(export, self.auth) - contents = self.list_dir(self.nfs3, mount_info["mountinfo"]["fhandle"], export, recurse_depth) - white_list.append(export) - self.logger.success(export) + mount_info = self.mount.mnt(share, self.auth) + contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, recurse_depth) + white_list.append(share) + self.logger.success(share) for content in contents: self.logger.highlight(f"\tUID: {self.auth['uid']} {content}") except Exception as e: if not max_uid: # To avoid mess in the debug logs if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e): - self.logger.fail(f"{export} - RPC Access denied") + self.logger.fail(f"{share} - RPC Access denied") elif "RPC_AUTH_ERROR: AUTH_TOOWEAK" in str(e): - self.logger.fail(f"{export} - Kerberos authentication required") + self.logger.fail(f"{share} - Kerberos authentication required") elif "Insufficient Permissions" in str(e): - self.logger.fail(f"{export} - Insufficient Permissions for share listing") + self.logger.fail(f"{share} - Insufficient Permissions for share listing") else: - self.logger.exception(f"{export} - {e}") + self.logger.exception(f"{share} - {e}") - def uid_brute(self, max_uid=None): - if not max_uid: - max_uid = int(self.args.uid_brute) - self.enum_shares(max_uid) + def uid_brute(self): + self.enum_shares(self.args.uid_brute) From 4cd46c7fd96ac343e839371d8b3e22e321ca434a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 28 Sep 2024 15:53:09 -0400 Subject: [PATCH 29/52] Separate enum_shares and uid_brute --- nxc/protocols/nfs.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 1ec1181b9..be3000aaf 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -187,7 +187,7 @@ def get_permissions(self, file_handle): exec_perm = False return read_perm, write_perm, exec_perm - def enum_shares(self, max_uid=0): + 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) @@ -198,13 +198,40 @@ def enum_shares(self, max_uid=0): reg = re.compile(r"ex_dir=b'([^']*)'") shares = list(reg.findall(output_export)) - self.list_exported_shares(max_uid, shares, recurse_depth=self.args.enum_shares) + for share in shares: + try: + mount_info = self.mount.mnt(share, self.auth) + contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, self.args.enum_shares) + self.logger.success(share) + for content in contents: + self.logger.highlight(f"\tUID: {self.auth['uid']} {content}") + except Exception as e: + if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e): + self.logger.fail(f"{share} - RPC Access denied") + elif "RPC_AUTH_ERROR: AUTH_TOOWEAK" in str(e): + self.logger.fail(f"{share} - Kerberos authentication required") + elif "Insufficient Permissions" in str(e): + self.logger.fail(f"{share} - Insufficient Permissions for share listing") + else: + self.logger.exception(f"{share} - {e}") 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 uid_brute(self): + 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)) + + self.list_exported_shares(self.args.uid_brute, shares, 1) + def list_exported_shares(self, max_uid, shares, recurse_depth): if self.args.uid_brute: self.logger.display(f"Enumerating NFS Shares up to UID {max_uid}") @@ -235,6 +262,3 @@ def list_exported_shares(self, max_uid, shares, recurse_depth): self.logger.fail(f"{share} - Insufficient Permissions for share listing") else: self.logger.exception(f"{share} - {e}") - - def uid_brute(self): - self.enum_shares(self.args.uid_brute) From b48f6945ae3a2e7d77274d0ebfa4c3a96243e578 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 28 Sep 2024 16:59:39 -0400 Subject: [PATCH 30/52] Added uid proto arg and rwx permission check for enum_shares --- nxc/protocols/nfs.py | 18 ++++++++++-------- nxc/protocols/nfs/proto_args.py | 3 ++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index be3000aaf..39cc9d561 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -12,10 +12,11 @@ def __init__(self, args, db, host): self.portmap = None self.mnt_port = None self.mount = None + self.uid = args.uid self.auth = { "flavor": 1, "machine_name": uuid.uuid4().hex.upper()[0:6], - "uid": 0, + "uid": self.uid, "gid": 0, "aux_gid": [], } @@ -86,12 +87,12 @@ def process_entries(entries, path, recurse): 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): - entry_type = entry["name_attributes"]["attributes"].get("type") - if entry_type == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. + if entry["name_attributes"]["attributes"]["type"] == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. dir_handle = entry["name_handle"]["handle"]["data"] contents += self.list_dir(dir_handle, item_path, recurse=recurse - 1) else: - contents.append(item_path) + read_perm, write_perm, exec_perm = self.get_permissions(entry["name_handle"]["handle"]["data"]) + contents.append({"path": item_path, "read": read_perm, "write": write_perm, "execute": exec_perm}) if entry["nextentry"]: # Processing next entries recursively @@ -102,7 +103,8 @@ def process_entries(entries, path, recurse): self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}") if recurse == 0: - return [path + "/"] + read_perm, write_perm, exec_perm = self.get_permissions(file_handle) + return [{"path": f"{path}/", "read": read_perm, "write": write_perm, "execute": exec_perm}] items = self.nfs3.readdirplus(file_handle, auth=self.auth) if "resfail" in items: @@ -139,8 +141,7 @@ def group_names(self, groups): return result def shares(self): - self.auth["uid"] = self.args.shares - self.logger.display(f"Enumerating NFS Shares with UID {self.args.shares}") + self.logger.display(f"Enumerating NFS Shares with UID {self.uid}") try: # Connect to NFS nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) @@ -198,13 +199,14 @@ def enum_shares(self): reg = re.compile(r"ex_dir=b'([^']*)'") shares = list(reg.findall(output_export)) + self.logger.display(f"Enumerating NFS Shares Directories with UID {self.uid}") for share in shares: try: mount_info = self.mount.mnt(share, self.auth) contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, self.args.enum_shares) self.logger.success(share) for content in contents: - self.logger.highlight(f"\tUID: {self.auth['uid']} {content}") + self.logger.highlight(f"{'r' if content['read'] else '-'}{'w' if content['write'] else '-'}{'x' if content['execute'] else '-'} {content['path']}") except Exception as e: if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e): self.logger.fail(f"{share} - RPC Access denied") diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 24d92a52e..7eba17fec 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -4,7 +4,8 @@ def proto_args(parser, parents): nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)") dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") - dgroup.add_argument("--shares", nargs="?", type=int, const=0, help="List NFS shares (default with UID: %(default)s)") + dgroup.add_argument("--uid", type=int, default=0, help="UID to use for NFS operations (default: %(default)s)") + dgroup.add_argument("--shares", action="store_true", help="List NFS shares") dgroup.add_argument("--enum-shares", nargs="?", type=int, const=1, help="Authenticate and enumerate exposed shares recursively (default depth: %(default)s)") dgroup.add_argument("--uid-brute", nargs="?", type=int, const=4000, metavar="MAX_UID", help="Enumerate shares by bruteforcing UIDs") From 1a38a288df1b49ee5077f9902a4b12fd2920202d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 29 Sep 2024 07:05:00 -0400 Subject: [PATCH 31/52] Add share usage --- nxc/protocols/nfs.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 39cc9d561..421cea1b7 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -3,6 +3,7 @@ from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3, ACCESS3_READ, ACCESS3_MODIFY, ACCESS3_EXECUTE import re import uuid +import math class nfs(connection): @@ -153,16 +154,20 @@ def shares(self): shares = list(reg.findall(output_export)) # Mount shares and check permissions - self.logger.highlight(f"{'Permissions':<15}{'Share':<15}") - self.logger.highlight(f"{'-----------':<15}{'-----':<15}") + self.logger.highlight(f"{'Perms':<9}{'Usage':<9}{'Share':<15}") + self.logger.highlight(f"{'-----':<9}{'-----':<9}{'-----':<15}") for share in shares: try: mnt_info = self.mount.mnt(share, self.auth) file_handle = mnt_info["mountinfo"]["fhandle"] + info = self.nfs3.fsstat(file_handle, self.auth) + free_space = info["resok"]["fbytes"] + total_space = info["resok"]["tbytes"] + used_space = total_space - free_space read_perm, write_perm, exec_perm = self.get_permissions(file_handle) self.mount.umnt(self.auth) - self.logger.highlight(f"{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<12} {share:<15}") + self.logger.highlight(f"{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<3}{convert_size(used_space)} / {convert_size(total_space)} {share:<15}") except Exception as e: self.logger.fail(f"{share} - {e}") self.logger.highlight(f"{'---':<15}{share:<15}") @@ -264,3 +269,13 @@ def list_exported_shares(self, max_uid, shares, recurse_depth): self.logger.fail(f"{share} - Insufficient Permissions for share listing") else: self.logger.exception(f"{share} - {e}") + + +def convert_size(size_bytes): + if size_bytes == 0: + return "0B" + size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + i = int(math.floor(math.log(size_bytes, 1024))) + p = math.pow(1024, i) + s = int(round(size_bytes / p, 0)) + return f"{s}{size_name[i]}" From 6f24858dc0ed0d72c4ca76c9ee250ea4c06bb0c6 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 29 Sep 2024 07:09:44 -0400 Subject: [PATCH 32/52] Fix indents --- nxc/protocols/nfs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 421cea1b7..5b7ef2a1e 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -154,8 +154,8 @@ def shares(self): shares = list(reg.findall(output_export)) # Mount shares and check permissions - self.logger.highlight(f"{'Perms':<9}{'Usage':<9}{'Share':<15}") - self.logger.highlight(f"{'-----':<9}{'-----':<9}{'-----':<15}") + self.logger.highlight(f"{'Perms':<9}{'Storage Usage':<17}{'Share':<15}") + self.logger.highlight(f"{'-----':<9}{'-------------':<17}{'-----':<15}") for share in shares: try: mnt_info = self.mount.mnt(share, self.auth) @@ -167,7 +167,7 @@ def shares(self): used_space = total_space - free_space read_perm, write_perm, exec_perm = self.get_permissions(file_handle) self.mount.umnt(self.auth) - self.logger.highlight(f"{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<3}{convert_size(used_space)} / {convert_size(total_space)} {share:<15}") + self.logger.highlight(f"{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<15}") except Exception as e: self.logger.fail(f"{share} - {e}") self.logger.highlight(f"{'---':<15}{share:<15}") @@ -277,5 +277,5 @@ def convert_size(size_bytes): size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") i = int(math.floor(math.log(size_bytes, 1024))) p = math.pow(1024, i) - s = int(round(size_bytes / p, 0)) + s = round(size_bytes / p, 1) return f"{s}{size_name[i]}" From a877047ba2ad8cc002254c9b25eade9d29d3b301 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:38:28 +0300 Subject: [PATCH 33/52] all feature flags done with output Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs.py | 51 +++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 5b7ef2a1e..a6b21c2f1 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -91,9 +91,13 @@ def process_entries(entries, path, recurse): if entry["name_attributes"]["attributes"]["type"] == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. dir_handle = entry["name_handle"]["handle"]["data"] contents += self.list_dir(dir_handle, item_path, recurse=recurse - 1) - else: + else: + file_handle = entry["name_handle"]["handle"]["data"] + attrs = self.nfs3.getattr(file_handle, auth=self.auth) + file_size = attrs["attributes"]["size"] + file_size = convert_size(file_size) read_perm, write_perm, exec_perm = self.get_permissions(entry["name_handle"]["handle"]["data"]) - contents.append({"path": item_path, "read": read_perm, "write": write_perm, "execute": exec_perm}) + contents.append({"path": item_path, "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": file_size}) if entry["nextentry"]: # Processing next entries recursively @@ -117,17 +121,18 @@ def process_entries(entries, path, recurse): def export_info(self, export_nodes): """Enumerates all NFS shares and their access range""" - result = [] + networks = [] for node in export_nodes: - ex_dir = node.ex_dir.decode() + # Collect the names of the groups associated with this export node group_names = self.group_names(node.ex_groups) - result.append(f"{ex_dir} {', '.join(group_names)}") + networks.append(group_names) # If there are more export nodes, process them recursively. More than one share. if node.ex_next: - result.extend(self.export_info(node.ex_next)) - return result + networks.extend(self.export_info(node.ex_next)) + + return networks def group_names(self, groups): """Enumerates all access range of the share(s)""" @@ -140,7 +145,7 @@ def group_names(self, groups): result.extend(self.group_names(group.gr_next)) return result - + def shares(self): self.logger.display(f"Enumerating NFS Shares with UID {self.uid}") try: @@ -150,27 +155,30 @@ def shares(self): self.nfs3.connect() 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()) + reg = re.compile(r"ex_dir=b'([^']*)'") # Get share names + shares = list(reg.findall(output_export)) + # Mount shares and check permissions - self.logger.highlight(f"{'Perms':<9}{'Storage Usage':<17}{'Share':<15}") - self.logger.highlight(f"{'-----':<9}{'-------------':<17}{'-----':<15}") - for share in shares: + self.logger.highlight(f"{'Perms':<9}{'Storage Usage':<17}{'Share':<30}{'Reachable Network(s)':<15}") + self.logger.highlight(f"{'-----':<9}{'-------------':<17}{'-----':<30}{'-----------------':15}") + for share, network in zip(shares, networks): try: mnt_info = self.mount.mnt(share, self.auth) file_handle = mnt_info["mountinfo"]["fhandle"] - + info = self.nfs3.fsstat(file_handle, self.auth) free_space = info["resok"]["fbytes"] total_space = info["resok"]["tbytes"] used_space = total_space - free_space + read_perm, write_perm, exec_perm = self.get_permissions(file_handle) self.mount.umnt(self.auth) - self.logger.highlight(f"{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<15}") + self.logger.highlight(f"{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<30}{', '.join(network) if network else 'No network':<15}") except Exception as e: self.logger.fail(f"{share} - {e}") - self.logger.highlight(f"{'---':<15}{share:<15}") + self.logger.highlight(f"{'---':<15}{share[0]:<15}") except Exception as e: self.logger.fail(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}") @@ -203,15 +211,19 @@ def enum_shares(self): 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.logger.display(f"Enumerating NFS Shares Directories with UID {self.uid}") - for share in shares: + for share, network in zip(shares, networks): try: mount_info = self.mount.mnt(share, self.auth) contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, self.args.enum_shares) + self.logger.success(share) + self.logger.highlight(f"{'Perms':<9}{'File Size':<15}{'File Path':<45}{'Reachable Network(s)':<15}") + self.logger.highlight(f"{'-----':<9}{'---------':<15}{'---------':<45}{'-----------------':<15}") for content in contents: - self.logger.highlight(f"{'r' if content['read'] else '-'}{'w' if content['write'] else '-'}{'x' if content['execute'] else '-'} {content['path']}") + self.logger.highlight(f"{'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") @@ -258,7 +270,7 @@ def list_exported_shares(self, max_uid, shares, recurse_depth): white_list.append(share) self.logger.success(share) for content in contents: - self.logger.highlight(f"\tUID: {self.auth['uid']} {content}") + self.logger.highlight(f"UID: {self.auth['uid']} {content['path']}") except Exception as e: if not max_uid: # To avoid mess in the debug logs if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e): @@ -270,7 +282,6 @@ def list_exported_shares(self, max_uid, shares, recurse_depth): else: self.logger.exception(f"{share} - {e}") - def convert_size(size_bytes): if size_bytes == 0: return "0B" From 9031589f0d8f21429f2201afa6d8ab2eb5332ed4 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:39:49 +0300 Subject: [PATCH 34/52] Changed enum-shares default depth to 3 from 1 Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs/proto_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 7eba17fec..3dab99a42 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -6,7 +6,7 @@ def proto_args(parser, parents): dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") dgroup.add_argument("--uid", type=int, default=0, help="UID to use for NFS operations (default: %(default)s)") dgroup.add_argument("--shares", action="store_true", help="List NFS shares") - dgroup.add_argument("--enum-shares", nargs="?", type=int, const=1, help="Authenticate and enumerate exposed shares recursively (default depth: %(default)s)") + dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)") dgroup.add_argument("--uid-brute", nargs="?", type=int, const=4000, metavar="MAX_UID", help="Enumerate shares by bruteforcing UIDs") return parser From 8c5b3617e842e637da74c8e9d68102c692a2ec80 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 29 Sep 2024 18:17:02 -0400 Subject: [PATCH 35/52] Fix bug for insufficient permissions, remove unnecessary code and formating --- nxc/protocols/nfs.py | 54 ++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index a6b21c2f1..ab02adfad 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -91,7 +91,7 @@ def process_entries(entries, path, recurse): if entry["name_attributes"]["attributes"]["type"] == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder. dir_handle = entry["name_handle"]["handle"]["data"] contents += self.list_dir(dir_handle, item_path, recurse=recurse - 1) - else: + else: file_handle = entry["name_handle"]["handle"]["data"] attrs = self.nfs3.getattr(file_handle, auth=self.auth) file_size = attrs["attributes"]["size"] @@ -107,9 +107,12 @@ def process_entries(entries, path, recurse): 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"] + 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}] + return [{"path": f"{path}/", "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": "-"}] items = self.nfs3.readdirplus(file_handle, auth=self.auth) if "resfail" in items: @@ -131,7 +134,7 @@ def export_info(self, export_nodes): # 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): @@ -145,7 +148,7 @@ def group_names(self, groups): result.extend(self.group_names(group.gr_next)) return result - + def shares(self): self.logger.display(f"Enumerating NFS Shares with UID {self.uid}") try: @@ -159,23 +162,27 @@ def shares(self): reg = re.compile(r"ex_dir=b'([^']*)'") # Get share names shares = list(reg.findall(output_export)) - + # Mount shares and check permissions self.logger.highlight(f"{'Perms':<9}{'Storage Usage':<17}{'Share':<30}{'Reachable Network(s)':<15}") - self.logger.highlight(f"{'-----':<9}{'-------------':<17}{'-----':<30}{'-----------------':15}") + self.logger.highlight(f"{'-----':<9}{'-------------':<17}{'-----':<30}{'-----------------':<15}") for share, network in zip(shares, networks): try: mnt_info = self.mount.mnt(share, self.auth) file_handle = mnt_info["mountinfo"]["fhandle"] - + info = self.nfs3.fsstat(file_handle, self.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"] + read_perm, write_perm, exec_perm = self.get_permissions(file_handle) self.mount.umnt(self.auth) - self.logger.highlight(f"{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<30}{', '.join(network) if network else 'No network':<15}") + self.logger.highlight(f"{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<30}{', '.join(network) if network else 'No network':<15}") except Exception as e: self.logger.fail(f"{share} - {e}") self.logger.highlight(f"{'---':<15}{share[0]:<15}") @@ -218,10 +225,11 @@ def enum_shares(self): try: mount_info = self.mount.mnt(share, self.auth) contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, self.args.enum_shares) - + self.logger.success(share) - self.logger.highlight(f"{'Perms':<9}{'File Size':<15}{'File Path':<45}{'Reachable Network(s)':<15}") - self.logger.highlight(f"{'-----':<9}{'---------':<15}{'---------':<45}{'-----------------':<15}") + if contents: + self.logger.highlight(f"{'Perms':<9}{'File Size':<15}{'File Path':<45}{'Reachable Network(s)':<15}") + self.logger.highlight(f"{'-----':<9}{'---------':<15}{'---------':<45}{'-----------------':<15}") for content in contents: self.logger.highlight(f"{'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: @@ -249,13 +257,10 @@ def uid_brute(self): reg = re.compile(r"ex_dir=b'([^']*)'") shares = list(reg.findall(output_export)) - self.list_exported_shares(self.args.uid_brute, shares, 1) + self.list_exported_shares(self.args.uid_brute, shares) - def list_exported_shares(self, max_uid, shares, recurse_depth): - if self.args.uid_brute: - self.logger.display(f"Enumerating NFS Shares up to UID {max_uid}") - else: - self.logger.display(f"Enumerating NFS Shares with UID {max_uid}") + def list_exported_shares(self, max_uid, shares): + self.logger.display(f"Enumerating NFS Shares up to UID {max_uid}") white_list = [] for uid in range(max_uid + 1): self.auth["uid"] = uid @@ -266,21 +271,16 @@ def list_exported_shares(self, max_uid, shares, recurse_depth): continue else: mount_info = self.mount.mnt(share, self.auth) - contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, recurse_depth) + contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, 1) # Try to list the share with depth 1 white_list.append(share) self.logger.success(share) for content in contents: self.logger.highlight(f"UID: {self.auth['uid']} {content['path']}") except Exception as e: - if not max_uid: # To avoid mess in the debug logs - if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e): - self.logger.fail(f"{share} - RPC Access denied") - elif "RPC_AUTH_ERROR: AUTH_TOOWEAK" in str(e): - self.logger.fail(f"{share} - Kerberos authentication required") - elif "Insufficient Permissions" in str(e): - self.logger.fail(f"{share} - Insufficient Permissions for share listing") - else: - self.logger.exception(f"{share} - {e}") + if "Insufficient Permissions" in str(e): + continue + self.logger.exception(f"{share} - {e}") + def convert_size(size_bytes): if size_bytes == 0: From 84aa78898b390711042e726fe503a4bb44d55968 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 29 Sep 2024 18:43:06 -0400 Subject: [PATCH 36/52] Implementing UID autodetection, making the uid flag redundant --- nxc/protocols/nfs.py | 24 +++++++++++------------- nxc/protocols/nfs/proto_args.py | 1 - 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index ab02adfad..dcfe757b9 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -13,11 +13,10 @@ def __init__(self, args, db, host): self.portmap = None self.mnt_port = None self.mount = None - self.uid = args.uid self.auth = { "flavor": 1, "machine_name": uuid.uuid4().hex.upper()[0:6], - "uid": self.uid, + "uid": 0, "gid": 0, "aux_gid": [], } @@ -80,7 +79,7 @@ def disconnect(self): self.logger.fail(f"Error during disconnect: {e}") def list_dir(self, file_handle, path, recurse=1): - """Process entries in NFS directory recursively""" + """Process entries in NFS directory recursively with UID autodection""" def process_entries(entries, path, recurse): try: contents = [] @@ -150,7 +149,7 @@ def group_names(self, groups): return result def shares(self): - self.logger.display(f"Enumerating NFS Shares with UID {self.uid}") + self.logger.display("Enumerating NFS Shares") try: # Connect to NFS nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) @@ -164,8 +163,8 @@ def shares(self): shares = list(reg.findall(output_export)) # Mount shares and check permissions - self.logger.highlight(f"{'Perms':<9}{'Storage Usage':<17}{'Share':<30}{'Reachable Network(s)':<15}") - self.logger.highlight(f"{'-----':<9}{'-------------':<17}{'-----':<30}{'-----------------':<15}") + self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'Storage Usage':<17}{'Share':<30} {'Reachable Network(s)':<15}") + self.logger.highlight(f"{'---':<11}{'-----':<9}{'-------------':<17}{'-----':<30} {'-----------------':<15}") for share, network in zip(shares, networks): try: mnt_info = self.mount.mnt(share, self.auth) @@ -182,10 +181,9 @@ def shares(self): read_perm, write_perm, exec_perm = self.get_permissions(file_handle) self.mount.umnt(self.auth) - self.logger.highlight(f"{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<30}{', '.join(network) if network else 'No network':<15}") + 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):<9} {share:<30} {', '.join(network) if network else 'No network':<15}") except Exception as e: - self.logger.fail(f"{share} - {e}") - self.logger.highlight(f"{'---':<15}{share[0]:<15}") + 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}") @@ -220,7 +218,7 @@ def enum_shares(self): shares = list(reg.findall(output_export)) networks = self.export_info(self.mount.export()) - self.logger.display(f"Enumerating NFS Shares Directories with UID {self.uid}") + self.logger.display("Enumerating NFS Shares Directories") for share, network in zip(shares, networks): try: mount_info = self.mount.mnt(share, self.auth) @@ -228,10 +226,10 @@ def enum_shares(self): self.logger.success(share) if contents: - self.logger.highlight(f"{'Perms':<9}{'File Size':<15}{'File Path':<45}{'Reachable Network(s)':<15}") - self.logger.highlight(f"{'-----':<9}{'---------':<15}{'---------':<45}{'-----------------':<15}") + self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'File Size':<15}{'File Path':<45} {'Reachable Network(s)':<15}") + self.logger.highlight(f"{'---':<11}{'-----':<9}{'---------':<15}{'---------':<45} {'-----------------':<15}") for content in contents: - self.logger.highlight(f"{'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"{self.auth['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") diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 3dab99a42..b30370ed4 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -4,7 +4,6 @@ def proto_args(parser, parents): nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)") dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") - dgroup.add_argument("--uid", type=int, default=0, help="UID to use for NFS operations (default: %(default)s)") dgroup.add_argument("--shares", action="store_true", help="List NFS shares") dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)") dgroup.add_argument("--uid-brute", nargs="?", type=int, const=4000, metavar="MAX_UID", help="Enumerate shares by bruteforcing UIDs") From 62b535ea501141b546169014640713e1a4810a95 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 29 Sep 2024 18:55:55 -0400 Subject: [PATCH 37/52] Rename Reachable Networks to Access List --- nxc/protocols/nfs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index dcfe757b9..83a5ea7eb 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -163,8 +163,8 @@ def shares(self): shares = list(reg.findall(output_export)) # Mount shares and check permissions - self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'Storage Usage':<17}{'Share':<30} {'Reachable Network(s)':<15}") - self.logger.highlight(f"{'---':<11}{'-----':<9}{'-------------':<17}{'-----':<30} {'-----------------':<15}") + 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): try: mnt_info = self.mount.mnt(share, self.auth) @@ -226,8 +226,8 @@ def enum_shares(self): self.logger.success(share) if contents: - self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'File Size':<15}{'File Path':<45} {'Reachable Network(s)':<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"{self.auth['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: From 4ae6c195f0f3f436915fdfcab0c2e92a703e1217 Mon Sep 17 00:00:00 2001 From: termanix Date: Mon, 30 Sep 2024 09:30:41 +0300 Subject: [PATCH 38/52] Fixed --enum-shares UID bug and removed uid-brute on proto-args --- nxc/protocols/nfs.py | 12 ++++++------ nxc/protocols/nfs/proto_args.py | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 83a5ea7eb..5b381bb96 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -80,7 +80,7 @@ def disconnect(self): def list_dir(self, file_handle, path, recurse=1): """Process entries in NFS directory recursively with UID autodection""" - def process_entries(entries, path, recurse): + def process_entries(entries, path, uid, recurse): try: contents = [] for entry in entries: @@ -96,11 +96,11 @@ def process_entries(entries, path, recurse): file_size = attrs["attributes"]["size"] file_size = convert_size(file_size) read_perm, write_perm, exec_perm = self.get_permissions(entry["name_handle"]["handle"]["data"]) - contents.append({"path": item_path, "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": file_size}) + contents.append({"path": item_path, "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": file_size, "uid": uid}) if entry["nextentry"]: # Processing next entries recursively - contents += process_entries(entry["nextentry"], path, recurse) + contents += process_entries(entry["nextentry"], path, uid, recurse) return contents except Exception as e: @@ -119,7 +119,7 @@ def process_entries(entries, path, recurse): else: entries = items["resok"]["reply"]["entries"] - return process_entries(entries, path, recurse) + return process_entries(entries, path, self.auth["uid"], recurse) def export_info(self, export_nodes): """Enumerates all NFS shares and their access range""" @@ -216,7 +216,7 @@ def enum_shares(self): 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()) + networks = self.export_info(self.mount.export()) self.logger.display("Enumerating NFS Shares Directories") for share, network in zip(shares, networks): @@ -229,7 +229,7 @@ def enum_shares(self): 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"{self.auth['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") diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index b30370ed4..50a7c22f5 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -6,6 +6,5 @@ def proto_args(parser, parents): dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") dgroup.add_argument("--shares", action="store_true", help="List NFS shares") dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)") - dgroup.add_argument("--uid-brute", nargs="?", type=int, const=4000, metavar="MAX_UID", help="Enumerate shares by bruteforcing UIDs") return parser From 79ecfa8a4da8d6400b2b820303ff198dce895197 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:52:24 +0300 Subject: [PATCH 39/52] uid-brute removed. Added get-file and put-file Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs.py | 98 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 5b381bb96..c9f72e83f 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -245,18 +245,6 @@ def enum_shares(self): finally: self.nfs3.disconnect() - def uid_brute(self): - 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)) - - self.list_exported_shares(self.args.uid_brute, shares) - def list_exported_shares(self, max_uid, shares): self.logger.display(f"Enumerating NFS Shares up to UID {max_uid}") white_list = [] @@ -279,6 +267,92 @@ def list_exported_shares(self, max_uid, shares): continue self.logger.exception(f"{share} - {e}") + def get_file_single(self, remote_file, local_file): + local_file_path = local_file + remote_file_path = remote_file + self.logger.display(f"Downloading {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() + + # Mount the NFS share + mnt_info = self.mount.mnt(remote_file_path, self.auth) + file_handle = mnt_info["mountinfo"]["fhandle"] + file_data = self.nfs3.read(file_handle, auth=self.auth) + + if "resfail" in file_data: + raise Exception("Insufficient Permissions") + else: + entries = file_data["resok"]["data"] + + # Write the data to the local file + with open(local_file_path, "wb+") as local_file: + local_file.write(entries) + + self.logger.highlight(f"File successfully downloaded to {local_file_path} from {remote_file_path}") + + # Unmount the share + self.mount.umnt(self.auth) + + except Exception as e: + self.logger.fail(f'Error writing file "{remote_file_path}" from share "{local_file_path}": {e}') + if os.path.getsize(local_file_path) == 0: + os.remove(local_file_path) + + def get_file(self): + self.get_file_single(self.args.get_file[0], self.args.get_file[1]) + + def put_file_single(self, local_file, remote_file): + local_file_path = local_file + remote_file_path = remote_file + if not remote_file_path.endswith("/"): + remote_file_path += "/" + self.logger.display(f"Uploading {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() + + try: + # Mount the NFS share for create file + mnt_info = self.mount.mnt(remote_file_path, self.auth) + dir_handle = mnt_info["mountinfo"]["fhandle"] + attrs = self.nfs3.getattr(dir_handle, auth=self.auth) + self.auth["uid"] = attrs["attributes"]["uid"] + self.logger.display(f"Trying to create {remote_file_path}{local_file_path}") + self.nfs3.create(dir_handle, local_file_path, 1, auth=self.auth) + self.logger.success(f"{local_file_path} successfully created.") + except Exception as e: + self.logger.fail(f"{local_file_path} was not created.") + self.logger.debug(f"Error while creating remote file: {e}") + + try: + # Mount the NFS share for mount created file + mnt_info = self.mount.mnt(remote_file_path + local_file, self.auth) + file_handle = mnt_info["mountinfo"]["fhandle"] + attrs = self.nfs3.getattr(file_handle, auth=self.auth) + self.auth["uid"] = attrs["attributes"]["uid"] + with open(local_file_path, "rb") as file: + file_data = file.read().decode() + + self.logger.display(f"Trying to write data from {local_file_path}") + self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth) + + self.logger.highlight(f"File {local_file_path} successfully uploaded on {remote_file_path}") + except Exception as e: + self.logger.fail(f"{local_file_path} was not writed.") + self.logger.debug(f"Error while creating remote file: {e}") + + # Unmount the share + self.mount.umnt(self.auth) + except Exception as e: + self.logger.fail(f"Error writing file to share {remote_file_path}: {e}") + + def put_file(self): + self.put_file_single(self.args.put_file[0], self.args.put_file[1]) def convert_size(size_bytes): if size_bytes == 0: From fbfbcc3baa5b398774271a432eb21a8a370c9436 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:53:49 +0300 Subject: [PATCH 40/52] added get, puf file flags Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs/proto_args.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 50a7c22f5..b328d8497 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -6,5 +6,7 @@ def proto_args(parser, parents): dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") dgroup.add_argument("--shares", action="store_true", help="List NFS shares") dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)") + dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file.\n--get-file remote_file local_file") + dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file.\n--put-file local_file remote_file") return parser From 74d7d07b1fae1e5e2bb852fd348f55121a57aba1 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Mon, 30 Sep 2024 23:00:00 +0300 Subject: [PATCH 41/52] import os for downloaded files Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index c9f72e83f..ab66f41ca 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -4,6 +4,7 @@ import re import uuid import math +import os class nfs(connection): From f95b9354b7d34c8cf3d51a6ac9929c24fa9c9408 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Mon, 30 Sep 2024 23:06:45 +0300 Subject: [PATCH 42/52] Renamed get file dispay name Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index ab66f41ca..fdb7d472b 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -271,7 +271,7 @@ def list_exported_shares(self, max_uid, shares): def get_file_single(self, remote_file, local_file): local_file_path = local_file remote_file_path = remote_file - self.logger.display(f"Downloading {local_file_path} to {remote_file_path}") + 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) From 555347752afc889d041cf69b99fa4dbfdfb9d330 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 30 Sep 2024 18:49:33 -0400 Subject: [PATCH 43/52] Fix bug with uid and uid autodetection --- nxc/protocols/nfs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index fdb7d472b..e74b22e90 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -96,6 +96,7 @@ def process_entries(entries, path, uid, recurse): attrs = self.nfs3.getattr(file_handle, auth=self.auth) file_size = attrs["attributes"]["size"] file_size = convert_size(file_size) + uid = attrs["attributes"]["uid"] read_perm, write_perm, exec_perm = self.get_permissions(entry["name_handle"]["handle"]["data"]) contents.append({"path": item_path, "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": file_size, "uid": uid}) @@ -112,7 +113,7 @@ def process_entries(entries, path, uid, recurse): 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": "-"}] + return [{"path": f"{path}/", "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": "-", "uid": self.auth["uid"]}] items = self.nfs3.readdirplus(file_handle, auth=self.auth) if "resfail" in items: From e737dd6aca675731182fb56115054fcc1d33ec48 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 30 Sep 2024 18:50:58 -0400 Subject: [PATCH 44/52] Remove unused function, fix uid detection, add comments and simplify code --- nxc/protocols/nfs.py | 104 ++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 55 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index e74b22e90..280a4e165 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -218,7 +218,7 @@ def enum_shares(self): 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()) + networks = self.export_info(self.mount.export()) self.logger.display("Enumerating NFS Shares Directories") for share, network in zip(shares, networks): @@ -247,114 +247,108 @@ def enum_shares(self): finally: self.nfs3.disconnect() - def list_exported_shares(self, max_uid, shares): - self.logger.display(f"Enumerating NFS Shares up to UID {max_uid}") - white_list = [] - for uid in range(max_uid + 1): - self.auth["uid"] = uid - for share in shares: - try: - if share in white_list: - self.logger.debug(f"Skipping {share} as it is already listed.") - continue - else: - mount_info = self.mount.mnt(share, self.auth) - contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, 1) # Try to list the share with depth 1 - white_list.append(share) - self.logger.success(share) - for content in contents: - self.logger.highlight(f"UID: {self.auth['uid']} {content['path']}") - except Exception as e: - if "Insufficient Permissions" in str(e): - continue - self.logger.exception(f"{share} - {e}") + def get_file(self): + """Downloads a file from the NFS share""" + remote_file_path = self.args.get_file[0] + local_file_path = self.args.get_file[1] + + # Do a bit of smart handling for the local file path + if local_file_path.endswith("/"): + local_file_path += remote_file_path.split("/")[-1] - def get_file_single(self, remote_file, local_file): - local_file_path = local_file - remote_file_path = remote_file 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() - + # Mount the NFS share mnt_info = self.mount.mnt(remote_file_path, self.auth) + # Update the UID for the file + attrs = self.nfs3.getattr(mnt_info["mountinfo"]["fhandle"], auth=self.auth) + self.auth["uid"] = attrs["attributes"]["uid"] file_handle = mnt_info["mountinfo"]["fhandle"] + # Read the file data file_data = self.nfs3.read(file_handle, auth=self.auth) if "resfail" in file_data: raise Exception("Insufficient Permissions") else: - entries = file_data["resok"]["data"] - + data = file_data["resok"]["data"] + # Write the data to the local file with open(local_file_path, "wb+") as local_file: - local_file.write(entries) + local_file.write(data) self.logger.highlight(f"File successfully downloaded to {local_file_path} from {remote_file_path}") # Unmount the share self.mount.umnt(self.auth) - except Exception as e: self.logger.fail(f'Error writing file "{remote_file_path}" from share "{local_file_path}": {e}') - if os.path.getsize(local_file_path) == 0: + if os.path.exists(local_file_path) and os.path.getsize(local_file_path) == 0: os.remove(local_file_path) - - def get_file(self): - self.get_file_single(self.args.get_file[0], self.args.get_file[1]) - - def put_file_single(self, local_file, remote_file): - local_file_path = local_file - remote_file_path = remote_file + + def put_file(self): + """Uploads a file to the NFS share""" + local_file_path = self.args.put_file[0] + remote_file_path = self.args.put_file[1] + file_name = "" + + # Do a bit of smart handling for the file paths + if "/" in local_file_path: + file_name = local_file_path.split("/")[-1] if not remote_file_path.endswith("/"): remote_file_path += "/" - self.logger.display(f"Uploading {local_file_path} to {remote_file_path}") + + 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() - + try: - # Mount the NFS share for create file + # Mount the NFS share to create the file mnt_info = self.mount.mnt(remote_file_path, self.auth) dir_handle = mnt_info["mountinfo"]["fhandle"] + # Update the UID from the directory attrs = self.nfs3.getattr(dir_handle, auth=self.auth) self.auth["uid"] = attrs["attributes"]["uid"] - self.logger.display(f"Trying to create {remote_file_path}{local_file_path}") - self.nfs3.create(dir_handle, local_file_path, 1, auth=self.auth) - self.logger.success(f"{local_file_path} successfully created.") + + # Create file + self.logger.display(f"Trying to create {remote_file_path}{file_name}") + self.nfs3.create(dir_handle, file_name, 1, auth=self.auth) + self.logger.success(f"{file_name} successfully created.") except Exception as e: - self.logger.fail(f"{local_file_path} was not created.") + self.logger.fail(f"{file_name} was not created.") self.logger.debug(f"Error while creating remote file: {e}") - + try: - # Mount the NFS share for mount created file - mnt_info = self.mount.mnt(remote_file_path + local_file, self.auth) + # Mount the NFS share to write the file + mnt_info = self.mount.mnt(remote_file_path, self.auth) file_handle = mnt_info["mountinfo"]["fhandle"] attrs = self.nfs3.getattr(file_handle, auth=self.auth) self.auth["uid"] = attrs["attributes"]["uid"] with open(local_file_path, "rb") as file: file_data = file.read().decode() - - self.logger.display(f"Trying to write data from {local_file_path}") - self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth) - self.logger.highlight(f"File {local_file_path} successfully uploaded on {remote_file_path}") + # Write the data to the remote file + self.logger.display(f"Trying to write data from {local_file_path} to {remote_file_path}") + self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth) + self.logger.success(f"Data from {local_file_path} successfully written to {remote_file_path}") except Exception as e: self.logger.fail(f"{local_file_path} was not writed.") self.logger.debug(f"Error while creating remote file: {e}") - + # Unmount the share self.mount.umnt(self.auth) except Exception as e: self.logger.fail(f"Error writing file to share {remote_file_path}: {e}") + else: + self.logger.highlight(f"File {local_file_path} successfully uploaded to {remote_file_path}") - def put_file(self): - self.put_file_single(self.args.put_file[0], self.args.put_file[1]) def convert_size(size_bytes): if size_bytes == 0: From c1308e6186d442538135d796b251d29691c4a1b6 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 30 Sep 2024 19:04:33 -0400 Subject: [PATCH 45/52] Fix local file name --- nxc/protocols/nfs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 280a4e165..7855befac 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -297,8 +297,7 @@ def put_file(self): file_name = "" # Do a bit of smart handling for the file paths - if "/" in local_file_path: - file_name = local_file_path.split("/")[-1] + file_name = local_file_path.split("/")[-1] if "/" in local_file_path else local_file_path if not remote_file_path.endswith("/"): remote_file_path += "/" From 1eafa319bcc463d8d6a85f46ae4d4fc3cf1fb971 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:30:14 +0300 Subject: [PATCH 46/52] fixed and done get-file for windows and linux Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 7855befac..cd6c269a2 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -251,6 +251,7 @@ def get_file(self): """Downloads a file from the NFS share""" remote_file_path = self.args.get_file[0] local_file_path = self.args.get_file[1] + windows = False # Do a bit of smart handling for the local file path if local_file_path.endswith("/"): @@ -265,12 +266,24 @@ def get_file(self): # Mount the NFS share mnt_info = self.mount.mnt(remote_file_path, self.auth) + if mnt_info["mountinfo"] is None: + windows = True + remote_file_path2, remote_file_path = os.path.split(remote_file_path.rstrip("/")) # For windows. Windows wants to share name, not whole file path. + mnt_info = self.mount.mnt(remote_file_path2, self.auth) + # Update the UID for the file attrs = self.nfs3.getattr(mnt_info["mountinfo"]["fhandle"], auth=self.auth) self.auth["uid"] = attrs["attributes"]["uid"] - file_handle = mnt_info["mountinfo"]["fhandle"] + dir_handle = mnt_info["mountinfo"]["fhandle"] + # Read the file data - file_data = self.nfs3.read(file_handle, auth=self.auth) + if windows: + dir_data = self.nfs3.lookup(dir_handle, remote_file_path, auth=self.auth) + file_handle = dir_data["resok"]["object"]["data"] + file_data = self.nfs3.read(file_handle, auth=self.auth) + else: + file_handle = mnt_info["mountinfo"]["fhandle"] + file_data = self.nfs3.read(file_handle, auth=self.auth) if "resfail" in file_data: raise Exception("Insufficient Permissions") @@ -323,10 +336,11 @@ def put_file(self): except Exception as e: self.logger.fail(f"{file_name} was not created.") self.logger.debug(f"Error while creating remote file: {e}") + exit(-1) try: # Mount the NFS share to write the file - mnt_info = self.mount.mnt(remote_file_path, self.auth) + mnt_info = self.mount.mnt(remote_file_path + "/" + file_name, self.auth) file_handle = mnt_info["mountinfo"]["fhandle"] attrs = self.nfs3.getattr(file_handle, auth=self.auth) self.auth["uid"] = attrs["attributes"]["uid"] @@ -340,6 +354,7 @@ def put_file(self): except Exception as e: self.logger.fail(f"{local_file_path} was not writed.") self.logger.debug(f"Error while creating remote file: {e}") + exit(-1) # Unmount the share self.mount.umnt(self.auth) From f396ff0b66e3e462c7f3c43dd0dc55832c393739 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Oct 2024 16:39:07 -0400 Subject: [PATCH 47/52] Simplify code --- nxc/protocols/nfs.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index cd6c269a2..061adcc25 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -250,12 +250,12 @@ def enum_shares(self): def get_file(self): """Downloads a file from the NFS share""" remote_file_path = self.args.get_file[0] + remote_dir_path, file_name = os.path.split(remote_file_path) local_file_path = self.args.get_file[1] - windows = False # Do a bit of smart handling for the local file path if local_file_path.endswith("/"): - local_file_path += remote_file_path.split("/")[-1] + local_file_path += file_name self.logger.display(f"Downloading {remote_file_path} to {local_file_path}") try: @@ -265,25 +265,17 @@ def get_file(self): self.nfs3.connect() # Mount the NFS share - mnt_info = self.mount.mnt(remote_file_path, self.auth) - if mnt_info["mountinfo"] is None: - windows = True - remote_file_path2, remote_file_path = os.path.split(remote_file_path.rstrip("/")) # For windows. Windows wants to share name, not whole file path. - mnt_info = self.mount.mnt(remote_file_path2, self.auth) + mnt_info = self.mount.mnt(remote_dir_path, self.auth) # Update the UID for the file attrs = self.nfs3.getattr(mnt_info["mountinfo"]["fhandle"], auth=self.auth) self.auth["uid"] = attrs["attributes"]["uid"] dir_handle = mnt_info["mountinfo"]["fhandle"] - # Read the file data - if windows: - dir_data = self.nfs3.lookup(dir_handle, remote_file_path, auth=self.auth) - file_handle = dir_data["resok"]["object"]["data"] - file_data = self.nfs3.read(file_handle, auth=self.auth) - else: - file_handle = mnt_info["mountinfo"]["fhandle"] - file_data = self.nfs3.read(file_handle, auth=self.auth) + # Get the file handle and read the file data + dir_data = self.nfs3.lookup(dir_handle, file_name, auth=self.auth) + file_handle = dir_data["resok"]["object"]["data"] + file_data = self.nfs3.read(file_handle, auth=self.auth) if "resfail" in file_data: raise Exception("Insufficient Permissions") @@ -299,7 +291,7 @@ def get_file(self): # Unmount the share self.mount.umnt(self.auth) except Exception as e: - self.logger.fail(f'Error writing file "{remote_file_path}" from share "{local_file_path}": {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: os.remove(local_file_path) From b5f9e131ee61aab4056c199332ff4864d217ebe1 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Oct 2024 17:43:45 -0400 Subject: [PATCH 48/52] Add error handling for upload, fix permissions for upload and fix upload for windows --- nxc/protocols/nfs.py | 24 ++++++++++-------------- nxc/protocols/nfs/proto_args.py | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 061adcc25..d2f922f1d 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -1,6 +1,6 @@ from nxc.connection import connection from nxc.logger import NXCAdapter -from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3, ACCESS3_READ, ACCESS3_MODIFY, ACCESS3_EXECUTE +from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3, ACCESS3_READ, ACCESS3_MODIFY, ACCESS3_EXECUTE, NFSSTAT3 import re import uuid import math @@ -323,19 +323,17 @@ def put_file(self): # Create file self.logger.display(f"Trying to create {remote_file_path}{file_name}") - self.nfs3.create(dir_handle, file_name, 1, auth=self.auth) - self.logger.success(f"{file_name} successfully created.") + res = self.nfs3.create(dir_handle, file_name, create_mode=1, mode=0o777, auth=self.auth) + if res["status"] != 0: + raise Exception(NFSSTAT3[res["status"]]) + else: + file_handle = res["resok"]["obj"]["handle"]["data"] + self.logger.success(f"{file_name} successfully created") except Exception as e: - self.logger.fail(f"{file_name} was not created.") - self.logger.debug(f"Error while creating remote file: {e}") - exit(-1) + self.logger.fail(f"{file_name} was not created: {e}") + return try: - # Mount the NFS share to write the file - mnt_info = self.mount.mnt(remote_file_path + "/" + file_name, self.auth) - file_handle = mnt_info["mountinfo"]["fhandle"] - attrs = self.nfs3.getattr(file_handle, auth=self.auth) - self.auth["uid"] = attrs["attributes"]["uid"] with open(local_file_path, "rb") as file: file_data = file.read().decode() @@ -344,9 +342,7 @@ def put_file(self): self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth) self.logger.success(f"Data from {local_file_path} successfully written to {remote_file_path}") except Exception as e: - self.logger.fail(f"{local_file_path} was not writed.") - self.logger.debug(f"Error while creating remote file: {e}") - exit(-1) + self.logger.fail(f"Could not write to {local_file_path}: {e}") # Unmount the share self.mount.umnt(self.auth) diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index b328d8497..64d38abc0 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -7,6 +7,6 @@ def proto_args(parser, parents): dgroup.add_argument("--shares", action="store_true", help="List NFS shares") dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)") dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file.\n--get-file remote_file local_file") - dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file.\n--put-file local_file remote_file") + dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions.\n--put-file local_file remote_file") return parser From 998b8787a6c65c231aad1994815f6b486bf3ad88 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Oct 2024 17:59:24 -0400 Subject: [PATCH 49/52] Change arg help --- nxc/protocols/nfs/proto_args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 64d38abc0..1eff132bc 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -6,7 +6,7 @@ def proto_args(parser, parents): dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") dgroup.add_argument("--shares", action="store_true", help="List NFS shares") dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)") - dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file.\n--get-file remote_file local_file") - dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions.\n--put-file local_file remote_file") + dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file") + dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file") return parser From bbb9d15e669b2702991cfd4b4eb83b9a58581851 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:11:42 +0300 Subject: [PATCH 50/52] e2e editted Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- tests/e2e_commands.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index a8d15ba43..190185aad 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -250,5 +250,6 @@ 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 ##### NFS netexec nfs TARGETHOST -u "" -p "" --shares -netexec nfs TARGETHOST -u "" -p "" --shares-list -netexec nfs TARGETHOST -u "" -p "" --bruteforce-uid 2000 +netexec nfs TARGETHOST -u "" -p "" --enum-shares +netexec nfs TARGETHOST -u "" -p "" --get-file /NFStest/test/test.txt ../test.txt +netexec nfs TARGETHOST -u "" -p "" --put-file ../test.txt /NFStest/test From 41681ee1cb0f1cd72a9b9d897cf133eea04b51b2 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:43:42 +0300 Subject: [PATCH 51/52] Fixed put-file Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/nfs.py | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index d2f922f1d..95ab7b082 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -1,5 +1,6 @@ from nxc.connection import connection from nxc.logger import NXCAdapter +from nxc.helpers.logger import highlight from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3, ACCESS3_READ, ACCESS3_MODIFY, ACCESS3_EXECUTE, NFSSTAT3 import re import uuid @@ -301,6 +302,11 @@ def put_file(self): remote_file_path = self.args.put_file[1] file_name = "" + # Check if local file is exist + if not os.path.isfile(local_file_path): + self.logger.fail(f"{local_file_path} does not exist.") + return + # Do a bit of smart handling for the file paths file_name = local_file_path.split("/")[-1] if "/" in local_file_path else local_file_path if not remote_file_path.endswith("/"): @@ -313,14 +319,18 @@ def put_file(self): self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) self.nfs3.connect() - try: - # Mount the NFS share to create the file - mnt_info = self.mount.mnt(remote_file_path, self.auth) - dir_handle = mnt_info["mountinfo"]["fhandle"] - # Update the UID from the directory - attrs = self.nfs3.getattr(dir_handle, auth=self.auth) - self.auth["uid"] = attrs["attributes"]["uid"] + # Mount the NFS share to create the file + mnt_info = self.mount.mnt(remote_file_path, self.auth) + dir_handle = mnt_info["mountinfo"]["fhandle"] + # Update the UID from the directory + attrs = self.nfs3.getattr(dir_handle, auth=self.auth) + self.auth["uid"] = attrs["attributes"]["uid"] + + # Checking if file_name is already exist on remote file path + lookup_response = self.nfs3.lookup(dir_handle, file_name, auth=self.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}{file_name}") res = self.nfs3.create(dir_handle, file_name, create_mode=1, mode=0o777, auth=self.auth) @@ -329,9 +339,16 @@ def put_file(self): else: file_handle = res["resok"]["obj"]["handle"]["data"] self.logger.success(f"{file_name} successfully created") - except Exception as e: - self.logger.fail(f"{file_name} was not created: {e}") - return + else: + # Asking to user overwriting. + ans = input(highlight(f"[!] {file_name} is already exist on {remote_file_path}. Do you want to overwrite it? [Y/n] ", "red")) + if ans.lower() in ["y", "yes", ""]: + # Overwriting + self.logger.display(f"{file_name} is already exist on {remote_file_path}. Trying to overwrite it...") + file_handle = lookup_response["resok"]["object"]["data"] + else: + self.logger.fail(f"Uploading was not successful. The {file_name} is exist on {remote_file_path}") + return try: with open(local_file_path, "rb") as file: From 90cc0e57d7452a94a7914d1609ba269c93ecd384 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 2 Oct 2024 17:56:27 -0400 Subject: [PATCH 52/52] Formating --- nxc/protocols/nfs.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 95ab7b082..30cef26b8 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -272,7 +272,7 @@ def get_file(self): attrs = self.nfs3.getattr(mnt_info["mountinfo"]["fhandle"], auth=self.auth) self.auth["uid"] = attrs["attributes"]["uid"] dir_handle = mnt_info["mountinfo"]["fhandle"] - + # Get the file handle and read the file data dir_data = self.nfs3.lookup(dir_handle, file_name, auth=self.auth) file_handle = dir_data["resok"]["object"]["data"] @@ -322,11 +322,12 @@ def put_file(self): # Mount the NFS share to create the file mnt_info = self.mount.mnt(remote_file_path, self.auth) dir_handle = mnt_info["mountinfo"]["fhandle"] + # Update the UID from the directory attrs = self.nfs3.getattr(dir_handle, auth=self.auth) self.auth["uid"] = attrs["attributes"]["uid"] - - # Checking if file_name is already exist on remote file path + + # Checking if file_name already exists on remote file path lookup_response = self.nfs3.lookup(dir_handle, file_name, auth=self.auth) # If success, file_name does not exist on remote machine. Else, trying to overwrite it. @@ -340,11 +341,10 @@ def put_file(self): file_handle = res["resok"]["obj"]["handle"]["data"] self.logger.success(f"{file_name} successfully created") else: - # Asking to user overwriting. - ans = input(highlight(f"[!] {file_name} is already exist on {remote_file_path}. Do you want to overwrite it? [Y/n] ", "red")) + # Asking the user if they want to overwrite the file + ans = input(highlight(f"[!] {file_name} already exists on {remote_file_path}. Do you want to overwrite it? [Y/n] ", "red")) if ans.lower() in ["y", "yes", ""]: - # Overwriting - self.logger.display(f"{file_name} is already exist on {remote_file_path}. Trying to overwrite it...") + self.logger.display(f"{file_name} already exists on {remote_file_path}. Trying to overwrite it...") file_handle = lookup_response["resok"]["object"]["data"] else: self.logger.fail(f"Uploading was not successful. The {file_name} is exist on {remote_file_path}")