Skip to content

Commit c201e1b

Browse files
committed
Improve PEK and DN
1 parent b1a4844 commit c201e1b

File tree

6 files changed

+82
-18
lines changed

6 files changed

+82
-18
lines changed

dissect/database/ese/ntds/database.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from functools import lru_cache
3+
from functools import cached_property, lru_cache
44
from io import BytesIO
55
from typing import TYPE_CHECKING, BinaryIO
66

@@ -10,12 +10,14 @@
1010
from dissect.database.ese.ntds.query import Query
1111
from dissect.database.ese.ntds.schema import Schema
1212
from dissect.database.ese.ntds.sd import ACL, SecurityDescriptor
13-
from dissect.database.ese.ntds.util import SearchFlags, encode_value
13+
from dissect.database.ese.ntds.util import DN, SearchFlags, encode_value
1414

1515
if TYPE_CHECKING:
1616
from collections.abc import Iterator
1717

1818
from dissect.database.ese.index import Index
19+
from dissect.database.ese.ntds.objects import DomainDNS, Top
20+
from dissect.database.ese.ntds.pek import PEK
1921

2022

2123
class Database:
@@ -48,13 +50,13 @@ def __init__(self, db: Database):
4850
self.get = lru_cache(4096)(self.get)
4951
self._make_dn = lru_cache(4096)(self._make_dn)
5052

51-
def root(self) -> Object:
53+
def root(self) -> Top:
5254
"""Return the top-level object in the NTDS database."""
5355
if (root := next(self.children_of(0), None)) is None:
5456
raise ValueError("No root object found")
5557
return root
5658

57-
def root_domain(self) -> Object:
59+
def root_domain(self) -> DomainDNS:
5860
"""Return the root domain object in the NTDS database."""
5961
obj = self.root()
6062
while True:
@@ -72,6 +74,11 @@ def root_domain(self) -> Object:
7274

7375
raise ValueError("No root domain object found")
7476

77+
@cached_property
78+
def pek(self) -> PEK:
79+
"""Return the PEK associated with the root domain."""
80+
return self.root_domain().pek
81+
7582
def walk(self) -> Iterator[Object]:
7683
"""Walk through all objects in the NTDS database."""
7784
stack = [self.root()]
@@ -161,7 +168,7 @@ def children_of(self, dnt: int) -> Iterator[Object]:
161168
yield Object.from_record(self.db, record)
162169
record = cursor.next()
163170

164-
def _make_dn(self, dnt: int) -> str:
171+
def _make_dn(self, dnt: int) -> DN:
165172
"""Construct Distinguished Name (DN) from a Directory Number Tag (DNT) value.
166173
167174
This method walks up the parent hierarchy to build the full DN path.
@@ -180,7 +187,9 @@ def _make_dn(self, dnt: int) -> str:
180187
return ""
181188

182189
parent_dn = self._make_dn(obj.pdnt)
183-
return f"{rdn_key}={rdn_value}".upper() + (f",{parent_dn}" if parent_dn else "")
190+
dn = f"{rdn_key}={rdn_value}".upper() + (f",{parent_dn}" if parent_dn else "")
191+
192+
return DN(dn, obj, parent_dn if parent_dn else None)
184193

185194
def _get_index(self, attribute: str) -> Index:
186195
"""Get the index for a given attribute name.

dissect/database/ese/ntds/ntds.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ def root_domain(self) -> DomainDNS:
3939
"""Return the root domain object of the Active Directory."""
4040
return self.db.data.root_domain()
4141

42-
@cached_property
42+
@property
4343
def pek(self) -> PEK:
4444
"""Return the PEK associated with the root domain."""
45-
return self.root_domain().pek
45+
return self.db.data.pek
4646

4747
def walk(self) -> Iterator[Object]:
4848
"""Walk through all objects in the NTDS database."""

dissect/database/ese/ntds/objects/object.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from functools import cached_property
44
from typing import TYPE_CHECKING, Any, ClassVar
55

6-
from dissect.database.ese.ntds.util import InstanceType, SystemFlags, decode_value
6+
from dissect.database.ese.ntds.util import DN, InstanceType, SystemFlags, decode_value
77

88
if TYPE_CHECKING:
99
from collections.abc import Iterator
@@ -181,12 +181,11 @@ def is_head_of_naming_context(self) -> bool:
181181
return self.instance_type is not None and bool(self.instance_type & InstanceType.HeadOfNamingContext)
182182

183183
@property
184-
def distinguished_name(self) -> str | None:
184+
def distinguished_name(self) -> DN | None:
185185
"""Return the fully qualified Distinguished Name (DN) for this object."""
186-
# return self.db.data._make_dn(self.dnt)
187186
return self.get("distinguishedName")
188187

189-
DN = distinguished_name
188+
dn = distinguished_name
190189

191190
@cached_property
192191
def sd(self) -> SecurityDescriptor | None:

dissect/database/ese/ntds/util.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from collections.abc import Callable
1313

1414
from dissect.database.ese.ntds.database import Database
15+
from dissect.database.ese.ntds.objects import Object
1516

1617

1718
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/7cda533e-d7a4-4aec-a517-91d02ff4a1aa
@@ -144,6 +145,21 @@ class SearchFlags(IntFlag):
144145
Confidential = 0x00000080
145146

