dissect.target 3.17.dev31__py3-none-any.whl → 3.17.dev33__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dissect/target/filesystems/dir.py +14 -1
- dissect/target/filesystems/overlay.py +103 -0
- dissect/target/helpers/compat/path_common.py +19 -5
- dissect/target/loader.py +1 -0
- dissect/target/loaders/itunes.py +3 -3
- dissect/target/loaders/overlay.py +31 -0
- dissect/target/plugins/apps/browser/brave.py +10 -0
- dissect/target/plugins/apps/browser/browser.py +43 -0
- dissect/target/plugins/apps/browser/chrome.py +10 -0
- dissect/target/plugins/apps/browser/chromium.py +234 -12
- dissect/target/plugins/apps/browser/edge.py +10 -0
- dissect/target/plugins/apps/browser/firefox.py +440 -19
- dissect/target/plugins/apps/browser/iexplore.py +1 -1
- dissect/target/plugins/apps/container/docker.py +24 -4
- dissect/target/plugins/apps/ssh/putty.py +10 -1
- dissect/target/plugins/child/docker.py +24 -0
- dissect/target/plugins/os/unix/linux/fortios/_os.py +6 -6
- dissect/target/plugins/os/windows/catroot.py +11 -2
- dissect/target/plugins/os/windows/dpapi/crypto.py +12 -1
- dissect/target/plugins/os/windows/dpapi/dpapi.py +62 -7
- dissect/target/plugins/os/windows/dpapi/master_key.py +22 -2
- dissect/target/plugins/os/windows/sam.py +10 -1
- {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/METADATA +1 -1
- {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/RECORD +29 -26
- {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/LICENSE +0 -0
- {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/WHEEL +0 -0
- {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
|
|
1
|
+
import base64
|
1
2
|
import itertools
|
2
3
|
import json
|
3
4
|
from collections import defaultdict
|
@@ -12,17 +13,28 @@ from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
|
|
12
13
|
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
|
13
14
|
from dissect.target.helpers.fsutil import TargetPath, join
|
14
15
|
from dissect.target.helpers.record import create_extended_descriptor
|
15
|
-
from dissect.target.plugin import export
|
16
|
+
from dissect.target.plugin import OperatingSystem, export
|
16
17
|
from dissect.target.plugins.apps.browser.browser import (
|
17
18
|
GENERIC_COOKIE_FIELDS,
|
18
19
|
GENERIC_DOWNLOAD_RECORD_FIELDS,
|
19
20
|
GENERIC_EXTENSION_RECORD_FIELDS,
|
20
21
|
GENERIC_HISTORY_RECORD_FIELDS,
|
22
|
+
GENERIC_PASSWORD_RECORD_FIELDS,
|
21
23
|
BrowserPlugin,
|
22
24
|
try_idna,
|
23
25
|
)
|
24
26
|
from dissect.target.plugins.general.users import UserDetails
|
25
27
|
|
28
|
+
try:
|
29
|
+
from Crypto.Cipher import AES
|
30
|
+
from Crypto.Protocol.KDF import PBKDF2
|
31
|
+
|
32
|
+
HAS_CRYPTO = True
|
33
|
+
|
34
|
+
except ImportError:
|
35
|
+
HAS_CRYPTO = False
|
36
|
+
|
37
|
+
|
26
38
|
CHROMIUM_DOWNLOAD_RECORD_FIELDS = [
|
27
39
|
("uri", "tab_url"),
|
28
40
|
("uri", "tab_referrer_url"),
|
@@ -51,6 +63,10 @@ class ChromiumMixin:
|
|
51
63
|
"browser/chromium/extension", GENERIC_EXTENSION_RECORD_FIELDS
|
52
64
|
)
|
53
65
|
|
66
|
+
BrowserPasswordRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
|
67
|
+
"browser/chromium/password", GENERIC_PASSWORD_RECORD_FIELDS
|
68
|
+
)
|
69
|
+
|
54
70
|
def _build_userdirs(self, hist_paths: list[str]) -> list[tuple[UserDetails, TargetPath]]:
|
55
71
|
"""Join the selected browser dirs with the user home path.
|
56
72
|
|
@@ -65,12 +81,14 @@ class ChromiumMixin:
|
|
65
81
|
for d in hist_paths:
|
66
82
|
cur_dir: TargetPath = user_details.home_path.joinpath(d)
|
67
83
|
cur_dir = cur_dir.resolve()
|
68
|
-
if not cur_dir.exists() or (user_details
|
84
|
+
if not cur_dir.exists() or (user_details, cur_dir) in users_dirs:
|
69
85
|
continue
|
70
|
-
users_dirs.append((user_details
|
86
|
+
users_dirs.append((user_details, cur_dir))
|
71
87
|
return users_dirs
|
72
88
|
|
73
|
-
def _iter_db(
|
89
|
+
def _iter_db(
|
90
|
+
self, filename: str, subdirs: Optional[list[str]] = None
|
91
|
+
) -> Iterator[tuple[UserDetails, TargetPath, SQLite3]]:
|
74
92
|
"""Generate a connection to a sqlite database file.
|
75
93
|
|
76
94
|
Args:
|
@@ -98,9 +116,9 @@ class ChromiumMixin:
|
|
98
116
|
except SQLError as e:
|
99
117
|
self.target.log.warning("Could not open %s file: %s", filename, db_file, exc_info=e)
|
100
118
|
|
101
|
-
def _iter_json(self, filename: str) -> Iterator[tuple[
|
119
|
+
def _iter_json(self, filename: str) -> Iterator[tuple[UserDetails, TargetPath, dict]]:
|
102
120
|
"""Iterate over all JSON files in the user directories, yielding a tuple
|
103
|
-
of
|
121
|
+
of username, JSON file path, and the parsed JSON data.
|
104
122
|
|
105
123
|
Args:
|
106
124
|
filename (str): The name of the JSON file to search for in each
|
@@ -120,7 +138,7 @@ class ChromiumMixin:
|
|
120
138
|
self.target.log.warning("Could not find %s file: %s", filename, json_file)
|
121
139
|
|
122
140
|
def check_compatible(self) -> None:
|
123
|
-
if not
|
141
|
+
if not self._build_userdirs(self.DIRS):
|
124
142
|
raise UnsupportedPluginError("No Chromium-based browser directories found")
|
125
143
|
|
126
144
|
def history(self, browser_name: Optional[str] = None) -> Iterator[BrowserHistoryRecord]:
|
@@ -179,7 +197,7 @@ class ChromiumMixin:
|
|
179
197
|
from_url=try_idna(from_url.url) if from_url else None,
|
180
198
|
source=db_file,
|
181
199
|
_target=self.target,
|
182
|
-
_user=user,
|
200
|
+
_user=user.user,
|
183
201
|
)
|
184
202
|
except SQLError as e:
|
185
203
|
self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)
|
@@ -205,14 +223,48 @@ class ChromiumMixin:
|
|
205
223
|
same_site (bool): Cookie same site flag.
|
206
224
|
"""
|
207
225
|
for user, db_file, db in self._iter_db("Cookies", subdirs=["Network"]):
|
226
|
+
decrypted_key = None
|
227
|
+
|
228
|
+
if self.target.os == OperatingSystem.WINDOWS.value:
|
229
|
+
try:
|
230
|
+
local_state_parent = db_file.parent.parent
|
231
|
+
if db_file.parent.name == "Network":
|
232
|
+
local_state_parent = local_state_parent.parent
|
233
|
+
local_state_path = local_state_parent.joinpath("Local State")
|
234
|
+
|
235
|
+
decrypted_key = self._get_local_state_key(local_state_path, user.user.name)
|
236
|
+
except ValueError:
|
237
|
+
self.target.log.warning("Failed to decrypt local state key")
|
238
|
+
|
208
239
|
try:
|
209
240
|
for cookie in db.table("cookies").rows():
|
241
|
+
cookie_value = cookie.value
|
242
|
+
|
243
|
+
if (
|
244
|
+
not cookie_value
|
245
|
+
and decrypted_key
|
246
|
+
and (enc_value := cookie.get("encrypted_value"))
|
247
|
+
and enc_value.startswith(b"v10")
|
248
|
+
):
|
249
|
+
try:
|
250
|
+
if self.target.os == OperatingSystem.LINUX.value:
|
251
|
+
cookie_value = decrypt_v10(enc_value)
|
252
|
+
elif self.target.os == OperatingSystem.WINDOWS.value:
|
253
|
+
cookie_value = decrypt_v10_2(enc_value, decrypted_key)
|
254
|
+
except (ValueError, UnicodeDecodeError):
|
255
|
+
pass
|
256
|
+
|
257
|
+
if not cookie_value:
|
258
|
+
self.target.log.warning(
|
259
|
+
"Failed to decrypt cookie value for %s %s", cookie.host_key, cookie.name
|
260
|
+
)
|
261
|
+
|
210
262
|
yield self.BrowserCookieRecord(
|
211
263
|
ts_created=webkittimestamp(cookie.creation_utc),
|
212
264
|
ts_last_accessed=webkittimestamp(cookie.last_access_utc),
|
213
265
|
browser=browser_name,
|
214
266
|
name=cookie.name,
|
215
|
-
value=
|
267
|
+
value=cookie_value,
|
216
268
|
host=cookie.host_key,
|
217
269
|
path=cookie.path,
|
218
270
|
expiry=int(cookie.has_expires),
|
@@ -220,7 +272,7 @@ class ChromiumMixin:
|
|
220
272
|
is_http_only=bool(cookie.is_httponly),
|
221
273
|
same_site=bool(cookie.samesite),
|
222
274
|
source=db_file,
|
223
|
-
_user=user,
|
275
|
+
_user=user.user,
|
224
276
|
)
|
225
277
|
except SQLError as e:
|
226
278
|
self.target.log.warning("Error processing cookie file: %s", db_file, exc_info=e)
|
@@ -280,7 +332,7 @@ class ChromiumMixin:
|
|
280
332
|
state=row.get("state"),
|
281
333
|
source=db_file,
|
282
334
|
_target=self.target,
|
283
|
-
_user=user,
|
335
|
+
_user=user.user,
|
284
336
|
)
|
285
337
|
except SQLError as e:
|
286
338
|
self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)
|
@@ -366,11 +418,135 @@ class ChromiumMixin:
|
|
366
418
|
manifest_version=manifest_version,
|
367
419
|
source=json_file,
|
368
420
|
_target=self.target,
|
369
|
-
_user=user,
|
421
|
+
_user=user.user,
|
370
422
|
)
|
371
423
|
except (AttributeError, KeyError) as e:
|
372
424
|
self.target.log.info("No browser extensions found in: %s", json_file, exc_info=e)
|
373
425
|
|
426
|
+
def _get_local_state_key(self, local_state_path: TargetPath, username: str) -> Optional[bytes]:
|
427
|
+
"""Get the Chromium ``os_crypt`` ``encrypted_key`` and decrypt it using DPAPI."""
|
428
|
+
|
429
|
+
if not local_state_path.exists():
|
430
|
+
self.target.log.warning("File %s does not exist.", local_state_path)
|
431
|
+
return None
|
432
|
+
|
433
|
+
try:
|
434
|
+
local_state_conf = json.loads(local_state_path.read_text())
|
435
|
+
except json.JSONDecodeError:
|
436
|
+
self.target.log.warning("File %s does not contain valid JSON.", local_state_path)
|
437
|
+
return None
|
438
|
+
|
439
|
+
if "os_crypt" not in local_state_conf:
|
440
|
+
self.target.log.warning(
|
441
|
+
"File %s does not contain os_crypt, Chrome is likely older than v80.", local_state_path
|
442
|
+
)
|
443
|
+
return None
|
444
|
+
|
445
|
+
encrypted_key = base64.b64decode(local_state_conf["os_crypt"]["encrypted_key"])[5:]
|
446
|
+
decrypted_key = self.target.dpapi.decrypt_user_blob(encrypted_key, username)
|
447
|
+
return decrypted_key
|
448
|
+
|
449
|
+
def passwords(self, browser_name: str = None) -> Iterator[BrowserPasswordRecord]:
|
450
|
+
"""Return browser password records from Chromium browsers.
|
451
|
+
|
452
|
+
Chromium on Linux has ``basic``, ``gnome`` and ``kwallet`` methods for password storage:
|
453
|
+
- ``basic`` ciphertext prefixed with ``v10`` and encrypted with hard coded parameters.
|
454
|
+
- ``gnome`` and ``kwallet`` ciphertext prefixed with ``v11`` which is not implemented (yet).
|
455
|
+
|
456
|
+
Chromium on Windows uses DPAPI user encryption.
|
457
|
+
|
458
|
+
The SHA1 hash of the user's password or the plaintext password is required to decrypt passwords
|
459
|
+
when dealing with encrypted passwords created with Chromium v80 (February 2020) and newer.
|
460
|
+
|
461
|
+
You can supply a SHA1 hash or plaintext password using the keychain.
|
462
|
+
|
463
|
+
Resources:
|
464
|
+
- https://chromium.googlesource.com/chromium/src/+/master/docs/linux/password_storage.md
|
465
|
+
- https://chromium.googlesource.com/chromium/src/+/master/components/os_crypt/sync/os_crypt_linux.cc#40
|
466
|
+
"""
|
467
|
+
|
468
|
+
for user, db_file, db in self._iter_db("Login Data"):
|
469
|
+
decrypted_key = None
|
470
|
+
|
471
|
+
if self.target.os == OperatingSystem.WINDOWS.value:
|
472
|
+
try:
|
473
|
+
local_state_path = db_file.parent.parent.joinpath("Local State")
|
474
|
+
decrypted_key = self._get_local_state_key(local_state_path, user.user.name)
|
475
|
+
except ValueError:
|
476
|
+
self.target.log.warning("Failed to decrypt local state key")
|
477
|
+
|
478
|
+
for row in db.table("logins").rows():
|
479
|
+
encrypted_password: bytes = row.password_value
|
480
|
+
decrypted_password = None
|
481
|
+
|
482
|
+
# 1. Windows DPAPI encrypted password. Chrome > 80
|
483
|
+
# For passwords saved after Chromium v80, we have to use DPAPI to decrypt the AES key
|
484
|
+
# stored by Chromium to encrypt and decrypt passwords.
|
485
|
+
if self.target.os == OperatingSystem.WINDOWS.value and encrypted_password.startswith(b"v10"):
|
486
|
+
if not decrypted_key:
|
487
|
+
self.target.log.warning("Cannot decrypt password, no decrypted_key could be calculated")
|
488
|
+
|
489
|
+
else:
|
490
|
+
try:
|
491
|
+
decrypted_password = decrypt_v10_2(encrypted_password, decrypted_key)
|
492
|
+
except Exception as e:
|
493
|
+
self.target.log.warning("Failed to decrypt AES Chromium password")
|
494
|
+
self.target.log.debug("", exc_info=e)
|
495
|
+
|
496
|
+
# 2. Windows DPAPI encrypted password. Chrome < 80
|
497
|
+
# For passwords saved before Chromium v80, we use DPAPI directly for each entry.
|
498
|
+
elif self.target.os == OperatingSystem.WINDOWS.value and encrypted_password.startswith(
|
499
|
+
b"\x01\x00\x00\x00"
|
500
|
+
):
|
501
|
+
try:
|
502
|
+
decrypted_password = self.target.dpapi.decrypt_blob(encrypted_password)
|
503
|
+
except ValueError as e:
|
504
|
+
self.target.log.warning("Failed to decrypt DPAPI Chromium password")
|
505
|
+
self.target.log.debug("", exc_info=e)
|
506
|
+
except UnsupportedPluginError as e:
|
507
|
+
self.target.log.warning("Target is missing required registry keys for DPAPI")
|
508
|
+
self.target.log.debug("", exc_info=e)
|
509
|
+
|
510
|
+
# 3. Linux 'basic' v10 encrypted password.
|
511
|
+
elif self.target.os != OperatingSystem.WINDOWS.value and encrypted_password.startswith(b"v10"):
|
512
|
+
try:
|
513
|
+
decrypted_password = decrypt_v10(encrypted_password)
|
514
|
+
except Exception as e:
|
515
|
+
self.target.log.warning("Failed to decrypt AES Chromium password")
|
516
|
+
self.target.log.debug("", exc_info=e)
|
517
|
+
|
518
|
+
# 4. Linux 'gnome' or 'kwallet' encrypted password.
|
519
|
+
elif self.target.os != OperatingSystem.WINDOWS.value and encrypted_password.startswith(b"v11"):
|
520
|
+
self.target.log.warning(
|
521
|
+
"Unable to decrypt %s password in '%s': unsupported format", browser_name, db_file
|
522
|
+
)
|
523
|
+
|
524
|
+
# 5. Unsupported.
|
525
|
+
else:
|
526
|
+
prefix = encrypted_password[:10]
|
527
|
+
self.target.log.warning(
|
528
|
+
"Unsupported %s encrypted password found in '%s' with prefix '%s'",
|
529
|
+
browser_name,
|
530
|
+
db_file,
|
531
|
+
prefix,
|
532
|
+
)
|
533
|
+
|
534
|
+
yield self.BrowserPasswordRecord(
|
535
|
+
ts_created=webkittimestamp(row.date_created),
|
536
|
+
ts_last_used=webkittimestamp(row.date_last_used),
|
537
|
+
ts_last_changed=webkittimestamp(row.date_password_modified or 0),
|
538
|
+
browser=browser_name,
|
539
|
+
id=row.id,
|
540
|
+
url=row.origin_url,
|
541
|
+
encrypted_username=None,
|
542
|
+
encrypted_password=base64.b64encode(row.password_value),
|
543
|
+
decrypted_username=row.username_value,
|
544
|
+
decrypted_password=decrypted_password,
|
545
|
+
source=db_file,
|
546
|
+
_target=self.target,
|
547
|
+
_user=user.user,
|
548
|
+
)
|
549
|
+
|
374
550
|
|
375
551
|
class ChromiumPlugin(ChromiumMixin, BrowserPlugin):
|
376
552
|
"""Chromium browser plugin."""
|
@@ -405,3 +581,49 @@ class ChromiumPlugin(ChromiumMixin, BrowserPlugin):
|
|
405
581
|
def extensions(self) -> Iterator[ChromiumMixin.BrowserExtensionRecord]:
|
406
582
|
"""Return browser extension records for Chromium browser."""
|
407
583
|
yield from super().extensions("chromium")
|
584
|
+
|
585
|
+
@export(record=ChromiumMixin.BrowserPasswordRecord)
|
586
|
+
def passwords(self) -> Iterator[ChromiumMixin.BrowserPasswordRecord]:
|
587
|
+
"""Return browser password records for Chromium browser."""
|
588
|
+
yield from super().passwords("chromium")
|
589
|
+
|
590
|
+
|
591
|
+
def remove_padding(decrypted: bytes) -> bytes:
|
592
|
+
number_of_padding_bytes = decrypted[-1]
|
593
|
+
return decrypted[:-number_of_padding_bytes]
|
594
|
+
|
595
|
+
|
596
|
+
def decrypt_v10(encrypted_password: bytes) -> str:
|
597
|
+
if not HAS_CRYPTO:
|
598
|
+
raise ValueError("Missing pycryptodome dependency for AES operation")
|
599
|
+
|
600
|
+
encrypted_password = encrypted_password[3:]
|
601
|
+
|
602
|
+
salt = b"saltysalt"
|
603
|
+
iv = b" " * 16
|
604
|
+
pbkdf_password = "peanuts"
|
605
|
+
|
606
|
+
key = PBKDF2(pbkdf_password, salt, 16, 1)
|
607
|
+
cipher = AES.new(key, AES.MODE_CBC, IV=iv)
|
608
|
+
|
609
|
+
decrypted = cipher.decrypt(encrypted_password)
|
610
|
+
return remove_padding(decrypted).decode()
|
611
|
+
|
612
|
+
|
613
|
+
def decrypt_v10_2(encrypted_password: bytes, key: bytes) -> str:
|
614
|
+
"""
|
615
|
+
struct chrome_pass {
|
616
|
+
byte signature[3] = 'v10';
|
617
|
+
byte iv[12];
|
618
|
+
byte ciphertext[EOF];
|
619
|
+
}
|
620
|
+
"""
|
621
|
+
|
622
|
+
if not HAS_CRYPTO:
|
623
|
+
raise ValueError("Missing pycryptodome dependency for AES operation")
|
624
|
+
|
625
|
+
iv = encrypted_password[3:15]
|
626
|
+
ciphertext = encrypted_password[15:]
|
627
|
+
cipher = AES.new(key, AES.MODE_GCM, iv)
|
628
|
+
plaintext = cipher.decrypt(ciphertext)
|
629
|
+
return plaintext[:-16].decode(errors="backslashreplace")
|
@@ -8,6 +8,7 @@ from dissect.target.plugins.apps.browser.browser import (
|
|
8
8
|
GENERIC_DOWNLOAD_RECORD_FIELDS,
|
9
9
|
GENERIC_EXTENSION_RECORD_FIELDS,
|
10
10
|
GENERIC_HISTORY_RECORD_FIELDS,
|
11
|
+
GENERIC_PASSWORD_RECORD_FIELDS,
|
11
12
|
BrowserPlugin,
|
12
13
|
)
|
13
14
|
from dissect.target.plugins.apps.browser.chromium import (
|
@@ -47,6 +48,10 @@ class EdgePlugin(ChromiumMixin, BrowserPlugin):
|
|
47
48
|
"browser/edge/extension", GENERIC_EXTENSION_RECORD_FIELDS
|
48
49
|
)
|
49
50
|
|
51
|
+
BrowserPasswordRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
|
52
|
+
"browser/edge/password", GENERIC_PASSWORD_RECORD_FIELDS
|
53
|
+
)
|
54
|
+
|
50
55
|
@export(record=BrowserHistoryRecord)
|
51
56
|
def history(self) -> Iterator[BrowserHistoryRecord]:
|
52
57
|
"""Return browser history records for Microsoft Edge."""
|
@@ -66,3 +71,8 @@ class EdgePlugin(ChromiumMixin, BrowserPlugin):
|
|
66
71
|
def extensions(self) -> Iterator[BrowserExtensionRecord]:
|
67
72
|
"""Return browser extension records for Microsoft Edge."""
|
68
73
|
yield from super().extensions("edge")
|
74
|
+
|
75
|
+
@export(record=BrowserPasswordRecord)
|
76
|
+
def passwords(self) -> Iterator[BrowserPasswordRecord]:
|
77
|
+
"""Return browser password records for Microsoft Edge."""
|
78
|
+
yield from super().passwords("edge")
|