dissect.target 3.16.dev44__py3-none-any.whl → 3.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. dissect/target/container.py +1 -0
  2. dissect/target/containers/fortifw.py +190 -0
  3. dissect/target/filesystem.py +192 -67
  4. dissect/target/filesystems/dir.py +14 -1
  5. dissect/target/filesystems/overlay.py +103 -0
  6. dissect/target/helpers/compat/path_common.py +19 -5
  7. dissect/target/helpers/configutil.py +30 -7
  8. dissect/target/helpers/network_managers.py +101 -73
  9. dissect/target/helpers/record_modifier.py +4 -1
  10. dissect/target/loader.py +3 -1
  11. dissect/target/loaders/dir.py +23 -5
  12. dissect/target/loaders/itunes.py +3 -3
  13. dissect/target/loaders/mqtt.py +309 -0
  14. dissect/target/loaders/overlay.py +31 -0
  15. dissect/target/loaders/target.py +12 -9
  16. dissect/target/loaders/vb.py +2 -2
  17. dissect/target/loaders/velociraptor.py +5 -4
  18. dissect/target/plugin.py +1 -1
  19. dissect/target/plugins/apps/browser/brave.py +10 -0
  20. dissect/target/plugins/apps/browser/browser.py +43 -0
  21. dissect/target/plugins/apps/browser/chrome.py +10 -0
  22. dissect/target/plugins/apps/browser/chromium.py +234 -12
  23. dissect/target/plugins/apps/browser/edge.py +10 -0
  24. dissect/target/plugins/apps/browser/firefox.py +512 -19
  25. dissect/target/plugins/apps/browser/iexplore.py +2 -2
  26. dissect/target/plugins/apps/container/docker.py +24 -4
  27. dissect/target/plugins/apps/ssh/openssh.py +4 -0
  28. dissect/target/plugins/apps/ssh/putty.py +45 -14
  29. dissect/target/plugins/apps/ssh/ssh.py +40 -0
  30. dissect/target/plugins/apps/vpn/openvpn.py +115 -93
  31. dissect/target/plugins/child/docker.py +24 -0
  32. dissect/target/plugins/filesystem/ntfs/mft.py +1 -1
  33. dissect/target/plugins/filesystem/walkfs.py +2 -2
  34. dissect/target/plugins/general/users.py +6 -0
  35. dissect/target/plugins/os/unix/bsd/__init__.py +0 -0
  36. dissect/target/plugins/os/unix/esxi/_os.py +2 -2
  37. dissect/target/plugins/os/unix/linux/debian/vyos/_os.py +1 -1
  38. dissect/target/plugins/os/unix/linux/fortios/_os.py +9 -9
  39. dissect/target/plugins/os/unix/linux/services.py +1 -0
  40. dissect/target/plugins/os/unix/linux/sockets.py +2 -2
  41. dissect/target/plugins/os/unix/log/messages.py +53 -8
  42. dissect/target/plugins/os/windows/_os.py +10 -1
  43. dissect/target/plugins/os/windows/catroot.py +178 -63
  44. dissect/target/plugins/os/windows/credhist.py +210 -0
  45. dissect/target/plugins/os/windows/dpapi/crypto.py +12 -1
  46. dissect/target/plugins/os/windows/dpapi/dpapi.py +62 -7
  47. dissect/target/plugins/os/windows/dpapi/master_key.py +22 -2
  48. dissect/target/plugins/os/windows/regf/runkeys.py +6 -4
  49. dissect/target/plugins/os/windows/sam.py +10 -1
  50. dissect/target/target.py +1 -1
  51. dissect/target/tools/dump/run.py +23 -28
  52. dissect/target/tools/dump/state.py +11 -8
  53. dissect/target/tools/dump/utils.py +5 -4
  54. dissect/target/tools/query.py +3 -15
  55. dissect/target/tools/shell.py +48 -8
  56. dissect/target/tools/utils.py +23 -0
  57. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/METADATA +7 -3
  58. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/RECORD +63 -56
  59. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/WHEEL +1 -1
  60. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/COPYRIGHT +0 -0
  61. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/LICENSE +0 -0
  62. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/entry_points.txt +0 -0
  63. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,309 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import ssl
