From 0800101dbcf59008a698d8a02eac5b2fd7e8182d Mon Sep 17 00:00:00 2001 From: Matthijs Vos Date: Thu, 19 Feb 2026 14:47:22 +0100 Subject: [PATCH 1/5] Add NTDS Group Policy records --- dissect/target/plugins/os/windows/ad/ntds.py | 39 +++++++++++++++++++- tests/plugins/os/windows/ad/test_ntds.py | 6 +++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/windows/ad/ntds.py b/dissect/target/plugins/os/windows/ad/ntds.py index 1acbf0aac5..ed730a04a6 100644 --- a/dissect/target/plugins/os/windows/ad/ntds.py +++ b/dissect/target/plugins/os/windows/ad/ntds.py @@ -6,6 +6,7 @@ from dissect.database.ese.ntds import NTDS +from dissect.target.exceptions import RegistryKeyNotFoundError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, UnsupportedPluginError, export, internal from dissect.target.plugins.os.windows.sam import des_decrypt @@ -69,6 +70,18 @@ ], ) +NtdsGPORecord = TargetRecordDescriptor( + "windows/ad/gpo", + [ + ("string", "cn"), + ("string", "distinguished_name"), + ("string", "object_guid"), + ("string", "name"), + ("string", "display_name"), + ("datetime", "creation_time"), + ("datetime", "last_modified_time"), + ], +) # NTDS Registry consts NTDS_PARAMETERS_REGISTRY_PATH = "HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters" @@ -92,9 +105,15 @@ def __init__(self, target: Target): super().__init__(target) self.path = None + # Fallback path + path = "sysvol/windows/NTDS/ntds.dit" if self.target.has_function("registry"): - key = self.target.registry.value(NTDS_PARAMETERS_REGISTRY_PATH, NTDS_PARAMETERS_DB_VALUE) - self.path = self.target.fs.path(key.value) + try: + path = self.target.registry.value(NTDS_PARAMETERS_REGISTRY_PATH, NTDS_PARAMETERS_DB_VALUE).value + except RegistryKeyNotFoundError: + pass + + self.path = self.target.fs.path(path) def check_compatible(self) -> None: if not self.target.has_function("lsa"): @@ -138,6 +157,22 @@ def computers(self) -> Iterator[NtdsComputerRecord]: _target=self.target, ) + @export(record=NtdsGPORecord) + def group_policy(self) -> Iterator[NtdsGPORecord]: + """Extract all group policy objects (GPO) NTDS.dit database.""" + + for gpo in self.ntds.group_policies(): + yield NtdsGPORecord( + cn=gpo.cn, + distinguished_name=gpo.distinguishedName, + object_guid=gpo.guid, + name=gpo.name, + display_name=gpo.displayName, + creation_time=gpo.whenCreated, + last_modified_time=gpo.whenChanged, + _target=self.target, + ) + def extract_user_info(user: User | Computer, target: Target) -> dict[str, Any]: """Extract generic information from a User or Computer account.""" diff --git a/tests/plugins/os/windows/ad/test_ntds.py b/tests/plugins/os/windows/ad/test_ntds.py index 290bc38670..f19fff4cb1 100644 --- a/tests/plugins/os/windows/ad/test_ntds.py +++ b/tests/plugins/os/windows/ad/test_ntds.py @@ -89,3 +89,9 @@ def test_computers(target_win_ntds: Target) -> None: continue assert cn_to_ntlm_hash_mapping[result.cn] == result.nt + + +def test_group_policy(target_win_ntds: Target) -> None: + results = list(target_win_ntds.ad.group_policy()) + + assert len(results) == 5 From 74a0ac9a7b0ecda7ec19d5fdf623492a7a8f511e Mon Sep 17 00:00:00 2001 From: Matthijs Vos Date: Thu, 19 Feb 2026 14:51:56 +0100 Subject: [PATCH 2/5] Rollback NTDS location --- dissect/target/plugins/os/windows/ad/ntds.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/dissect/target/plugins/os/windows/ad/ntds.py b/dissect/target/plugins/os/windows/ad/ntds.py index ed730a04a6..bcb97c835f 100644 --- a/dissect/target/plugins/os/windows/ad/ntds.py +++ b/dissect/target/plugins/os/windows/ad/ntds.py @@ -106,14 +106,9 @@ def __init__(self, target: Target): self.path = None # Fallback path - path = "sysvol/windows/NTDS/ntds.dit" if self.target.has_function("registry"): - try: - path = self.target.registry.value(NTDS_PARAMETERS_REGISTRY_PATH, NTDS_PARAMETERS_DB_VALUE).value - except RegistryKeyNotFoundError: - pass - - self.path = self.target.fs.path(path) + key = self.target.registry.value(NTDS_PARAMETERS_REGISTRY_PATH, NTDS_PARAMETERS_DB_VALUE) + self.path = self.target.fs.path(key.value) def check_compatible(self) -> None: if not self.target.has_function("lsa"): From c10eac97b2d029d64b0a636ef3cf386d47eda236 Mon Sep 17 00:00:00 2001 From: Matthijs Vos Date: Fri, 20 Feb 2026 14:04:13 +0100 Subject: [PATCH 3/5] Cleanup plugin --- dissect/target/plugins/os/windows/ad/ntds.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/dissect/target/plugins/os/windows/ad/ntds.py b/dissect/target/plugins/os/windows/ad/ntds.py index bcb97c835f..0805892d57 100644 --- a/dissect/target/plugins/os/windows/ad/ntds.py +++ b/dissect/target/plugins/os/windows/ad/ntds.py @@ -6,7 +6,6 @@ from dissect.database.ese.ntds import NTDS -from dissect.target.exceptions import RegistryKeyNotFoundError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, UnsupportedPluginError, export, internal from dissect.target.plugins.os.windows.sam import des_decrypt @@ -105,7 +104,6 @@ def __init__(self, target: Target): super().__init__(target) self.path = None - # Fallback path if self.target.has_function("registry"): key = self.target.registry.value(NTDS_PARAMETERS_REGISTRY_PATH, NTDS_PARAMETERS_DB_VALUE) self.path = self.target.fs.path(key.value) @@ -158,13 +156,13 @@ def group_policy(self) -> Iterator[NtdsGPORecord]: for gpo in self.ntds.group_policies(): yield NtdsGPORecord( - cn=gpo.cn, - distinguished_name=gpo.distinguishedName, - object_guid=gpo.guid, - name=gpo.name, - display_name=gpo.displayName, - creation_time=gpo.whenCreated, - last_modified_time=gpo.whenChanged, + cn=gpo.get("cn"), + distinguished_name=gpo.get("distinguishedName"), + object_guid=gpo.get("guid"), + name=gpo.get("name"), + display_name=gpo.get("displayName"), + creation_time=gpo.get("whenCreated"), + last_modified_time=gpo.get("whenChanged"), _target=self.target, ) From 1a3179a7ca83a29575a4949190f3834249bf5a1a Mon Sep 17 00:00:00 2001 From: Matthijs Vos Date: Fri, 20 Feb 2026 14:06:10 +0100 Subject: [PATCH 4/5] Plural name consistency --- dissect/target/plugins/os/windows/ad/ntds.py | 2 +- tests/plugins/os/windows/ad/test_ntds.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dissect/target/plugins/os/windows/ad/ntds.py b/dissect/target/plugins/os/windows/ad/ntds.py index 0805892d57..a349df5d73 100644 --- a/dissect/target/plugins/os/windows/ad/ntds.py +++ b/dissect/target/plugins/os/windows/ad/ntds.py @@ -151,7 +151,7 @@ def computers(self) -> Iterator[NtdsComputerRecord]: ) @export(record=NtdsGPORecord) - def group_policy(self) -> Iterator[NtdsGPORecord]: + def group_policies(self) -> Iterator[NtdsGPORecord]: """Extract all group policy objects (GPO) NTDS.dit database.""" for gpo in self.ntds.group_policies(): diff --git a/tests/plugins/os/windows/ad/test_ntds.py b/tests/plugins/os/windows/ad/test_ntds.py index f19fff4cb1..5488c1ec64 100644 --- a/tests/plugins/os/windows/ad/test_ntds.py +++ b/tests/plugins/os/windows/ad/test_ntds.py @@ -91,7 +91,7 @@ def test_computers(target_win_ntds: Target) -> None: assert cn_to_ntlm_hash_mapping[result.cn] == result.nt -def test_group_policy(target_win_ntds: Target) -> None: - results = list(target_win_ntds.ad.group_policy()) +def test_group_policies(target_win_ntds: Target) -> None: + results = list(target_win_ntds.ad.group_policies()) assert len(results) == 5 From 72a57528e1b38f41f0582bc313975153d4bac34b Mon Sep 17 00:00:00 2001 From: Matthijs Vos Date: Fri, 20 Feb 2026 18:50:59 +0100 Subject: [PATCH 5/5] Use attributes of GroupPolicyContainer --- dissect/target/plugins/os/windows/ad/ntds.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dissect/target/plugins/os/windows/ad/ntds.py b/dissect/target/plugins/os/windows/ad/ntds.py index a349df5d73..bcac2ebe24 100644 --- a/dissect/target/plugins/os/windows/ad/ntds.py +++ b/dissect/target/plugins/os/windows/ad/ntds.py @@ -156,13 +156,13 @@ def group_policies(self) -> Iterator[NtdsGPORecord]: for gpo in self.ntds.group_policies(): yield NtdsGPORecord( - cn=gpo.get("cn"), - distinguished_name=gpo.get("distinguishedName"), - object_guid=gpo.get("guid"), - name=gpo.get("name"), - display_name=gpo.get("displayName"), - creation_time=gpo.get("whenCreated"), - last_modified_time=gpo.get("whenChanged"), + cn=gpo.cn, + distinguished_name=gpo.distinguished_name, + object_guid=gpo.guid, + name=gpo.name, + display_name=gpo.display_name, + creation_time=gpo.when_created, + last_modified_time=gpo.when_changed, _target=self.target, ) @@ -186,7 +186,7 @@ def extract_user_info(user: User | Computer, target: Target) -> dict[str, Any]: # Extract supplemental credentials and yield records return { - "cn": user.get("cn"), + "cn": user.cn, "upn": user.get("userPrincipalName"), "sam_name": user.sam_account_name, "sam_type": user.sam_account_type.name,