From 7cad502a16c51cc6b764e394ab9da18aa4a6c633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Mon, 1 Jun 2026 17:58:09 +0200 Subject: [PATCH 1/6] Add --backups --- nxc/protocols/mssql.py | 29 +++++++++++++++++++++++++++++ nxc/protocols/mssql/proto_args.py | 1 + tests/e2e_commands.txt | 1 + 3 files changed, 31 insertions(+) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index b7edc248ec..3aef70e3c6 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -618,3 +618,32 @@ def lsa(self): ) LSA.dumpCachedHashes() LSA.dumpSecrets() + + @requires_admin + def backups(self): + self.logger.info("Dumping database backups") + query = """ + SELECT + bs.database_name, + bs.server_name, + bmf.physical_device_name AS backup_file_path + FROM msdb.dbo.backupset bs + INNER JOIN msdb.dbo.backupmediafamily bmf + ON bs.media_set_id = bmf.media_set_id + INNER JOIN sys.databases d + ON bs.database_name = d.name + ORDER BY bs.backup_finish_date DESC; + """ + + rows = self.conn.sql_query(query) or [] + if not rows: + self.logger.display("No backups returned") + return + else: + self.logger.display("Enumerated backups") + self.logger.highlight(f"{'Backup Name':<30} {'Backup Path'}") + self.logger.highlight(f"{'----------':<30} {'----------'}") + for row in rows: + database_name = row.get("database_name").strip() + backup_file_path = row.get("backup_file_path").strip() + self.logger.highlight(f"{database_name:<30} {backup_file_path}") diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index 9d494ed44e..e75ae1bc29 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -36,4 +36,5 @@ def proto_args(parser, parents): mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration") mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") + mapping_enum_group.add_argument("--backups", action="store_true", help="dump database backups path") return parser diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 4713e1b9dd..afd49c951f 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -270,6 +270,7 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-bru netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --database netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --backups ##### MSSQL PowerShell netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 From b96c7899eecddd3a8620d332f65cf865e3bcf408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 11:16:56 +0200 Subject: [PATCH 2/6] Patch futur reviews from Azox --- nxc/protocols/mssql.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 3aef70e3c6..d55481a492 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -634,8 +634,10 @@ def backups(self): ON bs.database_name = d.name ORDER BY bs.backup_finish_date DESC; """ - - rows = self.conn.sql_query(query) or [] + rows = self.conn.sql_query(query) + if self.conn.lastError: + self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") + return if not rows: self.logger.display("No backups returned") return From e2a7b853d717069e49a7c6958c88a7c7335abdf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 11:53:10 +0200 Subject: [PATCH 3/6] Remove @requires_admin as it is not --- nxc/protocols/mssql.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index d55481a492..82c5ea838b 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -619,7 +619,6 @@ def lsa(self): LSA.dumpCachedHashes() LSA.dumpSecrets() - @requires_admin def backups(self): self.logger.info("Dumping database backups") query = """ From 262487ba039550ba51e67af95191040dbae2fb63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 12:44:12 +0200 Subject: [PATCH 4/6] Switch --backups to --list-backups --- nxc/protocols/mssql.py | 2 +- nxc/protocols/mssql/proto_args.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 82c5ea838b..25d3699d88 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -619,7 +619,7 @@ def lsa(self): LSA.dumpCachedHashes() LSA.dumpSecrets() - def backups(self): + def list_backups(self): self.logger.info("Dumping database backups") query = """ SELECT diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index e75ae1bc29..3293bdd5ad 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -36,5 +36,5 @@ def proto_args(parser, parents): mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration") mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") - mapping_enum_group.add_argument("--backups", action="store_true", help="dump database backups path") + mapping_enum_group.add_argument("--list-backups", action="store_true", help="dump database backups path") return parser From 77f7f192231f94649fb1856a5dff656d937c8da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 13:48:03 +0200 Subject: [PATCH 5/6] Fix e2e --- 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 afd49c951f..4417b3c208 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -270,7 +270,7 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-bru netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --database netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa -netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --backups +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --list-backups ##### MSSQL PowerShell netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 From c79f649f9d78d555a2acef9c252eb23915aab994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Wed, 8 Jul 2026 17:43:06 +0200 Subject: [PATCH 6/6] Add encryption status --- nxc/protocols/mssql.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 25d3699d88..ae6e8be09e 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -625,11 +625,17 @@ def list_backups(self): SELECT bs.database_name, bs.server_name, - bmf.physical_device_name AS backup_file_path - FROM msdb.dbo.backupset bs - INNER JOIN msdb.dbo.backupmediafamily bmf + bmf.physical_device_name AS backup_file_path, + CASE + WHEN bs.encryptor_type IS NULL THEN 'Unencrypted' + ELSE 'Encrypted' + END AS backup_encryption_status, + bs.encryptor_type, + bs.key_algorithm + FROM msdb.dbo.backupset AS bs + INNER JOIN msdb.dbo.backupmediafamily AS bmf ON bs.media_set_id = bmf.media_set_id - INNER JOIN sys.databases d + INNER JOIN sys.databases AS d ON bs.database_name = d.name ORDER BY bs.backup_finish_date DESC; """ @@ -642,9 +648,10 @@ def list_backups(self): return else: self.logger.display("Enumerated backups") - self.logger.highlight(f"{'Backup Name':<30} {'Backup Path'}") - self.logger.highlight(f"{'----------':<30} {'----------'}") + self.logger.highlight(f"{'Backup Name':<20} {'Encryption':<15} {'Backup Path'}") + self.logger.highlight(f"{'-----------':<20} {'----------':<15} {'-----------'}") for row in rows: database_name = row.get("database_name").strip() + is_encrypted = row.get("backup_encryption_status").strip() backup_file_path = row.get("backup_file_path").strip() - self.logger.highlight(f"{database_name:<30} {backup_file_path}") + self.logger.highlight(f"{database_name:<20} {is_encrypted:<15s} {backup_file_path}")