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.
Files changed (29) 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/itunes.py +3 -3
  6. dissect/target/loaders/overlay.py +31 -0
  7. dissect/target/plugins/apps/browser/brave.py +10 -0
  8. dissect/target/plugins/apps/browser/browser.py +43 -0
  9. dissect/target/plugins/apps/browser/chrome.py +10 -0
  10. dissect/target/plugins/apps/browser/chromium.py +234 -12
  11. dissect/target/plugins/apps/browser/edge.py +10 -0
  12. dissect/target/plugins/apps/browser/firefox.py +440 -19
  13. dissect/target/plugins/apps/browser/iexplore.py +1 -1
  14. dissect/target/plugins/apps/container/docker.py +24 -4
  15. dissect/target/plugins/apps/ssh/putty.py +10 -1
  16. dissect/target/plugins/child/docker.py +24 -0
  17. dissect/target/plugins/os/unix/linux/fortios/_os.py +6 -6
  18. dissect/target/plugins/os/windows/catroot.py +11 -2
  19. dissect/target/plugins/os/windows/dpapi/crypto.py +12 -1
  20. dissect/target/plugins/os/windows/dpapi/dpapi.py +62 -7
  21. dissect/target/plugins/os/windows/dpapi/master_key.py +22 -2
  22. dissect/target/plugins/os/windows/sam.py +10 -1
  23. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/METADATA +1 -1
  24. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/RECORD +29 -26
  25. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/COPYRIGHT +0 -0
  26. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/LICENSE +0 -0
  27. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/WHEEL +0 -0
  28. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/entry_points.txt +0 -0
  29. {dissect.target-3.17.dev31.dist-info → dissect.target-3.17.dev33.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ from dissect.target.exceptions import (
7
7
  FilesystemError,
8
8
  IsADirectoryError,
9
9
  NotADirectoryError,
10
+ NotASymlinkError,
10
11
  )
11
12
  from dissect.target.filesystem import Filesystem, FilesystemEntry
12
13
  from dissect.target.helpers import fsutil
@@ -55,6 +56,8 @@ class DirectoryFilesystem(Filesystem):
55
56
 
56
57
 
57
58
  class DirectoryFilesystemEntry(FilesystemEntry):
59
+ entry: Path
60
+
58
61
  def get(self, path: str) -> FilesystemEntry:
59
62
  path = fsutil.join(self.path, path, alt_separator=self.fs.alt_separator)
60
63
  return self.fs.get(path)
@@ -113,7 +116,17 @@ class DirectoryFilesystemEntry(FilesystemEntry):
113
116
  return False
114
117
 
115
118
  def readlink(self) -> str:
116
- return os.readlink(self.entry) # Python 3.7 compatibility
119
+ if not self.is_symlink():
120
+ raise NotASymlinkError()
121
+
122
+ # We want to get the "truest" form of the symlink
123
+ # If we use the readlink() of pathlib.Path directly, it gets thrown into the path parsing of pathlib
124
+ # Because DirectoryFilesystem may also be used with TargetPath, we specifically handle that case here
125
+ # and use os.readlink for host paths
126
+ if isinstance(self.entry, fsutil.TargetPath):
127
+ return self.entry.get().readlink()
128
+ else:
129
+ return os.readlink(self.entry)
117
130
 
118
131
  def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
119
132
  return self._resolve(follow_symlinks=follow_symlinks).entry.lstat()
@@ -0,0 +1,103 @@
1
+ import json
2
+ import logging
3
+ from pathlib import Path
4
+
5
+ from dissect.target.filesystem import LayerFilesystem, VirtualFilesystem
6
+ from dissect.target.filesystems.dir import DirectoryFilesystem
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ class Overlay2Filesystem(LayerFilesystem):
12
+ """Overlay 2 filesystem implementation.
13
+
14
+ Deleted files will be present on the reconstructed filesystem.
15
+ Volumes and bind mounts will be added to their respective mount locations.
16
+ Does not support tmpfs mounts.
17
+
18
+ References:
19
+ - https://docs.docker.com/storage/storagedriver/
20
+ - https://docs.docker.com/storage/volumes/
21
+ - https://www.didactic-security.com/resources/docker-forensics.pdf
22
+ - https://www.didactic-security.com/resources/docker-forensics-cheatsheet.pdf
23
+ - https://github.com/google/docker-explorer
24
+ """
25
+
26
+ __type__ = "overlay2"
27
+
28
+ def __init__(self, path: Path, *args, **kwargs):
29
+ super().__init__(*args, **kwargs)
30
+ self.base_path = path
31
+
32
+ # base_path is /foo/bar/image/overlay2/layerdb/mounts/<id> so we traverse up to /foo/bar to get to the root.
33
+ root = path.parents[4]
34
+
35
+ layers = []
36
+ parent_layer = path.joinpath("parent").read_text()
37
+
38
+ # iterate over all image layers
39
+ while parent_layer:
40
+ hash_type, layer_hash = parent_layer.split(":")
41
+ layer_ref = root.joinpath("image", "overlay2", "layerdb", hash_type, layer_hash)
42
+ cache_id = layer_ref.joinpath("cache-id").read_text()
43
+ layers.append(("/", root.joinpath("overlay2", cache_id, "diff")))
44
+
45
+ if (parent_file := layer_ref.joinpath("parent")).exists():
46
+ parent_layer = parent_file.read_text()
47
+ else:
48
+ parent_layer = None
49
+
50
+ # add the container layers
51
+ for container_layer_name in ["init-id", "mount-id"]:
52
+ layer = path.joinpath(container_layer_name).read_text()
53
+ layers.append(("/", root.joinpath("overlay2", layer, "diff")))
54
+
55
+ # add anonymous volumes, named volumes and bind mounts
56
+ if (config_path := root.joinpath("containers", path.name, "config.v2.json")).exists():
57
+ try:
58
+ config = json.loads(config_path.read_text())
59
+ except json.JSONDecodeError as e:
60
+ log.warning("Unable to parse overlay mounts for container %s", path.name)
61
+ log.debug("", exc_info=e)
62
+ return
63
+
64
+ for mount in config.get("MountPoints").values():
65
+ if not mount["Type"] in ["volume", "bind"]:
66
+ log.warning("Encountered unsupported mount type %s in container %s", mount["Type"], path.name)
67
+ continue
68
+
69
+ if not mount["Source"] and mount["Name"]:
70
+ # anonymous volumes do not have a Source but a volume id
71
+ layer = root.joinpath("volumes", mount["Name"], "_data")
72
+ elif mount["Source"]:
73
+ # named volumes and bind mounts have a Source set
74
+ layer = root.parents[-1].joinpath(mount["Source"])
75
+ else:
76
+ log.warning("Could not determine layer source for mount in container %s", path.name)
77
+ log.debug(json.dumps(mount))
78
+ continue
79
+
80
+ layers.append((mount["Destination"], layer))
81
+
82
+ # add hosts, hostname and resolv.conf files
83
+ for file in ["HostnamePath", "HostsPath", "ResolvConfPath"]:
84
+ if not config.get(file) or not (fp := path.parents[-1].joinpath(config.get(file))).exists():
85
+ log.warning("Container %s has no %s mount", path.name, file)
86
+ continue
87
+
88
+ layers.append(("/etc/" + fp.name, fp))
89
+
90
+ # append and mount every layer
91
+ for dest, layer in layers:
92
+ if layer.is_file() and dest in ["/etc/hosts", "/etc/hostname", "/etc/resolv.conf"]:
93
+ layer_fs = VirtualFilesystem()
94
+ layer_fs.map_file_fh("/etc/" + layer.name, layer.open("rb"))
95
+ dest = dest.split("/")[0]
96
+
97
+ else:
98
+ layer_fs = DirectoryFilesystem(layer)
99
+
100
+ self.append_layer().mount(dest, layer_fs)
101
+
102
+ def __repr__(self) -> str:
103
+ return f"<{self.__class__.__name__} {self.base_path}>"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import io
4
4
  import posixpath
5
5
  import stat
6
+ import sys
6
7
  from typing import IO, TYPE_CHECKING, Iterator, Literal, Optional
7
8
 
8
9
  if TYPE_CHECKING:
@@ -27,11 +28,24 @@ try:
27
28
  self._fs = path._fs
28
29
  self._flavour = path._flavour
29
30
 
30
- def __getitem__(self, idx: int) -> TargetPath:
31
- result = super().__getitem__(idx)
32
- result._fs = self._fs
33
- result._flavour = self._flavour
34
- return result
31
+ if sys.version_info >= (3, 10):
32
+
33
+ def __getitem__(self, idx: int) -> TargetPath:
34
+ result = super().__getitem__(idx)
35
+ result._fs = self._fs
36
+ result._flavour = self._flavour
37
+ return result
38
+
39
+ else:
40
+
41
+ def __getitem__(self, idx: int) -> TargetPath:
42
+ if idx < 0:
43
+ idx = len(self) + idx
44
+
45
+ result = super().__getitem__(idx)
46
+ result._fs = self._fs
47
+ result._flavour = self._flavour
48
+ return result
35
49
 
36
50
  except ImportError:
37
51
  pass
dissect/target/loader.py CHANGED
@@ -199,6 +199,7 @@ register("target", "TargetLoader")
199
199
  register("log", "LogLoader")
200
200
  # Disabling ResLoader because of DIS-536
201
201
  # register("res", "ResLoader")
202
+ register("overlay", "Overlay2Loader")
202
203
  register("phobos", "PhobosLoader")
203
204
  register("velociraptor", "VelociraptorLoader")
204
205
  register("smb", "SmbLoader")
@@ -28,9 +28,9 @@ except ImportError:
28
28
  try:
29
29
  from Crypto.Cipher import AES
30
30
 
31
- HAS_PYCRYPTODOME = True
31
+ HAS_CRYPTO = True
32
32
  except ImportError:
33
- HAS_PYCRYPTODOME = False
33
+ HAS_CRYPTO = False
34
34
 
35
35
 
36
36
  DOMAIN_TRANSLATION = {
@@ -383,7 +383,7 @@ def _create_cipher(key: bytes, iv: bytes = b"\x00" * 16, mode: str = "cbc") -> A
383
383
  raise ValueError(f"Invalid key size: {key_size}")
384
384
 
385
385
  return _pystandalone.cipher(f"aes-{key_size * 8}-{mode}", key, iv)
386
- elif HAS_PYCRYPTODOME:
386
+ elif HAS_CRYPTO:
387
387
  mode_map = {
388
388
  "cbc": (AES.MODE_CBC, True),
389
389
  "ecb": (AES.MODE_ECB, False),
@@ -0,0 +1,31 @@
1
+ from dissect.target.filesystems.overlay import Overlay2Filesystem
2
+ from dissect.target.helpers.fsutil import TargetPath
3
+ from dissect.target.loader import Loader
4
+ from dissect.target.target import Target
5
+
6
+
7
+ class Overlay2Loader(Loader):
8
+ """Load overlay2 filesystems"""
9
+
10
+ def __init__(self, path: TargetPath, **kwargs):
11
+ super().__init__(path.resolve(), **kwargs)
12
+
13
+ @staticmethod
14
+ def detect(path: TargetPath) -> bool:
15
+ # path should be a folder
16
+ if not path.is_dir():
17
+ return False
18
+
19
+ # with the following three files
20
+ for required_file in ["init-id", "parent", "mount-id"]:
21
+ if not path.joinpath(required_file).exists():
22
+ return False
23
+
24
+ # and should have the following parent folders
25
+ if "image/overlay2/layerdb/mounts/" not in path.as_posix():
26
+ return False
27
+
28
+ return True
29
+
30
+ def map(self, target: Target) -> None:
31
+ target.filesystems.add(Overlay2Filesystem(self.path))
@@ -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 BravePlugin(ChromiumMixin, BrowserPlugin):
47
48
  "browser/brave/extension", GENERIC_EXTENSION_RECORD_FIELDS
48
49
  )
49
50
 
51
+ BrowserPasswordRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
52
+ "browser/brave/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 Brave."""
@@ -66,3 +71,8 @@ class BravePlugin(ChromiumMixin, BrowserPlugin):
66
71
  def extensions(self) -> Iterator[BrowserExtensionRecord]:
67
72
  """Return browser extension records for Brave."""
68
73
  yield from super().extensions("brave")
74
+
75
+ @export(record=BrowserPasswordRecord)
76
+ def passwords(self) -> Iterator[BrowserPasswordRecord]:
77
+ """Return browser password records for Brave."""
78
+ yield from super().passwords("brave")
@@ -1,6 +1,10 @@
1
+ from functools import cache
2
+
3
+ from dissect.target.helpers import keychain
1
4
  from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
2
5
  from dissect.target.helpers.record import create_extended_descriptor
3
6
  from dissect.target.plugin import NamespacePlugin
7
+ from dissect.target.target import Target
4
8
 
5
9
  GENERIC_DOWNLOAD_RECORD_FIELDS = [
6
10
  ("datetime", "ts_start"),
@@ -63,6 +67,21 @@ GENERIC_HISTORY_RECORD_FIELDS = [
63
67
  ("uri", "from_url"),
64
68
  ("path", "source"),
65
69
  ]
70
+
71
+ GENERIC_PASSWORD_RECORD_FIELDS = [
72
+ ("datetime", "ts_created"),
73
+ ("datetime", "ts_last_used"),
74
+ ("datetime", "ts_last_changed"),
75
+ ("string", "browser"),
76
+ ("varint", "id"),
77
+ ("uri", "url"),
78
+ ("string", "encrypted_username"),
79
+ ("string", "encrypted_password"),
80
+ ("string", "decrypted_username"),
81
+ ("string", "decrypted_password"),
82
+ ("path", "source"),
83
+ ]
84
+
66
85
  BrowserDownloadRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
67
86
  "browser/download", GENERIC_DOWNLOAD_RECORD_FIELDS
68
87
  )
@@ -75,11 +94,35 @@ BrowserHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension
75
94
  BrowserCookieRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
76
95
  "browser/cookie", GENERIC_COOKIE_FIELDS
77
96
  )
97
+ BrowserPasswordRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
98
+ "browser/password", GENERIC_PASSWORD_RECORD_FIELDS
99
+ )
78
100
 
