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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) 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/os/unix/bsd/__init__.py +0 -0
  35. dissect/target/plugins/os/unix/esxi/_os.py +2 -2
  36. dissect/target/plugins/os/unix/linux/debian/vyos/_os.py +1 -1
  37. dissect/target/plugins/os/unix/linux/fortios/_os.py +9 -9
  38. dissect/target/plugins/os/unix/linux/services.py +1 -0
  39. dissect/target/plugins/os/unix/linux/sockets.py +2 -2
  40. dissect/target/plugins/os/unix/log/messages.py +53 -8
  41. dissect/target/plugins/os/windows/_os.py +10 -1
  42. dissect/target/plugins/os/windows/catroot.py +178 -63
  43. dissect/target/plugins/os/windows/credhist.py +210 -0
  44. dissect/target/plugins/os/windows/dpapi/crypto.py +12 -1
  45. dissect/target/plugins/os/windows/dpapi/dpapi.py +62 -7
  46. dissect/target/plugins/os/windows/dpapi/master_key.py +22 -2
  47. dissect/target/plugins/os/windows/regf/runkeys.py +6 -4
  48. dissect/target/plugins/os/windows/sam.py +10 -1
  49. dissect/target/target.py +1 -1
  50. dissect/target/tools/dump/run.py +23 -28
  51. dissect/target/tools/dump/state.py +11 -8
  52. dissect/target/tools/dump/utils.py +5 -4
  53. dissect/target/tools/query.py +3 -15
  54. dissect/target/tools/shell.py +48 -8
  55. dissect/target/tools/utils.py +23 -0
  56. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/METADATA +7 -3
  57. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/RECORD +62 -55
  58. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/WHEEL +1 -1
  59. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/COPYRIGHT +0 -0
  60. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/LICENSE +0 -0
  61. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/entry_points.txt +0 -0
  62. {dissect.target-3.16.dev45.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")