146147

148+
def _pek_decrypt(db: Database, value: bytes) -> bytes:
149+
"""Decrypt a PEK-encrypted blob using the database's PEK, if it's unlocked.
150+
151+
Args:
152+
value: The PEK-encrypted data blob.
153+
154+
Returns:
155+
The decrypted data blob.
156+
"""
157+
if not db.data.pek.unlocked:
158+
return value
159+
160+
return db.data.pek.decrypt(value)
161+
162+
147163
ATTRIBUTE_ENCODE_DECODE_MAP: dict[
148164
str, tuple[Callable[[Database, Any], Any] | None, Callable[[Database, Any], Any] | None]
149165
] = {
@@ -162,8 +178,23 @@ class SearchFlags(IntFlag):
162178
None,
163179
lambda db, value: float("inf") if int(value) == ((1 << 63) - 1) else wintimestamp(int(value)),
164180
),
181+
# Protected attributes
182+
"unicodePwd": (None, _pek_decrypt),
183+
"dBCSPwd": (None, _pek_decrypt),
184+
"ntPwdHistory": (None, _pek_decrypt),
185+
"lmPwdHistory": (None, _pek_decrypt),
186+
"supplementalCredentials": (None, _pek_decrypt),
187+
"currentValue": (None, _pek_decrypt),
188+
"priorValue": (None, _pek_decrypt),
189+
"initialAuthIncoming": (None, _pek_decrypt),
190+
"initialAuthOutgoing": (None, _pek_decrypt),
191+
"trustAuthIncoming": (None, _pek_decrypt),
192+
"trustAuthOutgoing": (None, _pek_decrypt),
193+
"msDS-ExecuteScriptPassword": (None, _pek_decrypt),
165194
}
166195

196+
# TODO add for protected attributes
197+
167198

168199
def _ldapDisplayName_to_DNT(db: Database, value: str) -> int | str:
169200
"""Convert an LDAP display name to its corresponding DNT value.
@@ -179,8 +210,10 @@ def _ldapDisplayName_to_DNT(db: Database, value: str) -> int | str:
179210
return value
180211

181212

182-
def _DNT_to_ldapDisplayName(db: Database, value: int) -> str | int:
183-
"""Convert a DNT value to its corresponding LDAP display name.
213+
def _DNT_to_ldapDisplayName(db: Database, value: int) -> str | DN | int:
214+
"""Convert a DNT value to its corresponding LDAP display name or distinguished name.
215+
216+
For attributes and classes, the LDAP display name is returned. For objects, the distinguished name is returned.
184217
185218
Args:
186219
value: The Directory Number Tag to look up.
@@ -197,6 +230,18 @@ def _DNT_to_ldapDisplayName(db: Database, value: int) -> str | int:
197230
return value
198231

199232

233+
class DN(str):
234+
"""A distinguished name (DN) string wrapper. Presents the DN as a string but also retains the underlying object."""
235+
236+
__slots__ = ("object", "parent")
237+
238+
def __new__(cls, value: str, object: Object, parent: DN | None = None):
239+
instance = super().__new__(cls, value)
240+
instance.object = object
241+
instance.parent = parent
242+
return instance
243+
244+
200245
def _oid_to_attrtyp(db: Database, value: str) -> int | str:
201246
"""Convert OID string or LDAP display name to ATTRTYP value.
202247

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ documentation = "https://docs.dissect.tools/en/latest/projects/dissect.database"
3636
repository = "https://github.com/fox-it/dissect.database"
3737

3838
[project.optional-dependencies]
39+
full = [
40+
"pycryptodome"
41+
]
3942
dev = [
4043
"dissect.cstruct>=4.0.dev,<5.0.dev",
4144
"dissect.util>=3.24.dev,<4.0.dev",

tests/ese/ntds/test_pek.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@
99
def test_pek(goad: NTDS) -> None:
1010
"""Test PEK unlocking and decryption."""
1111
syskey = bytes.fromhex("079f95655b66f16deb28aa1ab3a81eb0")
12-
goad.pek.unlock(syskey)
13-
assert goad.pek.unlocked
1412

1513
user = next(goad.users(), None)
1614
assert user is not None
17-
assert user.unicodePwd == bytes.fromhex(
15+
16+
encrypted = user.unicodePwd
17+
# Verify encrypted value
18+
assert encrypted == bytes.fromhex(
1819
"130000000000000029fbdaafb52bf724a51052f668152ac5100000006d06616d95c026064fff245bd256f3d4990f7bffb546f76de566723da4855227"
1920
)
20-
assert goad.pek.decrypt(user.unicodePwd) == bytes.fromhex(
21+
22+
goad.pek.unlock(syskey)
23+
assert goad.pek.unlocked
24+
25+
# Test decryption of the user's password
26+
assert goad.pek.decrypt(encrypted) == bytes.fromhex(
2127
"06bb564317712dc60761a32914e4048c10101010101010101010101010101010"
2228
)
29+
# Should work transparently now too
30+
assert user.unicodePwd == bytes.fromhex("06bb564317712dc60761a32914e4048c10101010101010101010101010101010")

0 commit comments

Comments
 (0)