79
101
 
80
102
  class BrowserPlugin(NamespacePlugin):
81
103
  __namespace__ = "browser"
82
104
 
105
+ def __init__(self, target: Target):
106
+ super().__init__(target)
107
+ self.keychain = cache(self.keychain)
108
+
109
+ def keychain(self) -> set:
110
+ """Retrieve a set of passphrases to use for decrypting saved browser credentials.
111
+
112
+ Always adds an empty passphrase as some browsers encrypt values using empty passphrases.
113
+
114
+ Returns:
115
+ Set of passphrase strings.
116
+ """
117
+ passphrases = set()
118
+ for provider in [self.__namespace__, "browser", "user", None]:
119
+ for key in keychain.get_keys_for_provider(provider) if provider else keychain.get_keys_without_provider():
120
+ if key.key_type == keychain.KeyType.PASSPHRASE:
121
+ passphrases.add(key.value)
122
+
123
+ passphrases.add("")
124
+ return passphrases
125
+
83
126
 
84
127
  def try_idna(url: str) -> bytes:
85
128
  """Attempts to convert a possible Unicode url to ASCII using the IDNA standard.
@@ -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 (
@@ -49,6 +50,10 @@ class ChromePlugin(ChromiumMixin, BrowserPlugin):
49
50
  "browser/chrome/extension", GENERIC_EXTENSION_RECORD_FIELDS
50
51
  )
51
52
 
53
+ BrowserPasswordRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
54
+ "browser/chrome/password", GENERIC_PASSWORD_RECORD_FIELDS
55
+ )
56
+
52
57
  @export(record=BrowserHistoryRecord)
53
58
  def history(self) -> Iterator[BrowserHistoryRecord]:
54
59
  """Return browser history records for Google Chrome."""
@@ -68,3 +73,8 @@ class ChromePlugin(ChromiumMixin, BrowserPlugin):
68
73
  def extensions(self) -> Iterator[BrowserExtensionRecord]:
69
74
  """Return browser extension records for Google Chrome."""
70
75
  yield from super().extensions("chrome")
76
+
77
+ @export(record=BrowserPasswordRecord)
78
+ def passwords(self) -> Iterator[BrowserPasswordRecord]:
79
+ """Return browser password records for Google Chrome."""
80
+ yield from super().passwords("chrome")