dissect.target 3.17.dev31__py3-none-any.whl → 3.17.dev34__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. dissect/target/filesystems/dir.py +14 -1
  2. dissect/target/filesystems/overlay.py +103 -0
  3. dissect/target/helpers/compat/path_common.py +19 -5
  4. dissect/target/loader.py +1 -0
  5. dissect/target/loaders/dir.py +23 -5
  6. dissect/target/loaders/itunes.py +3 -3
  7. dissect/target/loaders/overlay.py +31 -0
  8. dissect/target/loaders/velociraptor.py +5 -4
  9. dissect/target/plugins/apps/browser/brave.py +10 -0
  10. dissect/target/plugins/apps/browser/browser.py +43 -0
  11. dissect/target/plugins/apps/browser/chrome.py +10 -0
  12. dissect/target/plugins/apps/browser/chromium.py +234 -12
  13. dissect/target/plugins/apps/browser/edge.py +10 -0
  14. dissect/target/plugins/apps/browser/firefox.py +440 -19
  15. dissect/target/plugins/apps/browser/iexplore.py +1 -1
  16. dissect/target/plugins/apps/container/docker.py +24 -4
  17. dissect/target/plugins/apps/ssh/putty.py +10 -1
  18. dissect/target/plugins/child/docker.py +24 -0
  19. dissect/target/plugins/os/unix/linux/fortios/_os.py +6 -6
  20. dissect/target/plugins/os/windows/catroot.py +11 -2
  21. dissect/target/plugins/os/windows/dpapi/crypto.py +12 -1
  22. dissect/target/plugins/os/windows/dpapi/dpapi.py +62 -7
  23. dissect/target/plugins/os/windows/dpapi/master_key.py +22 -2
  24. dissect/target/plugins/os/windows/sam.py +10 -1
  25. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev34.dist-info}/METADATA +1 -1
  26. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev34.dist-info}/RECORD +31 -28
  27. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev34.dist-info}/COPYRIGHT +0 -0
  28. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev34.dist-info}/LICENSE +0 -0
  29. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev34.dist-info}/WHEEL +0 -0
  30. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev34.dist-info}/entry_points.txt +0 -0
  31. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev34.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.user, cur_dir) in users_dirs:
84
+ if not cur_dir.exists() or (user_details, cur_dir) in users_dirs:
69
85
  continue
70
- users_dirs.append((user_details.user, cur_dir))
86
+ users_dirs.append((user_details, cur_dir))
71
87
  return users_dirs
72
88
 
73
- def _iter_db(self, filename: str, subdirs: Optional[list[str]] = None) -> Iterator[SQLite3]:
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[str, TargetPath, dict]]:
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 user name, JSON file path, and the parsed JSON data.
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 len(self._build_userdirs(self.DIRS)):
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=cookie.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")