dissect.target 3.18.dev16__py3-none-any.whl → 3.19__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dissect/target/filesystem.py +44 -25
- dissect/target/filesystems/config.py +32 -21
- dissect/target/filesystems/extfs.py +4 -0
- dissect/target/filesystems/itunes.py +1 -1
- dissect/target/filesystems/tar.py +1 -1
- dissect/target/filesystems/zip.py +81 -46
- dissect/target/helpers/config.py +22 -7
- dissect/target/helpers/configutil.py +69 -5
- dissect/target/helpers/cyber.py +4 -2
- dissect/target/helpers/fsutil.py +32 -4
- dissect/target/helpers/loaderutil.py +26 -7
- dissect/target/helpers/network_managers.py +22 -7
- dissect/target/helpers/record.py +37 -0
- dissect/target/helpers/record_modifier.py +23 -4
- dissect/target/helpers/shell_application_ids.py +732 -0
- dissect/target/helpers/utils.py +11 -0
- dissect/target/loader.py +1 -0
- dissect/target/loaders/ab.py +285 -0
- dissect/target/loaders/libvirt.py +40 -0
- dissect/target/loaders/mqtt.py +14 -1
- dissect/target/loaders/tar.py +8 -4
- dissect/target/loaders/utm.py +3 -0
- dissect/target/loaders/velociraptor.py +6 -6
- dissect/target/plugin.py +60 -3
- dissect/target/plugins/apps/browser/chrome.py +1 -0
- dissect/target/plugins/apps/browser/chromium.py +7 -5
- dissect/target/plugins/apps/browser/edge.py +1 -0
- dissect/target/plugins/apps/browser/firefox.py +82 -36
- dissect/target/plugins/apps/remoteaccess/anydesk.py +70 -50
- dissect/target/plugins/apps/remoteaccess/remoteaccess.py +8 -8
- dissect/target/plugins/apps/remoteaccess/teamviewer.py +46 -31
- dissect/target/plugins/apps/ssh/openssh.py +1 -1
- dissect/target/plugins/apps/ssh/ssh.py +177 -0
- dissect/target/plugins/apps/texteditor/__init__.py +0 -0
- dissect/target/plugins/apps/texteditor/texteditor.py +13 -0
- dissect/target/plugins/apps/texteditor/windowsnotepad.py +340 -0
- dissect/target/plugins/child/qemu.py +21 -0
- dissect/target/plugins/filesystem/ntfs/mft.py +132 -45
- dissect/target/plugins/filesystem/unix/capability.py +102 -87
- dissect/target/plugins/filesystem/walkfs.py +32 -21
- dissect/target/plugins/filesystem/yara.py +144 -23
- dissect/target/plugins/general/network.py +82 -0
- dissect/target/plugins/general/users.py +14 -10
- dissect/target/plugins/os/unix/_os.py +19 -5
- dissect/target/plugins/os/unix/bsd/freebsd/_os.py +3 -5
- dissect/target/plugins/os/unix/esxi/_os.py +29 -23
- dissect/target/plugins/os/unix/etc/etc.py +5 -8
- dissect/target/plugins/os/unix/history.py +3 -7
- dissect/target/plugins/os/unix/linux/_os.py +15 -14
- dissect/target/plugins/os/unix/linux/android/_os.py +15 -24
- dissect/target/plugins/os/unix/linux/redhat/_os.py +1 -1
- dissect/target/plugins/os/unix/locale.py +17 -6
- dissect/target/plugins/os/unix/shadow.py +47 -31
- dissect/target/plugins/os/windows/_os.py +4 -4
- dissect/target/plugins/os/windows/adpolicy.py +4 -1
- dissect/target/plugins/os/windows/catroot.py +1 -11
- dissect/target/plugins/os/windows/credential/__init__.py +0 -0
- dissect/target/plugins/os/windows/credential/lsa.py +174 -0
- dissect/target/plugins/os/windows/{sam.py → credential/sam.py} +5 -2
- dissect/target/plugins/os/windows/defender.py +6 -3
- dissect/target/plugins/os/windows/dpapi/blob.py +3 -0
- dissect/target/plugins/os/windows/dpapi/crypto.py +61 -23
- dissect/target/plugins/os/windows/dpapi/dpapi.py +127 -133
- dissect/target/plugins/os/windows/dpapi/keyprovider/__init__.py +0 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +21 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +17 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +20 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/keyprovider.py +8 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +38 -0
- dissect/target/plugins/os/windows/dpapi/master_key.py +3 -0
- dissect/target/plugins/os/windows/jumplist.py +292 -0
- dissect/target/plugins/os/windows/lnk.py +96 -93
- dissect/target/plugins/os/windows/regf/shimcache.py +2 -2
- dissect/target/plugins/os/windows/regf/usb.py +179 -114
- dissect/target/plugins/os/windows/task_helpers/tasks_xml.py +1 -1
- dissect/target/plugins/os/windows/wua_history.py +1073 -0
- dissect/target/target.py +4 -3
- dissect/target/tools/fs.py +53 -15
- dissect/target/tools/fsutils.py +243 -0
- dissect/target/tools/info.py +11 -4
- dissect/target/tools/query.py +2 -2
- dissect/target/tools/shell.py +505 -333
- dissect/target/tools/utils.py +23 -2
- dissect/target/tools/yara.py +65 -0
- dissect/target/volumes/md.py +2 -2
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/METADATA +11 -7
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/RECORD +93 -74
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/WHEEL +1 -1
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/entry_points.txt +1 -0
- dissect/target/helpers/ssh.py +0 -177
- /dissect/target/plugins/os/windows/{credhist.py → credential/credhist.py} +0 -0
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/LICENSE +0 -0
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/top_level.txt +0 -0
dissect/target/helpers/utils.py
CHANGED
@@ -13,6 +13,17 @@ from dissect.target.helpers import fsutil
|
|
13
13
|
log = logging.getLogger(__name__)
|
14
14
|
|
15
15
|
|
16
|
+
def findall(buf: bytes, needle: bytes) -> Iterator[int]:
|
17
|
+
offset = 0
|
18
|
+
while True:
|
19
|
+
offset = buf.find(needle, offset)
|
20
|
+
if offset == -1:
|
21
|
+
break
|
22
|
+
|
23
|
+
yield offset
|
24
|
+
offset += 1
|
25
|
+
|
26
|
+
|
16
27
|
class StrEnum(str, Enum):
|
17
28
|
"""Sortable and serializible string-based enum"""
|
18
29
|
|
dissect/target/loader.py
CHANGED
@@ -195,6 +195,7 @@ register("vma", "VmaLoader")
|
|
195
195
|
register("kape", "KapeLoader")
|
196
196
|
register("tanium", "TaniumLoader")
|
197
197
|
register("itunes", "ITunesLoader")
|
198
|
+
register("ab", "AndroidBackupLoader")
|
198
199
|
register("target", "TargetLoader")
|
199
200
|
register("log", "LogLoader")
|
200
201
|
# Disabling ResLoader because of DIS-536
|
@@ -0,0 +1,285 @@
|
|
1
|
+
import hashlib
|
2
|
+
import io
|
3
|
+
import posixpath
|
4
|
+
import shutil
|
5
|
+
import struct
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import BinaryIO
|
8
|
+
|
9
|
+
try:
|
10
|
+
from Crypto.Cipher import AES
|
11
|
+
|
12
|
+
HAS_PYCRYPTODOME = True
|
13
|
+
except ImportError:
|
14
|
+
HAS_PYCRYPTODOME = False
|
15
|
+
|
16
|
+
from dissect.util.stream import AlignedStream, RelativeStream, ZlibStream
|
17
|
+
|
18
|
+
from dissect.target.exceptions import LoaderError
|
19
|
+
from dissect.target.filesystem import VirtualFilesystem
|
20
|
+
from dissect.target.filesystems.tar import TarFilesystem
|
21
|
+
from dissect.target.helpers import keychain
|
22
|
+
from dissect.target.loader import Loader
|
23
|
+
from dissect.target.plugins.os.unix.linux.android._os import AndroidPlugin
|
24
|
+
from dissect.target.target import Target
|
25
|
+
|
26
|
+
DIRECTORY_MAPPING = {
|
27
|
+
"a": "/data/app/{id}",
|
28
|
+
"f": "/data/data/{id}/files",
|
29
|
+
"db": "/data/data/{id}/databases",
|
30
|
+
"ef": "/storage/emulated/0/Android/data/{id}",
|
31
|
+
"sp": "/data/data/{id}/shared_preferences",
|
32
|
+
"r": "/data/data/{id}",
|
33
|
+
"obb": "/storage/emulated/0/Android/obb/{id}",
|
34
|
+
}
|
35
|
+
|
36
|
+
|
37
|
+
class AndroidBackupLoader(Loader):
|
38
|
+
"""Load Android backup files.
|
39
|
+
|
40
|
+
References:
|
41
|
+
- http://fileformats.archiveteam.org/wiki/Android_ADB_Backup
|
42
|
+
"""
|
43
|
+
|
44
|
+
def __init__(self, path: Path, **kwargs):
|
45
|
+
super().__init__(path)
|
46
|
+
self.ab = AndroidBackup(path.open("rb"))
|
47
|
+
|
48
|
+
if self.ab.encrypted:
|
49
|
+
for key in keychain.get_keys_for_provider("ab") + keychain.get_keys_without_provider():
|
50
|
+
if key.key_type == keychain.KeyType.PASSPHRASE:
|
51
|
+
try:
|
52
|
+
self.ab.unlock(key.value)
|
53
|
+
break
|
54
|
+
except ValueError:
|
55
|
+
continue
|
56
|
+
else:
|
57
|
+
raise LoaderError(f"Missing password for encrypted Android Backup: {self.path}")
|
58
|
+
|
59
|
+
@staticmethod
|
60
|
+
def detect(path: Path) -> bool:
|
61
|
+
if path.suffix.lower() == ".ab":
|
62
|
+
return True
|
63
|
+
|
64
|
+
if path.is_file():
|
65
|
+
# The file extension can be chosen freely so let's also test for the file magic
|
66
|
+
with path.open("rb") as fh:
|
67
|
+
if fh.read(15) == b"ANDROID BACKUP\n":
|
68
|
+
return True
|
69
|
+
|
70
|
+
return False
|
71
|
+
|
72
|
+
def map(self, target: Target) -> None:
|
73
|
+
if self.ab.compressed or self.ab.encrypted:
|
74
|
+
if self.ab.compressed and not self.ab.encrypted:
|
75
|
+
word = "compressed"
|
76
|
+
elif self.ab.encrypted and not self.ab.compressed:
|
77
|
+
word = "encrypted"
|
78
|
+
else:
|
79
|
+
word = "compressed and encrypted"
|
80
|
+
|
81
|
+
target.log.warning(
|
82
|
+
f"Backup file is {word}, consider unwrapping with "
|
83
|
+
"`python -m dissect.target.loaders.ab <path/to/backup.ab>`"
|
84
|
+
)
|
85
|
+
|
86
|
+
vfs = VirtualFilesystem(case_sensitive=False)
|
87
|
+
|
88
|
+
fs = TarFilesystem(self.ab.open())
|
89
|
+
for app in fs.path("/apps").iterdir():
|
90
|
+
for subdir in app.iterdir():
|
91
|
+
if subdir.name not in DIRECTORY_MAPPING:
|
92
|
+
continue
|
93
|
+
|
94
|
+
path = DIRECTORY_MAPPING[subdir.name].format(id=app.name)
|
95
|
+
|
96
|
+
# TODO: Remove once we move towards "directory entries"
|
97
|
+
entry = subdir.get()
|
98
|
+
entry.name = posixpath.basename(path)
|
99
|
+
|
100
|
+
vfs.map_file_entry(path, entry)
|
101
|
+
|
102
|
+
target.filesystems.add(vfs)
|
103
|
+
target._os_plugin = AndroidPlugin.create(target, vfs)
|
104
|
+
|
105
|
+
|
106
|
+
class AndroidBackup:
|
107
|
+
def __init__(self, fh: BinaryIO):
|
108
|
+
self.fh = fh
|
109
|
+
|
110
|
+
size = fh.seek(0, io.SEEK_END)
|
111
|
+
fh.seek(0)
|
112
|
+
|
113
|
+
# Don't readline() straight away as we may be reading something other than a backup file
|
114
|
+
magic = fh.read(15)
|
115
|
+
if magic != b"ANDROID BACKUP\n":
|
116
|
+
raise ValueError("Not a valid Android Backup file")
|
117
|
+
|
118
|
+
self.version = int(fh.read(2)[:1])
|
119
|
+
self.compressed = bool(int(fh.read(2)[:1]))
|
120
|
+
|
121
|
+
self.encrypted = False
|
122
|
+
self.unlocked = True
|
123
|
+
self.encryption = fh.readline().strip().decode()
|
124
|
+
|
125
|
+
if self.encryption != "none":
|
126
|
+
self.encrypted = True
|
127
|
+
self.unlocked = False
|
128
|
+
self._user_salt = bytes.fromhex(fh.readline().strip().decode())
|
129
|
+
self._ck_salt = bytes.fromhex(fh.readline().strip().decode())
|
130
|
+
self._rounds = int(fh.readline().strip())
|
131
|
+
self._user_iv = bytes.fromhex(fh.readline().strip().decode())
|
132
|
+
self._master_key = bytes.fromhex(fh.readline().strip().decode())
|
133
|
+
|
134
|
+
self._mk = None
|
135
|
+
self._iv = None
|
136
|
+
|
137
|
+
self._data_offset = fh.tell()
|
138
|
+
self.size = size - self._data_offset
|
139
|
+
|
140
|
+
def unlock(self, password: str) -> None:
|
141
|
+
if not self.encrypted:
|
142
|
+
raise ValueError("Android Backup is not encrypted")
|
143
|
+
|
144
|
+
self._mk, self._iv = self._decrypt_mk(password)
|
145
|
+
self.unlocked = True
|
146
|
+
|
147
|
+
def _decrypt_mk(self, password: str) -> tuple[bytes, bytes]:
|
148
|
+
user_key = hashlib.pbkdf2_hmac("sha1", password.encode(), self._user_salt, self._rounds, 32)
|
149
|
+
|
150
|
+
blob = AES.new(user_key, AES.MODE_CBC, iv=self._user_iv).decrypt(self._master_key)
|
151
|
+
blob = blob[: -blob[-1]]
|
152
|
+
|
153
|
+
offset = 0
|
154
|
+
iv_len = blob[offset]
|
155
|
+
offset += 1
|
156
|
+
iv = blob[offset : offset + iv_len]
|
157
|
+
|
158
|
+
offset += iv_len
|
159
|
+
mk_len = blob[offset]
|
160
|
+
offset += 1
|
161
|
+
mk = blob[offset : offset + mk_len]
|
162
|
+
|
163
|
+
offset += mk_len
|
164
|
+
checksum_len = blob[offset]
|
165
|
+
offset += 1
|
166
|
+
checksum = blob[offset : offset + checksum_len]
|
167
|
+
|
168
|
+
ck_mk = _encode_bytes(mk) if self.version >= 2 else mk
|
169
|
+
our_checksum = hashlib.pbkdf2_hmac("sha1", ck_mk, self._ck_salt, self._rounds, 32)
|
170
|
+
if our_checksum != checksum:
|
171
|
+
# Try reverse encoding for good measure
|
172
|
+
ck_mk = mk if self.version >= 2 else _encode_bytes(mk)
|
173
|
+
our_checksum = hashlib.pbkdf2_hmac("sha1", ck_mk, self._ck_salt, self._rounds, 32)
|
174
|
+
|
175
|
+
if our_checksum != checksum:
|
176
|
+
raise ValueError("Invalid password: master key checksum does not match")
|
177
|
+
|
178
|
+
return mk, iv
|
179
|
+
|
180
|
+
def open(self) -> BinaryIO:
|
181
|
+
fh = RelativeStream(self.fh, self._data_offset)
|
182
|
+
|
183
|
+
if self.encrypted:
|
184
|
+
if not self.unlocked:
|
185
|
+
raise ValueError("Missing password for encrypted Android Backup")
|
186
|
+
fh = CipherStream(fh, self._mk, self._iv, self.size)
|
187
|
+
|
188
|
+
if self.compressed:
|
189
|
+
fh = ZlibStream(fh)
|
190
|
+
|
191
|
+
return fh
|
192
|
+
|
193
|
+
|
194
|
+
class CipherStream(AlignedStream):
|
195
|
+
"""Transparently AES-CBC decrypted stream."""
|
196
|
+
|
197
|
+
def __init__(self, fh: BinaryIO, key: bytes, iv: bytes, size: int):
|
198
|
+
self._fh = fh
|
199
|
+
|
200
|
+
self._key = key
|
201
|
+
self._iv = iv
|
202
|
+
self._cipher = None
|
203
|
+
self._cipher_offset = 0
|
204
|
+
self._reset_cipher()
|
205
|
+
|
206
|
+
super().__init__(size)
|
207
|
+
|
208
|
+
def _reset_cipher(self) -> None:
|
209
|
+
self._cipher = AES.new(self._key, AES.MODE_CBC, iv=self._iv)
|
210
|
+
self._cipher_offset = 0
|
211
|
+
|
212
|
+
def _seek_cipher(self, offset: int) -> None:
|
213
|
+
"""CBC is dependent on previous blocks so to seek the cipher, decrypt and discard to the wanted offset."""
|
214
|
+
if offset < self._cipher_offset:
|
215
|
+
self._reset_cipher()
|
216
|
+
self._fh.seek(0)
|
217
|
+
|
218
|
+
while self._cipher_offset < offset:
|
219
|
+
read_size = min(offset - self._cipher_offset, self.align)
|
220
|
+
self._cipher.decrypt(self._fh.read(read_size))
|
221
|
+
self._cipher_offset += read_size
|
222
|
+
|
223
|
+
def _read(self, offset: int, length: int) -> bytes:
|
224
|
+
self._seek_cipher(offset)
|
225
|
+
|
226
|
+
data = self._cipher.decrypt(self._fh.read(length))
|
227
|
+
if offset + length >= self.size:
|
228
|
+
# Remove padding
|
229
|
+
data = data[: -data[-1]]
|
230
|
+
|
231
|
+
self._cipher_offset += len(data)
|
232
|
+
|
233
|
+
return data
|
234
|
+
|
235
|
+
|
236
|
+
def _encode_bytes(buf: bytes) -> bytes:
|
237
|
+
# Emulate byte[] -> char[] -> utf8 byte[] casting
|
238
|
+
return struct.pack(">32h", *struct.unpack(">32b", buf)).decode("utf-16-be").encode("utf-8")
|
239
|
+
|
240
|
+
|
241
|
+
def main() -> None:
|
242
|
+
import argparse
|
243
|
+
|
244
|
+
parser = argparse.ArgumentParser(description="Android Backup file unwrapper")
|
245
|
+
parser.add_argument("path", type=Path, help="source path")
|
246
|
+
parser.add_argument("-p", "--password", help="encryption password")
|
247
|
+
parser.add_argument("-t", "--tar", action="store_true", help="write a tar file instead of a plain Android Backup")
|
248
|
+
parser.add_argument("-o", "--output", type=Path, help="output path")
|
249
|
+
args = parser.parse_args()
|
250
|
+
|
251
|
+
if not args.path.is_file():
|
252
|
+
parser.exit("source path does not exist or is not a file")
|
253
|
+
|
254
|
+
ext = ".tar" if args.tar else ".plain.ab"
|
255
|
+
if args.output is None:
|
256
|
+
output = args.path.with_suffix(ext)
|
257
|
+
elif args.output.is_dir():
|
258
|
+
output = args.output.joinpath(args.path.name).with_suffix(ext)
|
259
|
+
else:
|
260
|
+
output = args.output
|
261
|
+
|
262
|
+
if output.exists():
|
263
|
+
parser.exit(f"output path already exists: {output}")
|
264
|
+
|
265
|
+
print(f"unwrapping {args.path} -> {output}")
|
266
|
+
with args.path.open("rb") as fh:
|
267
|
+
ab = AndroidBackup(fh)
|
268
|
+
|
269
|
+
if ab.encrypted:
|
270
|
+
if not args.password:
|
271
|
+
parser.exit("missing password for encrypted Android Backup")
|
272
|
+
ab.unlock(args.password)
|
273
|
+
|
274
|
+
with ab.open() as fhab, output.open("wb") as fhout:
|
275
|
+
if not args.tar:
|
276
|
+
fhout.write(b"ANDROID BACKUP\n") # header
|
277
|
+
fhout.write(b"5\n") # version
|
278
|
+
fhout.write(b"0\n") # compressed
|
279
|
+
fhout.write(b"none\n") # encryption
|
280
|
+
|
281
|
+
shutil.copyfileobj(fhab, fhout, 1024 * 1024 * 64)
|
282
|
+
|
283
|
+
|
284
|
+
if __name__ == "__main__":
|
285
|
+
main()
|
@@ -0,0 +1,40 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from defusedxml import ElementTree
|
4
|
+
|
5
|
+
from dissect.target import container
|
6
|
+
from dissect.target.helpers import fsutil
|
7
|
+
from dissect.target.loader import Loader
|
8
|
+
from dissect.target.target import Target
|
9
|
+
|
10
|
+
|
11
|
+
class LibvirtLoader(Loader):
|
12
|
+
"""Load libvirt xml configuration files."""
|
13
|
+
|
14
|
+
def __init__(self, path: Path, **kwargs):
|
15
|
+
path = path.resolve()
|
16
|
+
self.base_dir = path.parent
|
17
|
+
super().__init__(path)
|
18
|
+
|
19
|
+
@staticmethod
|
20
|
+
def detect(path: Path) -> bool:
|
21
|
+
if path.suffix.lower() != ".xml":
|
22
|
+
return False
|
23
|
+
|
24
|
+
with path.open("rb") as fh:
|
25
|
+
lines = fh.read(512).split(b"\n")
|
26
|
+
# From what I've seen, these are are always at the start of the file
|
27
|
+
# If its generated using virt-install
|
28
|
+
needles = [b"<domain", b"<name>", b"<uuid>"]
|
29
|
+
return all(any(needle in line for line in lines) for needle in needles)
|
30
|
+
|
31
|
+
def map(self, target: Target) -> None:
|
32
|
+
xml_data = ElementTree.fromstring(self.path.read_text())
|
33
|
+
for disk in xml_data.findall("devices/disk/source"):
|
34
|
+
if not (file := disk.get("file")):
|
35
|
+
continue
|
36
|
+
|
37
|
+
for part in [fsutil.basename(file), file]:
|
38
|
+
if (path := self.base_dir.joinpath(part)).exists():
|
39
|
+
target.disks.add(container.open(path))
|
40
|
+
break
|
dissect/target/loaders/mqtt.py
CHANGED
@@ -10,6 +10,7 @@ import time
|
|
10
10
|
import urllib
|
11
11
|
from dataclasses import dataclass
|
12
12
|
from functools import lru_cache
|
13
|
+
from getpass import getpass
|
13
14
|
from pathlib import Path
|
14
15
|
from struct import pack, unpack_from
|
15
16
|
from threading import Thread
|
@@ -270,19 +271,25 @@ class Broker:
|
|
270
271
|
case = None
|
271
272
|
bytes_received = 0
|
272
273
|
monitor = False
|
274
|
+
username = None
|
275
|
+
password = None
|
273
276
|
|
274
277
|
diskinfo = {}
|
275
278
|
index = {}
|
276
279
|
topo = {}
|
277
280
|
factor = 1
|
278
281
|
|
279
|
-
def __init__(
|
282
|
+
def __init__(
|
283
|
+
self, broker: Broker, port: str, key: str, crt: str, ca: str, case: str, username: str, password: str, **kwargs
|
284
|
+
):
|
280
285
|
self.broker_host = broker
|
281
286
|
self.broker_port = int(port)
|
282
287
|
self.private_key_file = key
|
283
288
|
self.certificate_file = crt
|
284
289
|
self.cacert_file = ca
|
285
290
|
self.case = case
|
291
|
+
self.username = username
|
292
|
+
self.password = password
|
286
293
|
self.command = kwargs.get("command", None)
|
287
294
|
|
288
295
|
def clear_cache(self) -> None:
|
@@ -393,6 +400,7 @@ class Broker:
|
|
393
400
|
tls_version=ssl.PROTOCOL_TLS,
|
394
401
|
ciphers=None,
|
395
402
|
)
|
403
|
+
self.mqtt_client.username_pw_set(self.username, self.password)
|
396
404
|
self.mqtt_client.tls_insecure_set(True) # merely having the correct cert is ok
|
397
405
|
self.mqtt_client.on_connect = self._on_connect
|
398
406
|
self.mqtt_client.on_message = self._on_message
|
@@ -411,6 +419,8 @@ class Broker:
|
|
411
419
|
@arg("--mqtt-ca", dest="ca", help="certificate authority file")
|
412
420
|
@arg("--mqtt-command", dest="command", help="direct command to client(s)")
|
413
421
|
@arg("--mqtt-diag", action="store_true", dest="diag", help="show MQTT diagnostic information")
|
422
|
+
@arg("--mqtt-username", dest="username", help="Username for connection")
|
423
|
+
@arg("--mqtt-password", action="store_true", dest="password", help="Ask for password before connecting")
|
414
424
|
class MQTTLoader(Loader):
|
415
425
|
"""Load remote targets through a broker."""
|
416
426
|
|
@@ -435,7 +445,10 @@ class MQTTLoader(Loader):
|
|
435
445
|
if cls.broker is None:
|
436
446
|
if (uri := kwargs.get("parsed_path")) is None:
|
437
447
|
raise LoaderError("No URI connection details have been passed.")
|
448
|
+
|
438
449
|
options = dict(urllib.parse.parse_qsl(uri.query, keep_blank_values=True))
|
450
|
+
if options.get("password"):
|
451
|
+
options["password"] = getpass()
|
439
452
|
cls.broker = Broker(**options)
|
440
453
|
cls.broker.connect()
|
441
454
|
num_peers = int(options.get("peers", 1))
|
dissect/target/loaders/tar.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import logging
|
2
4
|
import re
|
3
5
|
import tarfile
|
4
6
|
from pathlib import Path
|
5
|
-
from typing import Union
|
6
7
|
|
7
8
|
from dissect.target import filesystem, target
|
8
9
|
from dissect.target.filesystems.tar import (
|
@@ -21,22 +22,25 @@ ANON_FS_RE = re.compile(r"^fs[0-9]+$")
|
|
21
22
|
class TarLoader(Loader):
|
22
23
|
"""Load tar files."""
|
23
24
|
|
24
|
-
def __init__(self, path:
|
25
|
+
def __init__(self, path: Path | str, **kwargs):
|
25
26
|
super().__init__(path)
|
26
27
|
|
28
|
+
if isinstance(path, str):
|
29
|
+
path = Path(path)
|
30
|
+
|
27
31
|
if self.is_compressed(path):
|
28
32
|
log.warning(
|
29
33
|
f"Tar file {path!r} is compressed, which will affect performance. "
|
30
34
|
"Consider uncompressing the archive before passing the tar file to Dissect."
|
31
35
|
)
|
32
36
|
|
33
|
-
self.tar = tarfile.open(path)
|
37
|
+
self.tar = tarfile.open(fileobj=path.open("rb"))
|
34
38
|
|
35
39
|
@staticmethod
|
36
40
|
def detect(path: Path) -> bool:
|
37
41
|
return path.name.lower().endswith((".tar", ".tar.gz", ".tgz"))
|
38
42
|
|
39
|
-
def is_compressed(self, path:
|
43
|
+
def is_compressed(self, path: Path | str) -> bool:
|
40
44
|
return str(path).lower().endswith((".tar.gz", ".tgz"))
|
41
45
|
|
42
46
|
def map(self, target: target.Target) -> None:
|
dissect/target/loaders/utm.py
CHANGED
@@ -21,6 +21,9 @@ class UtmLoader(Loader):
|
|
21
21
|
def map(self, target: Target) -> None:
|
22
22
|
data_dir = self.path.joinpath("Data")
|
23
23
|
for drive in self.config.get("Drive", []):
|
24
|
+
if drive.get("ImageType") != "Disk":
|
25
|
+
continue
|
26
|
+
|
24
27
|
path = data_dir.joinpath(drive["ImageName"])
|
25
28
|
try:
|
26
29
|
target.disks.add(container.open(path))
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import logging
|
4
4
|
import zipfile
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import TYPE_CHECKING
|
6
|
+
from typing import TYPE_CHECKING
|
7
7
|
|
8
8
|
from dissect.target.loaders.dir import DirLoader, find_dirs, map_dirs
|
9
9
|
from dissect.target.plugin import OperatingSystem
|
@@ -18,7 +18,7 @@ UNIX_ACCESSORS = ["file", "auto"]
|
|
18
18
|
WINDOWS_ACCESSORS = ["mft", "ntfs", "lazy_ntfs", "ntfs_vss", "auto"]
|
19
19
|
|
20
20
|
|
21
|
-
def find_fs_directories(path: Path) -> tuple[
|
21
|
+
def find_fs_directories(path: Path) -> tuple[OperatingSystem | None, list[Path] | None]:
|
22
22
|
fs_root = path.joinpath(FILESYSTEMS_ROOT)
|
23
23
|
|
24
24
|
# Unix
|
@@ -56,7 +56,7 @@ def find_fs_directories(path: Path) -> tuple[Optional[OperatingSystem], Optional
|
|
56
56
|
return None, None
|
57
57
|
|
58
58
|
|
59
|
-
def extract_drive_letter(name: str) ->
|
59
|
+
def extract_drive_letter(name: str) -> str | None:
|
60
60
|
# \\.\X: in URL encoding
|
61
61
|
if len(name) == 14 and name.startswith("%5C%5C.%5C") and name.endswith("%3A"):
|
62
62
|
return name[10].lower()
|
@@ -91,7 +91,7 @@ class VelociraptorLoader(DirLoader):
|
|
91
91
|
f"Velociraptor target {path!r} is compressed, which will slightly affect performance. "
|
92
92
|
"Consider uncompressing the archive and passing the uncompressed folder to Dissect."
|
93
93
|
)
|
94
|
-
self.root = zipfile.Path(path)
|
94
|
+
self.root = zipfile.Path(path.open("rb"))
|
95
95
|
else:
|
96
96
|
self.root = path
|
97
97
|
|
@@ -105,8 +105,8 @@ class VelociraptorLoader(DirLoader):
|
|
105
105
|
# results/
|
106
106
|
# uploads.json
|
107
107
|
# [...] other files related to the collection
|
108
|
-
if path.suffix == ".zip": # novermin
|
109
|
-
path = zipfile.Path(path)
|
108
|
+
if path.exists() and path.suffix == ".zip": # novermin
|
109
|
+
path = zipfile.Path(path.open("rb"))
|
110
110
|
|
111
111
|
if path.joinpath(FILESYSTEMS_ROOT).exists() and path.joinpath("uploads.json").exists():
|
112
112
|
_, dirs = find_fs_directories(path)
|
dissect/target/plugin.py
CHANGED
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
See dissect/target/plugins/general/example.py for an example plugin.
|
4
4
|
"""
|
5
|
+
|
5
6
|
from __future__ import annotations
|
6
7
|
|
7
8
|
import fnmatch
|
9
|
+
import functools
|
8
10
|
import importlib
|
9
11
|
import importlib.util
|
10
12
|
import inspect
|
@@ -140,7 +142,7 @@ def get_nonprivate_attributes(cls: Type[Plugin]) -> list[Any]:
|
|
140
142
|
|
141
143
|
def get_nonprivate_methods(cls: Type[Plugin]) -> list[Callable]:
|
142
144
|
"""Retrieve all public methods of a :class:`Plugin`."""
|
143
|
-
return [attr for attr in get_nonprivate_attributes(cls) if not isinstance(attr, property)]
|
145
|
+
return [attr for attr in get_nonprivate_attributes(cls) if not isinstance(attr, property) and callable(attr)]
|
144
146
|
|
145
147
|
|
146
148
|
def get_descriptors_on_nonprivate_methods(cls: Type[Plugin]) -> list[RecordDescriptor]:
|
@@ -196,6 +198,8 @@ class Plugin:
|
|
196
198
|
The :func:`internal` decorator and :class:`InternalPlugin` set the ``__internal__`` attribute.
|
197
199
|
Finally. :func:`args` decorator sets the ``__args__`` attribute.
|
198
200
|
|
201
|
+
The :func:`alias` decorator populates the ``__aliases__`` private attribute of :class:`Plugin` methods.
|
202
|
+
|
199
203
|
Args:
|
200
204
|
target: The :class:`~dissect.target.target.Target` object to load the plugin for.
|
201
205
|
"""
|
@@ -448,6 +452,11 @@ def register(plugincls: Type[Plugin]) -> None:
|
|
448
452
|
exports = []
|
449
453
|
functions = []
|
450
454
|
|
455
|
+
# First pass to resolve aliases
|
456
|
+
for attr in get_nonprivate_attributes(plugincls):
|
457
|
+
for alias in getattr(attr, "__aliases__", []):
|
458
|
+
clone_alias(plugincls, attr, alias)
|
459
|
+
|
451
460
|
for attr in get_nonprivate_attributes(plugincls):
|
452
461
|
if isinstance(attr, property):
|
453
462
|
attr = attr.fget
|
@@ -542,6 +551,47 @@ def arg(*args, **kwargs) -> Callable:
|
|
542
551
|
return decorator
|
543
552
|
|
544
553
|
|
554
|
+
def alias(*args, **kwargs: dict[str, Any]) -> Callable:
|
555
|
+
"""Decorator to be used on :class:`Plugin` functions to register an alias of that function."""
|
556
|
+
|
557
|
+
if not kwargs.get("name") and not args:
|
558
|
+
raise ValueError("Missing argument 'name'")
|
559
|
+
|
560
|
+
def decorator(obj: Callable) -> Callable:
|
561
|
+
if not hasattr(obj, "__aliases__"):
|
562
|
+
obj.__aliases__ = []
|
563
|
+
|
564
|
+
if name := (kwargs.get("name") or args[0]):
|
565
|
+
obj.__aliases__.append(name)
|
566
|
+
|
567
|
+
return obj
|
568
|
+
|
569
|
+
return decorator
|
570
|
+
|
571
|
+
|
572
|
+
def clone_alias(cls: type, attr: Callable, alias: str) -> None:
|
573
|
+
"""Clone the given attribute to an alias in the provided class."""
|
574
|
+
|
575
|
+
# Clone the function object
|
576
|
+
clone = type(attr)(attr.__code__, attr.__globals__, alias, attr.__defaults__, attr.__closure__)
|
577
|
+
clone.__kwdefaults__ = attr.__kwdefaults__
|
578
|
+
|
579
|
+
# Copy some attributes
|
580
|
+
functools.update_wrapper(clone, attr)
|
581
|
+
if wrapped := getattr(attr, "__wrapped__", None):
|
582
|
+
# update_wrapper sets a new wrapper, we want the original
|
583
|
+
clone.__wrapped__ = wrapped
|
584
|
+
|
585
|
+
# Update module path so we can fool inspect.getmodule with subclassed Plugin classes
|
586
|
+
clone.__module__ = cls.__module__
|
587
|
+
|
588
|
+
# Update the names
|
589
|
+
clone.__name__ = alias
|
590
|
+
clone.__qualname__ = f"{cls.__name__}.{alias}"
|
591
|
+
|
592
|
+
setattr(cls, alias, clone)
|
593
|
+
|
594
|
+
|
545
595
|
def plugins(
|
546
596
|
osfilter: Optional[type[OSPlugin]] = None,
|
547
597
|
special_keys: set[str] = set(),
|
@@ -970,7 +1020,7 @@ class NamespacePlugin(Plugin):
|
|
970
1020
|
continue
|
971
1021
|
|
972
1022
|
# The method needs to output records
|
973
|
-
if getattr(subplugin_func, "__output__", None)
|
1023
|
+
if getattr(subplugin_func, "__output__", None) not in ["record", "yield"]:
|
974
1024
|
continue
|
975
1025
|
|
976
1026
|
# The method may not be part of a parent class.
|
@@ -1056,6 +1106,9 @@ class NamespacePlugin(Plugin):
|
|
1056
1106
|
cls.__init_subclass_namespace__(cls, **kwargs)
|
1057
1107
|
|
1058
1108
|
|
1109
|
+
__COMMON_PLUGIN_METHOD_NAMES__ = {attr.__name__ for attr in get_nonprivate_methods(Plugin)}
|
1110
|
+
|
1111
|
+
|
1059
1112
|
class InternalPlugin(Plugin):
|
1060
1113
|
"""Parent class for internal plugins.
|
1061
1114
|
|
@@ -1065,13 +1118,17 @@ class InternalPlugin(Plugin):
|
|
1065
1118
|
|
1066
1119
|
def __init_subclass__(cls, **kwargs):
|
1067
1120
|
for method in get_nonprivate_methods(cls):
|
1068
|
-
if callable(method):
|
1121
|
+
if callable(method) and method.__name__ not in __COMMON_PLUGIN_METHOD_NAMES__:
|
1069
1122
|
method.__internal__ = True
|
1070
1123
|
|
1071
1124
|
super().__init_subclass__(**kwargs)
|
1072
1125
|
return cls
|
1073
1126
|
|
1074
1127
|
|
1128
|
+
class InternalNamespacePlugin(NamespacePlugin, InternalPlugin):
|
1129
|
+
pass
|
1130
|
+
|
1131
|
+
|
1075
1132
|
@dataclass(frozen=True, eq=True)
|
1076
1133
|
class PluginFunction:
|
1077
1134
|
name: str
|
@@ -25,6 +25,7 @@ class ChromePlugin(ChromiumMixin, BrowserPlugin):
|
|
25
25
|
DIRS = [
|
26
26
|
# Windows
|
27
27
|
"AppData/Local/Google/Chrome/User Data/Default",
|
28
|
+
"AppData/Local/Google/Chrome/User Data/Snapshots/*/Default",
|
28
29
|
"AppData/Local/Google/Chrome/continuousUpdates/User Data/Default",
|
29
30
|
"Local Settings/Application Data/Google/Chrome/User Data/Default",
|
30
31
|
# Linux
|
@@ -79,11 +79,12 @@ class ChromiumMixin:
|
|
79
79
|
users_dirs: list[tuple] = []
|
80
80
|
for user_details in self.target.user_details.all_with_home():
|
81
81
|
for d in hist_paths:
|
82
|
-
|
83
|
-
cur_dir
|
84
|
-
|
85
|
-
|
86
|
-
|
82
|
+
home_dir: TargetPath = user_details.home_path
|
83
|
+
for cur_dir in home_dir.glob(d):
|
84
|
+
cur_dir = cur_dir.resolve()
|
85
|
+
if not cur_dir.exists() or (user_details.user, cur_dir) in users_dirs:
|
86
|
+
continue
|
87
|
+
users_dirs.append((user_details, cur_dir))
|
87
88
|
return users_dirs
|
88
89
|
|
89
90
|
def _iter_db(
|
@@ -278,6 +279,7 @@ class ChromiumMixin:
|
|
278
279
|
is_http_only=bool(cookie.is_httponly),
|
279
280
|
same_site=bool(cookie.samesite),
|
280
281
|
source=db_file,
|
282
|
+
_target=self.target,
|
281
283
|
_user=user.user,
|
282
284
|
)
|
283
285
|
except SQLError as e:
|
@@ -28,6 +28,7 @@ class EdgePlugin(ChromiumMixin, BrowserPlugin):
|
|
28
28
|
".var/app/com.microsoft.Edge/config/microsoft-edge/Default",
|
29
29
|
# Windows
|
30
30
|
"AppData/Local/Microsoft/Edge/User Data/Default",
|
31
|
+
"AppData/Local/Microsoft/Edge/User Data/Snapshots/*/Default",
|
31
32
|
# Macos
|
32
33
|
"Library/Application Support/Microsoft Edge/Default",
|
33
34
|
]
|