5
+ import time
6
+ import urllib
7
+ from dataclasses import dataclass
8
+ from functools import lru_cache
9
+ from pathlib import Path
10
+ from struct import pack, unpack_from
11
+ from typing import Any, Callable, Iterator, Optional, Union
12
+
13
+ import paho.mqtt.client as mqtt
14
+ from dissect.util.stream import AlignedStream
15
+
16
+ from dissect.target.containers.raw import RawContainer
17
+ from dissect.target.exceptions import LoaderError
18
+ from dissect.target.loader import Loader
19
+ from dissect.target.plugin import arg
20
+ from dissect.target.target import Target
21
+
22
+ log = logging.getLogger(__name__)
23
+
24
+ DISK_INDEX_OFFSET = 9
25
+
26
+
27
+ def suppress(func: Callable) -> Callable:
28
+ def suppressed(*args, **kwargs):
29
+ try:
30
+ return func(*args, **kwargs)
31
+ except Exception:
32
+ return
33
+
34
+ return suppressed
35
+
36
+
37
+ @dataclass
38
+ class InfoMessage:
39
+ disks: list[DiskMessage]
40
+
41
+
42
+ @dataclass
43
+ class DiskMessage:
44
+ index: int = 0
45
+ sector_size: int = 0
46
+ total_size: int = 0
47
+
48
+
49
+ @dataclass
50
+ class SeekMessage:
51
+ data: bytes = b""
52
+
53
+
54
+ class MQTTStream(AlignedStream):
55
+ def __init__(self, stream: MQTTConnection, disk_id: int, size: Optional[int] = None):
56
+ self.stream = stream
57
+ self.disk_id = disk_id
58
+ super().__init__(size)
59
+
60
+ def _read(self, offset: int, length: int, optimization_strategy: int = 0) -> bytes:
61
+ data = self.stream.read(self.disk_id, offset, length, optimization_strategy)
62
+ return data
63
+
64
+
65
+ class MQTTConnection:
66
+ broker = None
67
+ host = None
68
+ prev = -1
69
+ factor = 1
70
+ prefetch_factor_inc = 10
71
+
72
+ def __init__(self, broker: Broker, host: str):
73
+ self.broker = broker
74
+ self.host = str(host)
75
+ self.info = lru_cache(128)(self.info)
76
+ self.read = lru_cache(128)(self.read)
77
+
78
+ def topo(self, peers: int) -> list[str]:
79
+ self.broker.topology(self.host)
80
+
81
+ while len(self.broker.peers(self.host)) < peers:
82
+ self.broker.topology(self.host)
83
+ time.sleep(1)
84
+ return self.broker.peers(self.host)
85
+
86
+ def info(self) -> list[MQTTStream]:
87
+ disks = []
88
+ self.broker.info(self.host)
89
+
90
+ message = None
91
+ while message is None:
92
+ message = self.broker.disk(self.host)
93
+
94
+ for idx, disk in enumerate(message.disks):
95
+ disks.append(MQTTStream(self, idx, disk.total_size))
96
+
97
+ return disks
98
+
99
+ def read(self, disk_id: int, offset: int, length: int, optimization_strategy: int) -> bytes:
100
+ message = None
101
+
102
+ message = self.broker.read(self.host, disk_id, offset, length)
103
+ if message:
104
+ return message.data
105
+
106
+ if self.prev == offset - (length * self.factor):
107
+ if self.factor < 500:
108
+ self.factor += self.prefetch_factor_inc
109
+ else:
110
+ self.factor = 1
111
+
112
+ self.prev = offset
113
+ flength = length * self.factor
114
+ self.broker.factor = self.factor
115
+ self.broker.seek(self.host, disk_id, offset, flength, optimization_strategy)
116
+ attempts = 0
117
+ while True:
118
+ if message := self.broker.read(self.host, disk_id, offset, length):
119
+ # don't waste time with sleep if we have a response
120
+ break
121
+
122
+ attempts += 1
123
+ time.sleep(0.1)
124
+ if attempts > 300:
125
+ # message might have not reached agent, resend...
126
+ self.broker.seek(self.host, disk_id, offset, flength, optimization_strategy)
127
+ attempts = 0
128
+
129
+ return message.data
130
+
131
+
132
+ class Broker:
133
+ broker_host = None
134
+ broker_port = None
135
+ private_key_file = None
136
+ certificate_file = None
137
+ cacert_file = None
138
+ mqtt_client = None
139
+ connected = False
140
+ case = None
141
+
142
+ diskinfo = {}
143
+ index = {}
144
+ topo = {}
145
+ factor = 1
146
+
147
+ def __init__(self, broker: Broker, port: str, key: str, crt: str, ca: str, case: str, **kwargs):
148
+ self.broker_host = broker
149
+ self.broker_port = int(port)
150
+ self.private_key_file = key
151
+ self.certificate_file = crt
152
+ self.cacert_file = ca
153
+ self.case = case
154
+ self.command = kwargs.get("command", None)
155
+
156
+ def clear_cache(self) -> None:
157
+ self.index = {}
158
+
159
+ @suppress
160
+ def read(self, host: str, disk_id: int, seek_address: int, read_length: int) -> SeekMessage:
161
+ key = f"{host}-{disk_id}-{seek_address}-{read_length}"
162
+ return self.index.get(key)
163
+
164
+ @suppress
165
+ def disk(self, host: str) -> DiskMessage:
166
+ return self.diskinfo[host]
167
+
168
+ def peers(self, host: str) -> list[str]:
169
+ return self.topo[host]
170
+
171
+ def _on_disk(self, hostname: str, payload: bytes) -> None:
172
+ (num_of_disks,) = unpack_from("<B", payload, offset=0)
173
+ disks = []
174
+ for disk_index in range(num_of_disks):
175
+ (
176
+ sector_size,
177
+ total_size,
178
+ ) = unpack_from("<IQ", payload, offset=1 + (disk_index * DISK_INDEX_OFFSET))
179
+ disks.append(DiskMessage(index=disk_index, sector_size=sector_size, total_size=total_size))
180
+
181
+ self.diskinfo[hostname] = InfoMessage(disks=disks)
182
+
183
+ def _on_read(self, hostname: str, tokens: list[str], payload: bytes) -> None:
184
+ disk_id = tokens[3]
185
+ seek_address = int(tokens[4], 16)
186
+ read_length = int(tokens[5], 16)
187
+
188
+ for i in range(self.factor):
189
+ sublength = int(read_length / self.factor)
190
+ start = i * sublength
191
+ key = f"{hostname}-{disk_id}-{seek_address+start}-{sublength}"
192
+ if key in self.index:
193
+ continue
194
+
195
+ self.index[key] = SeekMessage(data=payload[start : start + sublength])
196
+
197
+ def _on_id(self, hostname: str, payload: bytes) -> None:
198
+ key = hostname
199
+ host = payload.decode("utf-8")
200
+ if host not in self.topo[key]:
201
+ self.topo[key].append(payload.decode("utf-8"))
202
+ self.mqtt_client.subscribe(f"{self.case}/{host}/DISKS")
203
+ self.mqtt_client.subscribe(f"{self.case}/{host}/READ/#")
204
+ if self.command is not None:
205
+ self.mqtt_client.publish(f"{self.case}/{host}/COMM", self.command.encode("utf-8"))
206
+ time.sleep(1)
207
+
208
+ def _on_log(self, client: mqtt.Client, userdata: Any, log_level: int, message: str) -> None:
209
+ log.debug(message)
210
+
211
+ def _on_connect(self, client: mqtt.Client, userdata: Any, flags: dict, rc: int) -> None:
212
+ self.connected = True
213
+
214
+ def _on_message(self, client: mqtt.Client, userdata: Any, msg: mqtt.client.MQTTMessage) -> None:
215
+ tokens = msg.topic.split("/")
216
+ casename, hostname, response, *_ = tokens
217
+ if casename != self.case:
218
+ return
219
+
220
+ if response == "DISKS":
221
+ self._on_disk(hostname, msg.payload)
222
+ elif response == "READ":
223
+ self._on_read(hostname, tokens, msg.payload)
224
+ elif response == "ID":
225
+ self._on_id(hostname, msg.payload)
226
+
227
+ def seek(self, host: str, disk_id: int, offset: int, flength: int, optimization_strategy: int) -> None:
228
+ length = int(flength / self.factor)
229
+ key = f"{host}-{disk_id}-{offset}-{length}"
230
+ if key in self.index:
231
+ return
232
+
233
+ self.mqtt_client.publish(
234
+ f"{self.case}/{host}/SEEK/{disk_id}/{hex(offset)}/{hex(flength)}", pack("<I", optimization_strategy)
235
+ )
236
+
237
+ def info(self, host: str) -> None:
238
+ self.mqtt_client.publish(f"{self.case}/{host}/INFO")
239
+
240
+ def topology(self, host: str) -> None:
241
+ self.topo[host] = []
242
+ self.mqtt_client.subscribe(f"{self.case}/{host}/ID")
243
+ time.sleep(1) # need some time to avoid race condition, i.e. MQTT might react too fast
244
+ self.mqtt_client.publish(f"{self.case}/{host}/TOPO")
245
+
246
+ def connect(self) -> None:
247
+ self.mqtt_client = mqtt.Client(
248
+ client_id="", clean_session=True, userdata=None, protocol=mqtt.MQTTv311, transport="tcp"
249
+ )
250
+ self.mqtt_client.tls_set(
251
+ ca_certs=self.cacert_file,
252
+ certfile=self.certificate_file,
253
+ keyfile=self.private_key_file,
254
+ cert_reqs=ssl.CERT_REQUIRED,
255
+ tls_version=ssl.PROTOCOL_TLS,
256
+ ciphers=None,
257
+ )
258
+ self.mqtt_client.tls_insecure_set(True) # merely having the correct cert is ok
259
+ self.mqtt_client.on_connect = self._on_connect
260
+ self.mqtt_client.on_message = self._on_message
261
+ if log.getEffectiveLevel() == logging.DEBUG:
262
+ self.mqtt_client.on_log = self._on_log
263
+ self.mqtt_client.connect(self.broker_host, port=self.broker_port, keepalive=60)
264
+ self.mqtt_client.loop_start()
265
+
266
+
267
+ @arg("--mqtt-peers", type=int, dest="peers", help="minimum number of peers to await for first alias")
268
+ @arg("--mqtt-case", dest="case", help="case name (broker will determine if you are allowed to access this data)")
269
+ @arg("--mqtt-port", type=int, dest="port", help="broker connection port")
270
+ @arg("--mqtt-broker", dest="broker", help="broker ip-address")
271
+ @arg("--mqtt-key", dest="key", help="private key file")
272
+ @arg("--mqtt-crt", dest="crt", help="client certificate file")
273
+ @arg("--mqtt-ca", dest="ca", help="certificate authority file")
274
+ @arg("--mqtt-command", dest="command", help="direct command to client(s)")
275
+ class MQTTLoader(Loader):
276
+ """Load remote targets through a broker."""
277
+
278
+ connection = None
279
+ broker = None
280
+ peers = []
281
+
282
+ def __init__(self, path: Union[Path, str], **kwargs):
283
+ super().__init__(path)
284
+ cls = MQTTLoader
285
+ self.broker = cls.broker
286
+ self.connection = MQTTConnection(self.broker, path)
287
+
288
+ @staticmethod
289
+ def detect(path: Path) -> bool:
290
+ return False
291
+
292
+ def find_all(path: Path, **kwargs) -> Iterator[str]:
293
+ cls = MQTTLoader
294
+ num_peers = 1
295
+ if cls.broker is None:
296
+ if (uri := kwargs.get("parsed_path")) is None:
297
+ raise LoaderError("No URI connection details have been passed.")
298
+ options = dict(urllib.parse.parse_qsl(uri.query, keep_blank_values=True))
299
+ cls.broker = Broker(**options)
300
+ cls.broker.connect()
301
+ num_peers = int(options.get("peers", 1))
302
+
303
+ cls.connection = MQTTConnection(cls.broker, path)
304
+ cls.peers = cls.connection.topo(num_peers)
305
+ yield from cls.peers
306
+
307
+ def map(self, target: Target) -> None:
308
+ for disk in self.connection.info():
309
+ target.disks.add(RawContainer(disk))
@@ -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))
@@ -1,27 +1,30 @@
1
+ from pathlib import Path
2
+ from typing import TYPE_CHECKING
3
+
1
4
  try:
