From bb0d09335190c63854edea26b360c1fd4649ab21 Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 23 Jan 2026 15:04:35 +0100 Subject: [PATCH 1/7] issue exchange attributes on 2012 R2 --- dissect/database/ese/ntds/schema.py | 5 +++++ dissect/database/ese/ntds/util.py | 1 + dissect/database/ese/tools/ntds.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 dissect/database/ese/tools/ntds.py diff --git a/dissect/database/ese/ntds/schema.py b/dissect/database/ese/ntds/schema.py index 7fe2d73..5838e41 100644 --- a/dissect/database/ese/ntds/schema.py +++ b/dissect/database/ese/ntds/schema.py @@ -170,6 +170,11 @@ def _iter(id: int) -> Iterator[Object]: class_schema = self.lookup_class(name="classSchema") for obj in _iter(attribute_schema.id): + + if obj.get("attributeID", raw=True) is None: + print(f"Weird attributes : {obj.get('lDAPDisplayName')}") + print(obj.as_dict()) + self._add_attribute( dnt=obj.dnt, id=obj.get("attributeID", raw=True), diff --git a/dissect/database/ese/ntds/util.py b/dissect/database/ese/ntds/util.py index db0b22d..31e6a0a 100644 --- a/dissect/database/ese/ntds/util.py +++ b/dissect/database/ese/ntds/util.py @@ -67,6 +67,7 @@ 0x00250000: "1.3.6.1.1.1.1", 0x00260000: "1.3.6.1.1.1.2", 0x46080000: "1.2.840.113556.1.8000.2554", # commonly used for custom attributes +# 0x48230000: "1.2.840.113556.1.4.7000.102", # Related to exchange } diff --git a/dissect/database/ese/tools/ntds.py b/dissect/database/ese/tools/ntds.py new file mode 100644 index 0000000..0c9f71f --- /dev/null +++ b/dissect/database/ese/tools/ntds.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from dissect.database.ese.ntds import NTDS + + +def main() -> None: + parser = argparse.ArgumentParser(description="dissect.database.ese NTDS parser") + parser.add_argument("input", help="NTDS database to read") + parser.add_argument("-o", "--objectClass", help="show only 'object'", required=True) + parser.add_argument("-j", "--json", help="output in JSON format", action="store_true", default=False) + args = parser.parse_args() + + with Path(args.input).open("rb") as fh: + ntds = NTDS(fh) + + for record in ntds.search(objectClass=args.objectClass): + if args.json: + print(json.dumps(record, default=str)) + else: + print(record) + + +if __name__ == "__main__": + main() From 1267f019c97b7f1d8e236ad30b067664d2df2edb Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 23 Jan 2026 15:04:54 +0100 Subject: [PATCH 2/7] dns node --- dissect/database/ese/ntds/ntds.py | 6 +- .../database/ese/ntds/objects/c_dns_record.py | 101 ++++++ dissect/database/ese/ntds/objects/dnsnode.py | 290 ++++++++++++++++++ tests/_data/ese/ntds/large/ntds.dit.gz | 3 - tests/ese/ntds/test_dns_nodename.py | 18 ++ tests/ese/ntds/test_ntds.py | 10 +- 6 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 dissect/database/ese/ntds/objects/c_dns_record.py delete mode 100644 tests/_data/ese/ntds/large/ntds.dit.gz create mode 100644 tests/ese/ntds/test_dns_nodename.py diff --git a/dissect/database/ese/ntds/ntds.py b/dissect/database/ese/ntds/ntds.py index 1dd4047..362be31 100644 --- a/dissect/database/ese/ntds/ntds.py +++ b/dissect/database/ese/ntds/ntds.py @@ -7,7 +7,7 @@ if TYPE_CHECKING: from collections.abc import Iterator - from dissect.database.ese.ntds.objects import Computer, DomainDNS, Group, Object, Server, User + from dissect.database.ese.ntds.objects import Computer, DnsNode, DomainDNS, Group, Object, Server, User from dissect.database.ese.ntds.objects.trusteddomain import TrustedDomain from dissect.database.ese.ntds.pek import PEK @@ -89,3 +89,7 @@ def computers(self) -> Iterator[Computer]: def trusts(self) -> Iterator[TrustedDomain]: """Get all trust objects from the database.""" yield from self.search(objectClass="trustedDomain") + + def dns_node(self) -> Iterator[DnsNode]: + """Get all dnsNode objects from the database.""" + yield from self.search(objectClass="dnsNode") diff --git a/dissect/database/ese/ntds/objects/c_dns_record.py b/dissect/database/ese/ntds/objects/c_dns_record.py new file mode 100644 index 0000000..62a206b --- /dev/null +++ b/dissect/database/ese/ntds/objects/c_dns_record.py @@ -0,0 +1,101 @@ +from dissect.cstruct import cstruct + +dns_record_def = """ + +// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/39b03b89-2264-4063-8198-d62f62a6441a +enum DNS_RECORD_TYPE : uint16 { + ZERO = 0x0000, // An empty record type ([RFC1034] section 3.6 and [RFC1035] section 3.2.2). + A = 0x0001, // An A record type, used for storing an IP address ([RFC1035] section 3.2.2). + NS = 0x0002, // An authoritative name-server + // record type ([RFC1034] section 3.6 and [RFC1035] section 3.2.2). + MD = 0x0003, // A mail-destination record type ([RFC1035] section 3.2.2). + MF = 0x0004, // A mail forwarder record type ([RFC1035] section 3.2.2). + CNAME = 0x0005, // A record type that contains the canonical name of a DNS alias ([RFC1035] section 3.2.2). + SOA = 0x0006, // A Start of Authority (SOA) record type ([RFC1035] section 3.2.2). + MB = 0x0007, // A mailbox record type ([RFC1035] section 3.2.2). + MG = 0x0008, // A mail group member record type ([RFC1035] section 3.2.2). + MR = 0x0009, // A mail-rename record type ([RFC1035] section 3.2.2). + NULL = 0x000A, // A record type for completion queries ([RFC1035] section 3.2.2). + WKS = 0x000B, // A record type for a well-known service ([RFC1035] section 3.2.2). + PTR = 0x000C, // A record type containing FQDN pointer ([RFC1035] section 3.2.2). + HINFO = 0x000D, // A host information record type ([RFC1035] section 3.2.2). + MINFO = 0x000E, // A mailbox or mailing list information record type ([RFC1035] section 3.2.2). + MX = 0x000F, // A mail-exchanger record type ([RFC1035] section 3.2.2). + TXT = 0x0010, // A record type containing a text string ([RFC1035] section 3.2.2). + RP = 0x0011, // A responsible-person record type [RFC1183]. + AFSDB = 0x0012, // A record type containing AFS database location [RFC1183]. + X25 = 0x0013, // An X25 PSDN address record type [RFC1183]. + ISDN = 0x0014, // An ISDN address record type [RFC1183]. + RT = 0x0015, // A route through record type [RFC1183]. + SIG = 0x0018, // A cryptographic public key signature record type [RFC2931]. + KEY = 0x0019, // A record type containing public key used in DNSSEC [RFC2535]. + AAAA = 0x001C, // An IPv6 address record type [RFC3596]. + LOC = 0x001D, // A location information record type [RFC1876]. + NXT = 0x001E, // A next-domain record type [RFC2065]. + SRV = 0x0021, // A server selection record type [RFC2782]. + ATMA = 0x0022, // An Asynchronous Transfer Mode (ATM) address record type [ATMA]. + NAPTR = 0x0023, // An NAPTR record type [RFC2915]. + DNAME = 0x0027, // A DNAME record type [RFC2672]. + DS = 0x002B, // A DS record type [RFC4034]. + RRSIG = 0x002E, // An RRSIG record type [RFC4034]. + NSEC = 0x002F, // An NSEC record type [RFC4034]. + DNSKEY = 0x0030, // A DNSKEY record type [RFC4034]. + DHCID = 0x0031, // A DHCID record type [RFC4701]. + NSEC3 = 0x0032, // An NSEC3 record type [RFC5155]. + NSEC3PARAM = 0x0033, // An NSEC3PARAM record type [RFC5155]. + TLSA = 0x0034, // A TLSA record type [RFC6698]. + ALL = 0x00FF, // A query-only type requesting all records [RFC1035]. + WINS = 0xFF01, // A record type containing Windows Internet Name Service (WINS) + // forward lookup data MS-WINSRADNS_TYPE_WINSR, ]. + WINSR = 0xFF02 // A record type containing WINS reverse lookup data [MS-WINSRA]. +}; + +typedef struct DNS_RECORD_HEADER { + uint16 DataLength; + DNS_RECORD_TYPE Type; + uint8 Version; // Must be 0x05 + uint8 Rank; // Must be 0x05 + uint16 Flags; // Must be 0x00 + uint32 Serial; + uint32 TtlSeconds; // Big Endian + uint32 Reserved; // MUST be 0x00000000. + uint32 TimeStamp; + BYTE Data[DataLength]; +}; + +// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/3fd41adc-c69e-407b-979e-721251403132 +// MS docs indicate that structure is 4 byte aligned, and that The string MUST NOT be null-terminated. +// But observed reality is a null terminated string (null char not counted in NameLength) +typedef struct DNS_RPC_NAME{ + uint8 NameLength; + char dnsName[NameLength+1]; +} + +// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/db37cab7-f121-43ba-81c5-ca0e198d4b9a +typedef struct DNS_RPC_RECORD_SRV { + uint16 Priority; + uint16 Weight; + uint16 Port; + DNS_RPC_NAME nameTarget; +}; + + +// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/f647d391-6614-4c3e-b38b-4df971590eb6 +typedef struct DNS_RPC_RECORD_NAME_PREFERENCE { + uint16 Preference; + DNS_RPC_NAME nameExchange; +}; + + +// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/dcd3ec16-d6bf-4bb4-9128-6172f9e5f066 +typedef struct DNS_RPC_RECORD_SOA { + uint32 Serial; + uint32 Refresh; + uint32 Retry; + uint32 Expire; + uint32 MinimumTtl; + DNS_RPC_NAME namePrimaryServer; + DNS_RPC_NAME ZoneAdministratorEmail; +}; +""" +c_dns_record = cstruct(dns_record_def) diff --git a/dissect/database/ese/ntds/objects/dnsnode.py b/dissect/database/ese/ntds/objects/dnsnode.py index e581dba..83ca25f 100644 --- a/dissect/database/ese/ntds/objects/dnsnode.py +++ b/dissect/database/ese/ntds/objects/dnsnode.py @@ -1,13 +1,303 @@ from __future__ import annotations +import datetime +import logging +import socket +import struct +from typing import NamedTuple + +from dissect.cstruct.utils import hexdump + +from dissect.database.ese.ntds.objects.c_dns_record import c_dns_record from dissect.database.ese.ntds.objects.top import Top +log = logging.getLogger(__name__) + + +def swap_endianess(data: int, int_len: int = 2, unsigned: bool = True) -> int: + """ + Swap endianess for a integer value + Args: + data: integer to conver + int_len: 1, 2, 4 or 8 + unsigned: if integer must be considered a signed or unsigned int + + Returns: + + """ + struct_letter = "h" + match int_len: + case 1: + struct_letter = "b" + case 2: + struct_letter = "h" + case 4: + struct_letter = "i" + case 8: + struct_letter = "q" + + if unsigned: + struct_letter = struct_letter.upper() + return struct.unpack(f">{struct_letter}", struct.pack(f"<{struct_letter}", int(data)))[0] + + +class DnsARecord(NamedTuple): + ipv4_address: str + + @property + def ip_address(self) -> str: + return self.ipv4_address + + +class DnsAAAARecord(NamedTuple): + ipv6_address: str + + @property + def ip_address(self) -> str: + return self.ipv6_address + + +class SOARecord(NamedTuple): + """https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/dcd3ec16-d6bf-4bb4-9128-6172f9e5f066""" + + name_primary_server: str + serial: int + refresh: int + retry: int + minimum_ttl: int + zone_administrator_email: str + + +class NodeNameRecord(NamedTuple): + """https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/8f986756-f151-4f5b-bfcf-0d85be8b0d7e""" + + name_node: str + + +class StringRecord(NamedTuple): + """https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/69166ff5-36c1-4542-9243-13b8931fa447""" + + stringData: str + + +class NamePreferenceRecord(NamedTuple): + """https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/f647d391-6614-4c3e-b38b-4df971590eb6""" + + name_exchange: str + preference: int + + +class SRVRecord(NamedTuple): + """https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/db37cab7-f121-43ba-81c5-ca0e198d4b9a""" + + name_target: str + port: int + weight: int + priority: int + + +class DnsRecord: + def __init__(self, dns_records_bytes: bytes): + self.raw = dns_records_bytes + self.c_record_header = c_dns_record.DNS_RECORD_HEADER(dns_records_bytes) + self.type = self.c_record_header.Type + self.ttl_seconds = swap_endianess(self.c_record_header.TtlSeconds, int_len=4) + self.timestamp = self._timestamp() + + def __repr__(self): + return f"type={self.type!r} ttl_seconds={self.ttl_seconds!r} timestamp={self.timestamp} data={self.data}" + + def _timestamp(self) -> datetime.datetime | None: + """timestamp is stored in hours""" + if self.c_record_header.TimeStamp == 0: + return None + try: + # Windows timestamp is hours since 1601-01-01 + base_date = datetime.datetime(1601, 1, 1, tzinfo=datetime.timezone.utc) + return base_date + datetime.timedelta(hours=self.c_record_header.TimeStamp) + except OverflowError: + return None + + @property + def data(self) -> bytes | DnsARecord | DnsAAAARecord | NodeNameRecord | NamePreferenceRecord | StringRecord| None: + data = bytearray(self.c_record_header.Data) + DNS_RECORD_TYPE = c_dns_record.DNS_RECORD_TYPE + match self.type: + case DNS_RECORD_TYPE.A: + return self._parse_a_record(data) + case c_dns_record.DNS_RECORD_TYPE.AAAA: + return self._parse_aaaa_record(data) + case ( + DNS_RECORD_TYPE.PTR + | DNS_RECORD_TYPE.NS + | DNS_RECORD_TYPE.CNAME + | DNS_RECORD_TYPE.DNAME + | DNS_RECORD_TYPE.MB + | DNS_RECORD_TYPE.MR + | DNS_RECORD_TYPE.MG + | DNS_RECORD_TYPE.MD + | DNS_RECORD_TYPE.MF + ): + return self._parse_node_name_record(data) + case DNS_RECORD_TYPE.MX | DNS_RECORD_TYPE.AFSDB | DNS_RECORD_TYPE.RT: + return self._parse_name_preference_record(data) + case DNS_RECORD_TYPE.SRV: + return self._parse_srv_record(data) + case DNS_RECORD_TYPE.SOA: + return self._parse_soa_record(data) + case DNS_RECORD_TYPE.HINFO | DNS_RECORD_TYPE.ISDN | DNS_RECORD_TYPE.TXT, DNS_RECORD_TYPE.X25 | DNS_RECORD_TYPE.LOC: + return self._parse_string_record(data) + return data + + @classmethod + def _parse_a_record(cls, data: bytes) -> DnsARecord | None: + """Parse A record (IPv4 address)""" + if len(data) >= 4: + ip = socket.inet_ntop(socket.AF_INET, data[:4]) + return DnsARecord(ipv4_address=ip) + return None + + @classmethod + def _parse_aaaa_record(cls, data: bytes) -> DnsAAAARecord | None: + """Parse AAAA record (IPv4 address)""" + if len(data) >= 16: + print() + ip = socket.inet_ntop(socket.AF_INET6, data[:16]) + return DnsAAAARecord(ipv6_address=ip) + return None + + @classmethod + def _parse_soa_record(cls, data: bytes) -> SOARecord | None: + """https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/dcd3ec16-d6bf-4bb4-9128-6172f9e5f066 + Todo parse all fields + """ + try: + dns_rpc_record_soa = c_dns_record.DNS_RPC_RECORD_SOA(data) + + return SOARecord( + name_primary_server=cls._parse_dns_name(dns_rpc_record_soa.namePrimaryServer.dnsName), + serial=swap_endianess(dns_rpc_record_soa.Serial, int_len=4), + refresh=swap_endianess(dns_rpc_record_soa.Refresh, int_len=4), + retry=swap_endianess(dns_rpc_record_soa.Retry, int_len=4), + minimum_ttl=swap_endianess(dns_rpc_record_soa.MinimumTtl, int_len=4), + zone_administrator_email=cls._parse_dns_name(dns_rpc_record_soa.ZoneAdministratorEmail.dnsName), + ) + except EOFError: + return None + + @classmethod + def _parse_node_name_record(cls, data: bytes) -> NodeNameRecord | None: + """Parse Node Name type record, used for following record type : + DNS_TYPE_PTR, DNS_TYPE_N, DNS_TYPE_CNAM, DNS_TYPE_DNAM, + DNS_TYPE_M, DNS_TYPE_M, DNS_TYPE_M, DNS_TYPE_M, DNS_TYPE_MF + + """ + try: + return NodeNameRecord(cls._parse_dns_name(c_dns_record.DNS_RPC_NAME(data).dnsName)) + except EOFError: + log.warning("Error while processing node name record%s", data) + hexdump(data) + return None + + @classmethod + def _parse_name_preference_record(cls, data: bytes) -> NamePreferenceRecord | None: + """Parse DNS_RPC_RECORD_NAME_PREFERENCE record (E.g Mx)""" + try: + dns_rpc_record_name_preference = c_dns_record.DNS_RPC_RECORD_NAME_PREFERENCE(data) + return NamePreferenceRecord( + preference=dns_rpc_record_name_preference.Preference, + name_exchange=cls._parse_dns_name(dns_rpc_record_name_preference.nameExchange.dnsName), + ) + except EOFError: + return None + + @classmethod + def _parse_srv_record(cls, data: bytes) -> SRVRecord | None: + """Parse SRV record""" + try: + dns_rpc_record_srv = c_dns_record.DNS_RPC_RECORD_SRV(data) + target = cls._parse_dns_name(dns_rpc_record_srv.nameTarget.dnsName) + return SRVRecord( + priority=dns_rpc_record_srv.Priority, + weight=dns_rpc_record_srv.Weight, + port=dns_rpc_record_srv.Port, + name_target=target, + ) + except EOFError: + return None + + @classmethod + def _parse_string_record(cls, data: bytes) -> StringRecord | None: + """Parse Node Name type record, used for following record type : + DNS_TYPE_HINFO, DNS_TYPE_ISDN, DNS_TYPE_TXT, DNS_TYPE_X25, DNS_TYPE_LOC + """ + try: + return StringRecord(c_dns_record.DNS_RPC_NAME(data).dnsName.decode('utf-8', errors="backslashreplace")) + except EOFError: + log.warning("Error while processing node name record%s", data) + hexdump(data) + return None + + @classmethod + def _parse_dns_name(cls, data: bytes) -> str: + """Parse DNS name as specified in rfc1035#section-3.1 format + + Args: + data: + + Returns: + + References: + - https://datatracker.ietf.org/doc/html/rfc1035#section-3.1 + """ + if not data: + return "" + _nb_segment = data[0] + data = data[1:] + name_parts = [] + offset = 0 + # Domain names in messages are expressed in terms of a sequence of labels. + # Each label is represented as a one octet length field followed by that + # number of octets. Since every domain name ends with the null label of + # the root, a domain name is terminated by a length byte of zero. + while offset < len(data): + length = data[offset] + if length == 0: + name_parts.append("") + break + # The high order two bits of every length octet must be zero, and the + # remaining six bits of the length field limit the label to 63 octets or + # less. + if length > 63: # Compression pointer + return "" + + offset += 1 + if offset + length > len(data): + return "" + + part = data[offset : offset + length].decode("utf-8", errors="backslashreplace") + name_parts.append(part) + offset += length + + return ".".join(name_parts) if name_parts else "" + class DnsNode(Top): """Represents a DNS node object in the Active Directory. References: - https://learn.microsoft.com/en-us/windows/win32/adschema/c-dnsnode + - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/6912b338-5472-4f59-b912-0edb536b6ed8 """ __object_class__ = "dnsNode" + + def __repr_body__(self) -> str: + return f"name={self.name!r}, records=|{'|'.join(repr(d) for d in self.dns_record)}|" + + @property + def dns_record(self) -> list[DnsRecord]: + dns_record = self.get("dnsRecord") + if dns_record is None: + return [] + return [DnsRecord(x) for x in dns_record] diff --git a/tests/_data/ese/ntds/large/ntds.dit.gz b/tests/_data/ese/ntds/large/ntds.dit.gz deleted file mode 100644 index 92b337e..0000000 --- a/tests/_data/ese/ntds/large/ntds.dit.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ac1f9f526c817633ef3d6a73a26d6cfd5490a86e0ff8a1f64791fef12e95506f -size 126730533 diff --git a/tests/ese/ntds/test_dns_nodename.py b/tests/ese/ntds/test_dns_nodename.py new file mode 100644 index 0000000..b7703c9 --- /dev/null +++ b/tests/ese/ntds/test_dns_nodename.py @@ -0,0 +1,18 @@ +import pytest + +from dissect.database.ese.ntds.objects.dnsnode import DnsRecord, _parse_dns_name + + +@pytest.mark.parametrize( + ("data", "expected_output"), + [(b"\x03\x0ckingslanding\rsevenkingdoms\x05local", "kingslanding.sevenkingdoms.local")], +) +def test_parse_dns_name(data: bytes, expected_output: str) -> None: + assert _parse_dns_name(data) == expected_output + + +@pytest.mark.parametrize( + ("data", "expected_output"), [(b"\x11\x03\x06dc2-eu\x04test\x03lan\x00", "dc2-eu.test.lan.")], ids=["odd_length"] +) +def test_parse_dns_node_name(data: bytes, expected_output: str) -> None: + assert DnsRecord._parse_node_name_record(data).name_node == expected_output diff --git a/tests/ese/ntds/test_ntds.py b/tests/ese/ntds/test_ntds.py index c06e5c2..b200ce0 100644 --- a/tests/ese/ntds/test_ntds.py +++ b/tests/ese/ntds/test_ntds.py @@ -4,7 +4,7 @@ import pytest -from dissect.database.ese.ntds.objects import Computer, Group, Server, SubSchema, User +from dissect.database.ese.ntds.objects import Computer, DnsNode, Group, Server, SubSchema, User if TYPE_CHECKING: from dissect.database.ese.ntds import NTDS @@ -259,3 +259,11 @@ def test_all_memberships(large: NTDS) -> None: for user in large.users(): # Just iterate all memberships to see if any errors occur list(user.groups()) + + +def test_dnsnode(goad: NTDS) -> None: + dns_node: list[DnsNode] = sorted(goad.dns_node(), key=lambda x: x.name) + assert len(dns_node) == 113 + a = sum((d.dns_record for d in dns_node), []) + print(list(u for u in a)) + assert len(a) == 100 From 5adf3b759096df18143f2d2a360b0a11f3aed86b Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 28 Jan 2026 13:07:31 +0100 Subject: [PATCH 3/7] Revert --- dissect/database/ese/ntds/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dissect/database/ese/ntds/util.py b/dissect/database/ese/ntds/util.py index 31e6a0a..db0b22d 100644 --- a/dissect/database/ese/ntds/util.py +++ b/dissect/database/ese/ntds/util.py @@ -67,7 +67,6 @@ 0x00250000: "1.3.6.1.1.1.1", 0x00260000: "1.3.6.1.1.1.2", 0x46080000: "1.2.840.113556.1.8000.2554", # commonly used for custom attributes -# 0x48230000: "1.2.840.113556.1.4.7000.102", # Related to exchange } From 7dc9ff73a216b9b138b90e7a11f832afb03867b7 Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 28 Jan 2026 13:09:10 +0100 Subject: [PATCH 4/7] revert change --- dissect/database/ese/tools/ntds.py | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 dissect/database/ese/tools/ntds.py diff --git a/dissect/database/ese/tools/ntds.py b/dissect/database/ese/tools/ntds.py deleted file mode 100644 index 0c9f71f..0000000 --- a/dissect/database/ese/tools/ntds.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -import argparse -import json -from pathlib import Path - -from dissect.database.ese.ntds import NTDS - - -def main() -> None: - parser = argparse.ArgumentParser(description="dissect.database.ese NTDS parser") - parser.add_argument("input", help="NTDS database to read") - parser.add_argument("-o", "--objectClass", help="show only 'object'", required=True) - parser.add_argument("-j", "--json", help="output in JSON format", action="store_true", default=False) - args = parser.parse_args() - - with Path(args.input).open("rb") as fh: - ntds = NTDS(fh) - - for record in ntds.search(objectClass=args.objectClass): - if args.json: - print(json.dumps(record, default=str)) - else: - print(record) - - -if __name__ == "__main__": - main() From 8a3a85f729ae62275a15a4a804522a2cd111acb5 Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 28 Jan 2026 13:13:19 +0100 Subject: [PATCH 5/7] revert change --- tests/_data/ese/ntds/large/ntds.dit.gz | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 tests/_data/ese/ntds/large/ntds.dit.gz diff --git a/tests/_data/ese/ntds/large/ntds.dit.gz b/tests/_data/ese/ntds/large/ntds.dit.gz new file mode 100644 index 0000000..92b337e --- /dev/null +++ b/tests/_data/ese/ntds/large/ntds.dit.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac1f9f526c817633ef3d6a73a26d6cfd5490a86e0ff8a1f64791fef12e95506f +size 126730533 From 7bdfe8b179126c29faabd0955c43504383fd8c27 Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 28 Jan 2026 13:14:03 +0100 Subject: [PATCH 6/7] revert changes --- dissect/database/ese/ntds/schema.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dissect/database/ese/ntds/schema.py b/dissect/database/ese/ntds/schema.py index 5838e41..fadfb41 100644 --- a/dissect/database/ese/ntds/schema.py +++ b/dissect/database/ese/ntds/schema.py @@ -171,9 +171,6 @@ def _iter(id: int) -> Iterator[Object]: for obj in _iter(attribute_schema.id): - if obj.get("attributeID", raw=True) is None: - print(f"Weird attributes : {obj.get('lDAPDisplayName')}") - print(obj.as_dict()) self._add_attribute( dnt=obj.dnt, From 4345689e1cadfbe21dd402c6903f23d27caf62e4 Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 28 Jan 2026 13:35:41 +0100 Subject: [PATCH 7/7] Typo --- dissect/database/ese/ntds/schema.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dissect/database/ese/ntds/schema.py b/dissect/database/ese/ntds/schema.py index fadfb41..7fe2d73 100644 --- a/dissect/database/ese/ntds/schema.py +++ b/dissect/database/ese/ntds/schema.py @@ -170,8 +170,6 @@ def _iter(id: int) -> Iterator[Object]: class_schema = self.lookup_class(name="classSchema") for obj in _iter(attribute_schema.id): - - self._add_attribute( dnt=obj.dnt, id=obj.get("attributeID", raw=True),