dissect.target 3.16.dev45__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.
- 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")
|