2
- import yaml
5
+ from ruamel.yaml import YAML
3
6
  except ImportError:
4
- raise ImportError("Missing PyYAML dependency")
7
+ raise ImportError("Missing ruamel.yaml dependency")
5
8
 
6
9
  from dissect.target import container
7
10
  from dissect.target.loader import Loader
8
11
 
12
+ if TYPE_CHECKING:
13
+ from dissect.target.target import Target
14
+
9
15
 
10
16
  class TargetLoader(Loader):
11
17
  """Load target files."""
12
18
 
13
- def __init__(self, path, **kwargs):
19
+ def __init__(self, path: Path, **kwargs):
14
20
  super().__init__(path)
15
21
  self.base_dir = path.parent
16
- self.definition = yaml.safe_load(path.open("rb"))
22
+ self.definition = YAML(typ="safe").load(path.open("rb"))
17
23
 
18
24
  @staticmethod
19
- def detect(path):
25
+ def detect(path: Path) -> bool:
20
26
  return path.suffix.lower() == ".target"
21
27
 
22
- def map(self, target):
28
+ def map(self, target: Target) -> None:
23
29
  for disk in self.definition["disks"]:
24
30
  target.disks.add(container.open(disk))
25
-
26
- def open(self, path):
27
- return self.base_dir.joinpath(path).open("rb")
@@ -14,8 +14,8 @@ class VBLoader(Loader):
14
14
  return (mft_exists or c_drive_exists) and config_exists
