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.
- dissect/target/container.py +1 -0
- dissect/target/containers/fortifw.py +190 -0
- dissect/target/filesystem.py +192 -67
- 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/helpers/configutil.py +30 -7
- dissect/target/helpers/network_managers.py +101 -73
- dissect/target/helpers/record_modifier.py +4 -1
- dissect/target/loader.py +3 -1
- dissect/target/loaders/dir.py +23 -5
- dissect/target/loaders/itunes.py +3 -3
- dissect/target/loaders/mqtt.py +309 -0
- dissect/target/loaders/overlay.py +31 -0
- dissect/target/loaders/target.py +12 -9
- dissect/target/loaders/vb.py +2 -2
- dissect/target/loaders/velociraptor.py +5 -4
- dissect/target/plugin.py +1 -1
- 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 +512 -19
- dissect/target/plugins/apps/browser/iexplore.py +2 -2
- dissect/target/plugins/apps/container/docker.py +24 -4
- dissect/target/plugins/apps/ssh/openssh.py +4 -0
- dissect/target/plugins/apps/ssh/putty.py +45 -14
- dissect/target/plugins/apps/ssh/ssh.py +40 -0
- dissect/target/plugins/apps/vpn/openvpn.py +115 -93
- dissect/target/plugins/child/docker.py +24 -0
- dissect/target/plugins/filesystem/ntfs/mft.py +1 -1
- dissect/target/plugins/filesystem/walkfs.py +2 -2
- dissect/target/plugins/os/unix/bsd/__init__.py +0 -0
- dissect/target/plugins/os/unix/esxi/_os.py +2 -2
- dissect/target/plugins/os/unix/linux/debian/vyos/_os.py +1 -1
- dissect/target/plugins/os/unix/linux/fortios/_os.py +9 -9
- dissect/target/plugins/os/unix/linux/services.py +1 -0
- dissect/target/plugins/os/unix/linux/sockets.py +2 -2
- dissect/target/plugins/os/unix/log/messages.py +53 -8
- dissect/target/plugins/os/windows/_os.py +10 -1
- dissect/target/plugins/os/windows/catroot.py +178 -63
- dissect/target/plugins/os/windows/credhist.py +210 -0
- 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/regf/runkeys.py +6 -4
- dissect/target/plugins/os/windows/sam.py +10 -1
- dissect/target/target.py +1 -1
- dissect/target/tools/dump/run.py +23 -28
- dissect/target/tools/dump/state.py +11 -8
- dissect/target/tools/dump/utils.py +5 -4
- dissect/target/tools/query.py +3 -15
- dissect/target/tools/shell.py +48 -8
- dissect/target/tools/utils.py +23 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/METADATA +7 -3
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/RECORD +62 -55
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/WHEEL +1 -1
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/LICENSE +0 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/entry_points.txt +0 -0
- {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))
|
dissect/target/loaders/target.py
CHANGED
@@ -1,27 +1,30 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import TYPE_CHECKING
|
3
|
+
|
1
4
|
try:
|
2
|
-
import
|
5
|
+
from ruamel.yaml import YAML
|
3
6
|
except ImportError:
|
4
|
-
raise ImportError("Missing
|
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 =
|
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")
|
dissect/target/loaders/vb.py
CHANGED
@@ -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
|
-
|
18
|
-
|
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
|
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")
|