dissect.target 3.17.dev34__py3-none-any.whl → 3.17.dev36__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dissect/target/plugins/apps/browser/firefox.py +72 -0
- dissect/target/plugins/os/windows/credhist.py +210 -0
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev36.dist-info}/METADATA +1 -1
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev36.dist-info}/RECORD +9 -8
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev36.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev36.dist-info}/LICENSE +0 -0
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev36.dist-info}/WHEEL +0 -0
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev36.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev36.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,7 @@ from dissect.target.plugin import export
|
|
18
18
|
from dissect.target.plugins.apps.browser.browser import (
|
19
19
|
GENERIC_COOKIE_FIELDS,
|
20
20
|
GENERIC_DOWNLOAD_RECORD_FIELDS,
|
21
|
+
GENERIC_EXTENSION_RECORD_FIELDS,
|
21
22
|
GENERIC_HISTORY_RECORD_FIELDS,
|
22
23
|
GENERIC_PASSWORD_RECORD_FIELDS,
|
23
24
|
BrowserPlugin,
|
@@ -43,6 +44,7 @@ try:
|
|
43
44
|
except ImportError:
|
44
45
|
HAS_CRYPTO = False
|
45
46
|
|
47
|
+
FIREFOX_EXTENSION_RECORD_FIELDS = [("uri", "source_uri"), ("string[]", "optional_permissions")]
|
46
48
|
|
47
49
|
log = logging.getLogger(__name__)
|
48
50
|
|
@@ -76,6 +78,10 @@ class FirefoxPlugin(BrowserPlugin):
|
|
76
78
|
"browser/firefox/download", GENERIC_DOWNLOAD_RECORD_FIELDS
|
77
79
|
)
|
78
80
|
|
81
|
+
BrowserExtensionRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
|
82
|
+
"browser/firefox/extension", GENERIC_EXTENSION_RECORD_FIELDS + FIREFOX_EXTENSION_RECORD_FIELDS
|
83
|
+
)
|
84
|
+
|
79
85
|
BrowserPasswordRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
|
80
86
|
"browser/firefox/password", GENERIC_PASSWORD_RECORD_FIELDS
|
81
87
|
)
|
@@ -305,6 +311,72 @@ class FirefoxPlugin(BrowserPlugin):
|
|
305
311
|
except SQLError as e:
|
306
312
|
self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)
|
307
313
|
|
314
|
+
@export(record=BrowserExtensionRecord)
|
315
|
+
def extensions(self) -> Iterator[BrowserExtensionRecord]:
|
316
|
+
"""Return browser extension records for Firefox.
|
317
|
+
|
318
|
+
Yields BrowserExtensionRecord with the following fields::
|
319
|
+
ts_install (datetime): Extension install timestamp.
|
320
|
+
ts_update (datetime): Extension update timestamp.
|
321
|
+
browser (string): The browser from which the records are generated.
|
322
|
+
id (string): Extension unique identifier.
|
323
|
+
name (string): Name of the extension.
|
324
|
+
short_name (string): Short name of the extension.
|
325
|
+
default_title (string): Default title of the extension.
|
326
|
+
description (string): Description of the extension.
|
327
|
+
version (string): Version of the extension.
|
328
|
+
ext_path (path): Relative path of the extension.
|
329
|
+
from_webstore (boolean): Extension from webstore.
|
330
|
+
permissions (string[]): Permissions of the extension.
|
331
|
+
manifest (varint): Version of the extensions' manifest.
|
332
|
+
optional_permissions (string[]): Optional permissions of the extension.
|
333
|
+
source_uri (path): Source path from which the extension was downloaded.
|
334
|
+
source (path): The source file of the download record.
|
335
|
+
"""
|
336
|
+
for user, _, profile_dir in self._iter_profiles():
|
337
|
+
extension_file = profile_dir.joinpath("extensions.json")
|
338
|
+
|
339
|
+
if not extension_file.exists():
|
340
|
+
self.target.log.warning(
|
341
|
+
"No 'extensions.json' addon file found for user %s in directory %s", user, profile_dir
|
342
|
+
)
|
343
|
+
continue
|
344
|
+
|
345
|
+
try:
|
346
|
+
extensions = json.load(extension_file.open())
|
347
|
+
|
348
|
+
for extension in extensions.get("addons", []):
|
349
|
+
yield self.BrowserExtensionRecord(
|
350
|
+
ts_install=extension.get("installDate", 0) // 1000,
|
351
|
+
ts_update=extension.get("updateDate", 0) // 1000,
|
352
|
+
browser="firefox",
|
353
|
+
id=extension.get("id"),
|
354
|
+
name=extension.get("defaultLocale", {}).get("name"),
|
355
|
+
short_name=None,
|
356
|
+
default_title=None,
|
357
|
+
description=extension.get("defaultLocale", {}).get("description"),
|
358
|
+
version=extension.get("version"),
|
359
|
+
ext_path=extension.get("path"),
|
360
|
+
from_webstore=None,
|
361
|
+
permissions=extension.get("userPermissions", {}).get("permissions"),
|
362
|
+
manifest_version=extension.get("manifestVersion"),
|
363
|
+
source_uri=extension.get("sourceURI"),
|
364
|
+
optional_permissions=extension.get("optionalPermissions", {}).get("permissions"),
|
365
|
+
source=extension_file,
|
366
|
+
_target=self.target,
|
367
|
+
_user=user.user,
|
368
|
+
)
|
369
|
+
|
370
|
+
except FileNotFoundError:
|
371
|
+
self.target.log.info(
|
372
|
+
"No 'extensions.json' addon file found for user %s in directory %s", user, profile_dir
|
373
|
+
)
|
374
|
+
except json.JSONDecodeError:
|
375
|
+
self.target.log.warning(
|
376
|
+
"extensions.json file in directory %s is malformed, consider inspecting the file manually",
|
377
|
+
profile_dir,
|
378
|
+
)
|
379
|
+
|
308
380
|
@export(record=BrowserPasswordRecord)
|
309
381
|
def passwords(self) -> Iterator[BrowserPasswordRecord]:
|
310
382
|
"""Return Firefox browser password records.
|
@@ -0,0 +1,210 @@
|
|
1
|
+
import hashlib
|
2
|
+
import logging
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import BinaryIO, Iterator, Optional
|
6
|
+
from uuid import UUID
|
7
|
+
|
8
|
+
from dissect.cstruct import cstruct
|
9
|
+
from dissect.util.sid import read_sid
|
10
|
+
|
11
|
+
from dissect.target.exceptions import UnsupportedPluginError
|
12
|
+
from dissect.target.helpers import keychain
|
13
|
+
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
|
14
|
+
from dissect.target.helpers.record import create_extended_descriptor
|
15
|
+
from dissect.target.plugin import Plugin, export
|
16
|
+
from dissect.target.plugins.general.users import UserDetails
|
17
|
+
from dissect.target.plugins.os.windows.dpapi.crypto import (
|
18
|
+
CipherAlgorithm,
|
19
|
+
HashAlgorithm,
|
20
|
+
derive_password_hash,
|
21
|
+
)
|
22
|
+
from dissect.target.target import Target
|
23
|
+
|
24
|
+
log = logging.getLogger(__name__)
|
25
|
+
|
26
|
+
|
27
|
+
CredHistRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
|
28
|
+
"windows/credential/history",
|
29
|
+
[
|
30
|
+
("string", "guid"),
|
31
|
+
("boolean", "decrypted"),
|
32
|
+
("string", "sha1"),
|
33
|
+
("string", "nt"),
|
34
|
+
],
|
35
|
+
)
|
36
|
+
|
37
|
+
|
38
|
+
credhist_def = """
|
39
|
+
struct entry {
|
40
|
+
DWORD dwVersion;
|
41
|
+
CHAR guidLink[16];
|
42
|
+
DWORD dwNextLinkSize;
|
43
|
+
DWORD dwCredLinkType;
|
44
|
+
DWORD algHash; // ALG_ID
|
45
|
+
DWORD dwPbkdf2IterationCount;
|
46
|
+
DWORD dwSidSize;
|
47
|
+
DWORD algCrypt; // ALG_ID
|
48
|
+
DWORD dwShaHashSize;
|
49
|
+
DWORD dwNtHashSize;
|
50
|
+
CHAR pSalt[16];
|
51
|
+
CHAR pSid[dwSidSize];
|
52
|
+
CHAR encrypted[0];
|
53
|
+
};
|
54
|
+
"""
|
55
|
+
|
56
|
+
c_credhist = cstruct()
|
57
|
+
c_credhist.load(credhist_def)
|
58
|
+
|
59
|
+
|
60
|
+
@dataclass
|
61
|
+
class CredHistEntry:
|
62
|
+
version: int
|
63
|
+
guid: str
|
64
|
+
user_sid: str
|
65
|
+
sha1: Optional[bytes]
|
66
|
+
nt: Optional[bytes]
|
67
|
+
hash_alg: HashAlgorithm = field(repr=False)
|
68
|
+
cipher_alg: CipherAlgorithm = field(repr=False)
|
69
|
+
raw: c_credhist.entry = field(repr=False)
|
70
|
+
decrypted: bool = False
|
71
|
+
|
72
|
+
def decrypt(self, password_hash: bytes) -> None:
|
73
|
+
"""Decrypt this CREDHIST entry using the provided password hash. Modifies ``CredHistEntry.sha1``
|
74
|
+
and ``CredHistEntry.nt`` values.
|
75
|
+
|
76
|
+
If the decrypted ``nt`` value is 16 bytes we assume the decryption was successful.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
password_hash: Bytes of SHA1 password hash digest.
|
80
|
+
|
81
|
+
Raises:
|
82
|
+
ValueError: If the decryption seems to have failed.
|
83
|
+
"""
|
84
|
+
data = self.cipher_alg.decrypt_with_hmac(
|
85
|
+
data=self.raw.encrypted,
|
86
|
+
key=derive_password_hash(password_hash, self.user_sid),
|
87
|
+
iv=self.raw.pSalt,
|
88
|
+
hash_algorithm=self.hash_alg,
|
89
|
+
rounds=self.raw.dwPbkdf2IterationCount,
|
90
|
+
)
|
91
|
+
|
92
|
+
sha_size = self.raw.dwShaHashSize
|
93
|
+
nt_size = self.raw.dwNtHashSize
|
94
|
+
|
95
|
+
sha1 = data[:sha_size]
|
96
|
+
nt = data[sha_size : sha_size + nt_size].rstrip(b"\x00")
|
97
|
+
|
98
|
+
if len(nt) != 16:
|
99
|
+
raise ValueError("Decrypting failed, invalid password hash?")
|
100
|
+
|
101
|
+
self.decrypted = True
|
102
|
+
self.sha1 = sha1
|
103
|
+
self.nt = nt
|
104
|
+
|
105
|
+
|
106
|
+
class CredHistFile:
|
107
|
+
def __init__(self, fh: BinaryIO) -> None:
|
108
|
+
self.fh = fh
|
109
|
+
self.entries = list(self._parse())
|
110
|
+
|
111
|
+
def __repr__(self) -> str:
|
112
|
+
return f"<CredHistFile fh='{self.fh}' entries={len(self.entries)}>"
|
113
|
+
|
114
|
+
def _parse(self) -> Iterator[CredHistEntry]:
|
115
|
+
self.fh.seek(0)
|
116
|
+
try:
|
117
|
+
while True:
|
118
|
+
entry = c_credhist.entry(self.fh)
|
119
|
+
|
120
|
+
# determine size of encrypted data and add to entry
|
121
|
+
cipher_alg = CipherAlgorithm.from_id(entry.algCrypt)
|
122
|
+
enc_size = entry.dwShaHashSize + entry.dwNtHashSize
|
123
|
+
enc_size += enc_size % cipher_alg.block_length
|
124
|
+
entry.encrypted = self.fh.read(enc_size)
|
125
|
+
|
126
|
+
yield CredHistEntry(
|
127
|
+
version=entry.dwVersion,
|
128
|
+
guid=UUID(bytes_le=entry.guidLink),
|
129
|
+
user_sid=read_sid(entry.pSid),
|
130
|
+
hash_alg=HashAlgorithm.from_id(entry.algHash),
|
131
|
+
cipher_alg=cipher_alg,
|
132
|
+
sha1=None,
|
133
|
+
nt=None,
|
134
|
+
raw=entry,
|
135
|
+
)
|
136
|
+
except EOFError:
|
137
|
+
pass
|
138
|
+
|
139
|
+
def decrypt(self, password_hash: bytes) -> None:
|
140
|
+
"""Decrypt a CREDHIST chain using the provided password SHA1 hash."""
|
141
|
+
|
142
|
+
for entry in reversed(self.entries):
|
143
|
+
try:
|
144
|
+
entry.decrypt(password_hash)
|
145
|
+
except ValueError as e:
|
146
|
+
log.warning("Could not decrypt entry %s with password %s", entry.guid, password_hash.hex())
|
147
|
+
log.debug("", exc_info=e)
|
148
|
+
continue
|
149
|
+
password_hash = entry.sha1
|
150
|
+
|
151
|
+
|
152
|
+
class CredHistPlugin(Plugin):
|
153
|
+
"""Windows CREDHIST file parser.
|
154
|
+
|
155
|
+
Windows XP: ``C:\\Documents and Settings\\username\\Application Data\\Microsoft\\Protect\\CREDHIST``
|
156
|
+
Windows 7 and up: ``C:\\Users\\username\\AppData\\Roaming\\Microsoft\\Protect\\CREDHIST``
|
157
|
+
|
158
|
+
Resources:
|
159
|
+
- https://www.passcape.com/index.php?section=docsys&cmd=details&id=28#41
|
160
|
+
"""
|
161
|
+
|
162
|
+
def __init__(self, target: Target):
|
163
|
+
super().__init__(target)
|
164
|
+
self.files = list(self._find_files())
|
165
|
+
|
166
|
+
def _find_files(self) -> Iterator[tuple[UserDetails, Path]]:
|
167
|
+
hashes = set()
|
168
|
+
for user_details in self.target.user_details.all_with_home():
|
169
|
+
for path in ["AppData/Roaming/Microsoft/Protect/CREDHIST", "Application Data/Microsoft/Protect/CREDHIST"]:
|
170
|
+
credhist_path = user_details.home_path.joinpath(path)
|
171
|
+
if credhist_path.exists() and (hash := credhist_path.get().hash()) not in hashes:
|
172
|
+
hashes.add(hash)
|
173
|
+
yield user_details.user, credhist_path
|
174
|
+
|
175
|
+
def check_compatible(self) -> None:
|
176
|
+
if not self.files:
|
177
|
+
raise UnsupportedPluginError("No CREDHIST files found on target.")
|
178
|
+
|
179
|
+
@export(record=CredHistRecord)
|
180
|
+
def credhist(self) -> Iterator[CredHistRecord]:
|
181
|
+
"""Yield and decrypt all Windows CREDHIST entries on the target."""
|
182
|
+
passwords = keychain_passwords()
|
183
|
+
|
184
|
+
if not passwords:
|
185
|
+
self.target.log.warning("No passwords provided in keychain, cannot decrypt CREDHIST hashes")
|
186
|
+
|
187
|
+
for user, path in self.files:
|
188
|
+
credhist = CredHistFile(path.open("rb"))
|
189
|
+
|
190
|
+
for password in passwords:
|
191
|
+
credhist.decrypt(hashlib.sha1(password.encode("utf-16-le")).digest())
|
192
|
+
|
193
|
+
for entry in credhist.entries:
|
194
|
+
yield CredHistRecord(
|
195
|
+
guid=entry.guid,
|
196
|
+
decrypted=entry.decrypted,
|
197
|
+
sha1=entry.sha1.hex() if entry.sha1 else None,
|
198
|
+
nt=entry.nt.hex() if entry.nt else None,
|
199
|
+
_user=user,
|
200
|
+
_target=self.target,
|
201
|
+
)
|
202
|
+
|
203
|
+
|
204
|
+
def keychain_passwords() -> set:
|
205
|
+
passphrases = set()
|
206
|
+
for key in keychain.get_keys_for_provider("user") + keychain.get_keys_without_provider():
|
207
|
+
if key.key_type == keychain.KeyType.PASSPHRASE:
|
208
|
+
passphrases.add(key.value)
|
209
|
+
passphrases.add("")
|
210
|
+
return passphrases
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dissect.target
|
3
|
-
Version: 3.17.
|
3
|
+
Version: 3.17.dev36
|
4
4
|
Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
6
6
|
License: Affero General Public License v3
|
@@ -124,7 +124,7 @@ dissect/target/plugins/apps/browser/browser.py,sha256=rBIwcgdl73gm-8APwx2jEUAYXR
|
|
124
124
|
dissect/target/plugins/apps/browser/chrome.py,sha256=hxS8gqpBwoCrPaxNpllIa6K9DtsSGzn6XXcUaHyes6w,3048
|
125
125
|
dissect/target/plugins/apps/browser/chromium.py,sha256=1oaQhMN5mJysw0VIVpTEmRCAifgv-mUQxZwrGmGHqAQ,27875
|
126
126
|
dissect/target/plugins/apps/browser/edge.py,sha256=woXzZtHPWmfcV8vbxGKHELKru5JRb32MAXs43_b4K4E,2883
|
127
|
-
dissect/target/plugins/apps/browser/firefox.py,sha256=
|
127
|
+
dissect/target/plugins/apps/browser/firefox.py,sha256=Msicw-13AJWbXRRF6m_p4L84rXAjsIYGFRve29cPY2M,30806
|
128
128
|
dissect/target/plugins/apps/browser/iexplore.py,sha256=MqMonoaM5lj0ZFqGwS4F-P1eLmnLvX7VQGE9S3hxXag,8739
|
129
129
|
dissect/target/plugins/apps/container/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
130
130
|
dissect/target/plugins/apps/container/docker.py,sha256=67Eih9AfUbqsP-HlnlwoHi4rSAnVCZWM76sEyO_1m18,15316
|
@@ -260,6 +260,7 @@ dissect/target/plugins/os/windows/amcache.py,sha256=ZZNOs3bILTf0AGkDkhoatndl0j39
|
|
260
260
|
dissect/target/plugins/os/windows/catroot.py,sha256=eSfVqXvWWZpXoxKB1FT_evjXXNmlD7wHhA3lYpfQDeQ,11043
|
261
261
|
dissect/target/plugins/os/windows/cim.py,sha256=jsrpu6TZpBUh7VWI9AV2Ib5bebTwsvqOwRfa5gjJd7c,3056
|
262
262
|
dissect/target/plugins/os/windows/clfs.py,sha256=begVsZ-CY97Ksh6S1g03LjyBgu8ERY2hfNDWYPj0GXI,4872
|
263
|
+
dissect/target/plugins/os/windows/credhist.py,sha256=FX_pW-tU9esdvDTSx913kf_CpGE_1jbD6bkjDb-cxHk,7069
|
263
264
|
dissect/target/plugins/os/windows/datetime.py,sha256=tuBOkewmbCW8sFXcYp5p82oM5RCsVwmtC79BDCTLz8k,9472
|
264
265
|
dissect/target/plugins/os/windows/defender.py,sha256=Vp_IP6YKm4igR765WvXJrHQ3RMu7FJKM3VOoR8AybV8,23737
|
265
266
|
dissect/target/plugins/os/windows/env.py,sha256=-u9F9xWy6PUbQmu5Tv_MDoVmy6YB-7CbHokIK_T3S44,13891
|
@@ -339,10 +340,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
|
|
339
340
|
dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
|
340
341
|
dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
|
341
342
|
dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
|
342
|
-
dissect.target-3.17.
|
343
|
-
dissect.target-3.17.
|
344
|
-
dissect.target-3.17.
|
345
|
-
dissect.target-3.17.
|
346
|
-
dissect.target-3.17.
|
347
|
-
dissect.target-3.17.
|
348
|
-
dissect.target-3.17.
|
343
|
+
dissect.target-3.17.dev36.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
|
344
|
+
dissect.target-3.17.dev36.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
345
|
+
dissect.target-3.17.dev36.dist-info/METADATA,sha256=jvQ5nLWplAsNNZMyR1nIBwBQZEe-FnQRkk56_YpDAtw,11300
|
346
|
+
dissect.target-3.17.dev36.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
347
|
+
dissect.target-3.17.dev36.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
|
348
|
+
dissect.target-3.17.dev36.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
|
349
|
+
dissect.target-3.17.dev36.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
{dissect.target-3.17.dev34.dist-info → dissect.target-3.17.dev36.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|