15
15
 
16
16
  def map(self, target):
17
- ntfs_overlay = target.fs.add_layer()
18
- remap_overlay = target.fs.add_layer()
17
+ remap_overlay = target.fs.append_layer()
18
+ ntfs_overlay = target.fs.append_layer()
19
19
  dfs = DirectoryFilesystem(self.path, case_sensitive=False)
20
20
  target.filesystems.add(dfs)
21
21
 
@@ -61,6 +61,10 @@ def extract_drive_letter(name: str) -> Optional[str]:
61
61
  if len(name) == 14 and name.startswith("%5C%5C.%5C") and name.endswith("%3A"):
62
62
  return name[10].lower()
63
63
 
64
+ # X: in URL encoding
65
+ if len(name) == 4 and name.endswith("%3A"):
66
+ return name[0].lower()
67
+
64
68
 
65
69
  class VelociraptorLoader(DirLoader):
66
70
  """Load Rapid7 Velociraptor forensic image files.
@@ -71,10 +75,7 @@ class VelociraptorLoader(DirLoader):
71
75
  {"Generic.Collectors.File":{"Root":"/","collectionSpec":"Glob\\netc/**\\nvar/log/**"}}
72
76
 
73
77
  Generic.Collectors.File (Windows) and Windows.KapeFiles.Targets (Windows) uses the accessors mft, ntfs, lazy_ntfs,
74
- ntfs_vss and auto. The loader only supports a collection where a single accessor is used, which can be forced by
75
- using the following configuration::
76
-
77
- {"Windows.KapeFiles.Targets":{"VSSAnalysisAge":"1000","_SANS_Triage":"Y"}}
78
+ ntfs_vss and auto. The loader supports a collection where multiple accessors were used.
78
79
 
79
80
  References:
80
81
  - https://www.rapid7.com/products/velociraptor/
dissect/target/plugin.py CHANGED
@@ -82,7 +82,7 @@ def export(*args, **kwargs) -> Callable:
82
82
  - default: Single return value
83
83
  - record: Yields records. Implicit when record argument is given.
84
84
  - yield: Yields printable values.
85
- - none: No return value.
85
+ - none: No return value. Plugin is responsible for output formatting and should return ``None``.
86
86
 
87
87
  The ``export`` decorator adds some additional private attributes to an exported method or property:
88
88
 
@@ -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")