dissect.target 3.19.dev40__py3-none-any.whl → 3.19.dev42__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dissect/target/filesystems/extfs.py +4 -0
- dissect/target/helpers/shell_application_ids.py +732 -0
- dissect/target/helpers/utils.py +11 -0
- dissect/target/loaders/tar.py +8 -4
- dissect/target/loaders/velociraptor.py +6 -6
- dissect/target/plugin.py +50 -0
- dissect/target/plugins/os/unix/history.py +3 -7
- dissect/target/plugins/os/windows/catroot.py +1 -11
- dissect/target/plugins/os/windows/jumplist.py +292 -0
- dissect/target/plugins/os/windows/lnk.py +84 -89
- dissect/target/target.py +1 -1
- dissect/target/tools/fs.py +25 -65
- dissect/target/tools/fsutils.py +243 -0
- dissect/target/tools/info.py +5 -1
- dissect/target/tools/shell.py +473 -347
- dissect/target/tools/utils.py +9 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/METADATA +10 -6
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/RECORD +23 -20
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/LICENSE +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/WHEEL +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/top_level.txt +0 -0
dissect/target/tools/shell.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import argparse
|
2
4
|
import cmd
|
3
5
|
import contextlib
|
4
|
-
import datetime
|
5
6
|
import fnmatch
|
6
7
|
import io
|
7
8
|
import itertools
|
@@ -13,27 +14,33 @@ import random
|
|
13
14
|
import re
|
14
15
|
import shlex
|
15
16
|
import shutil
|
16
|
-
import stat
|
17
17
|
import subprocess
|
18
18
|
import sys
|
19
19
|
from contextlib import contextmanager
|
20
|
-
from
|
20
|
+
from datetime import datetime, timedelta, timezone
|
21
|
+
from typing import Any, BinaryIO, Callable, Iterator, TextIO
|
21
22
|
|
22
23
|
from dissect.cstruct import hexdump
|
23
24
|
from flow.record import RecordOutput
|
24
25
|
|
25
26
|
from dissect.target.exceptions import (
|
26
|
-
FileNotFoundError,
|
27
27
|
PluginError,
|
28
28
|
RegistryError,
|
29
29
|
RegistryKeyNotFoundError,
|
30
30
|
RegistryValueNotFoundError,
|
31
31
|
TargetError,
|
32
32
|
)
|
33
|
-
from dissect.target.filesystem import FilesystemEntry
|
33
|
+
from dissect.target.filesystem import FilesystemEntry
|
34
34
|
from dissect.target.helpers import cyber, fsutil, regutil
|
35
|
-
from dissect.target.plugin import PluginFunction, arg
|
35
|
+
from dissect.target.plugin import PluginFunction, alias, arg, clone_alias
|
36
36
|
from dissect.target.target import Target
|
37
|
+
from dissect.target.tools.fsutils import (
|
38
|
+
fmt_ls_colors,
|
39
|
+
ls_scandir,
|
40
|
+
print_ls,
|
41
|
+
print_stat,
|
42
|
+
print_xattr,
|
43
|
+
)
|
37
44
|
from dissect.target.tools.info import print_target_info
|
38
45
|
from dissect.target.tools.utils import (
|
39
46
|
args_to_uri,
|
@@ -52,47 +59,23 @@ logging.raiseExceptions = False
|
|
52
59
|
try:
|
53
60
|
import readline
|
54
61
|
|
55
|
-
# remove
|
62
|
+
# remove `-`, `$` and `{` as an autocomplete delimeter on Linux
|
56
63
|
# https://stackoverflow.com/questions/27288340/python-cmd-on-linux-does-not-autocomplete-special-characters-or-symbols
|
57
|
-
readline.set_completer_delims(readline.get_completer_delims().replace("-", "").replace("$", ""))
|
64
|
+
readline.set_completer_delims(readline.get_completer_delims().replace("-", "").replace("$", "").replace("{", ""))
|
65
|
+
|
66
|
+
# Fix autocomplete on macOS
|
67
|
+
# https://stackoverflow.com/a/7116997
|
68
|
+
if "libedit" in readline.__doc__:
|
69
|
+
readline.parse_and_bind("bind ^I rl_complete")
|
70
|
+
else:
|
71
|
+
readline.parse_and_bind("tab: complete")
|
58
72
|
except ImportError:
|
59
73
|
# Readline is not available on Windows
|
60
74
|
log.warning("Readline module is not available")
|
61
75
|
readline = None
|
62
76
|
|
63
|
-
# ['mode', 'addr', 'dev', 'nlink', 'uid', 'gid', 'size', 'atime', 'mtime', 'ctime']
|
64
|
-
STAT_TEMPLATE = """ File: {path} {symlink}
|
65
|
-
Size: {size} {filetype}
|
66
|
-
Inode: {inode} Links: {nlink}
|
67
|
-
Access: ({modeord}/{modestr}) Uid: ( {uid} ) Gid: ( {gid} )
|
68
|
-
Access: {atime}
|
69
|
-
Modify: {mtime}
|
70
|
-
Change: {ctime}"""
|
71
|
-
|
72
|
-
FALLBACK_LS_COLORS = "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32" # noqa: E501
|
73
|
-
|
74
|
-
|
75
|
-
def prepare_ls_colors() -> dict[str, str]:
|
76
|
-
"""Parse the LS_COLORS environment variable so we can use it later."""
|
77
|
-
d = {}
|
78
|
-
ls_colors = os.environ.get("LS_COLORS", FALLBACK_LS_COLORS)
|
79
|
-
for line in ls_colors.split(":"):
|
80
|
-
if not line:
|
81
|
-
continue
|
82
|
-
|
83
|
-
ft, _, value = line.partition("=")
|
84
|
-
if ft.startswith("*"):
|
85
|
-
ft = ft[1:]
|
86
|
-
|
87
|
-
d[ft] = f"\x1b[{value}m{{}}\x1b[0m"
|
88
77
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
LS_COLORS = prepare_ls_colors()
|
93
|
-
|
94
|
-
|
95
|
-
class TargetCmd(cmd.Cmd):
|
78
|
+
class ExtendedCmd(cmd.Cmd):
|
96
79
|
"""Subclassed cmd.Cmd to provide some additional features.
|
97
80
|
|
98
81
|
Add new simple commands by implementing:
|
@@ -112,49 +95,25 @@ class TargetCmd(cmd.Cmd):
|
|
112
95
|
|
113
96
|
CMD_PREFIX = "cmd_"
|
114
97
|
|
115
|
-
|
116
|
-
DEFAULT_HISTFILESIZE = 10_000
|
117
|
-
DEFAULT_HISTDIR = None
|
118
|
-
DEFAULT_HISTDIRFMT = ".dissect_history_{uid}_{target}"
|
119
|
-
|
120
|
-
def __init__(self, target: Target):
|
98
|
+
def __init__(self, cyber: bool = False):
|
121
99
|
cmd.Cmd.__init__(self)
|
122
|
-
self.target = target
|
123
100
|
self.debug = False
|
101
|
+
self.cyber = cyber
|
124
102
|
self.identchars += "."
|
125
103
|
|
126
|
-
self.
|
127
|
-
self.histdir = getattr(target._config, "HISTDIR", self.DEFAULT_HISTDIR)
|
128
|
-
|
129
|
-
if self.histdir:
|
130
|
-
self.histdirfmt = getattr(target._config, "HISTDIRFMT", self.DEFAULT_HISTDIRFMT)
|
131
|
-
self.histfile = pathlib.Path(self.histdir).resolve() / pathlib.Path(
|
132
|
-
self.histdirfmt.format(uid=os.getuid(), target=target.name)
|
133
|
-
)
|
134
|
-
else:
|
135
|
-
self.histfile = pathlib.Path(getattr(target._config, "HISTFILE", self.DEFAULT_HISTFILE)).expanduser()
|
136
|
-
|
137
|
-
def preloop(self) -> None:
|
138
|
-
if readline and self.histfile.exists():
|
139
|
-
try:
|
140
|
-
readline.read_history_file(self.histfile)
|
141
|
-
except Exception as e:
|
142
|
-
log.debug("Error reading history file: %s", e)
|
143
|
-
|
144
|
-
def postloop(self) -> None:
|
145
|
-
if readline:
|
146
|
-
readline.set_history_length(self.histfilesize)
|
147
|
-
try:
|
148
|
-
readline.write_history_file(self.histfile)
|
149
|
-
except Exception as e:
|
150
|
-
log.debug("Error writing history file: %s", e)
|
104
|
+
self.register_aliases()
|
151
105
|
|
152
106
|
def __getattr__(self, attr: str) -> Any:
|
153
107
|
if attr.startswith("help_"):
|
154
108
|
_, _, command = attr.partition("_")
|
109
|
+
|
110
|
+
def print_help(command: str, func: Callable) -> None:
|
111
|
+
parser = generate_argparse_for_bound_method(func, usage_tmpl=f"{command} {{usage}}")
|
112
|
+
parser.print_help()
|
113
|
+
|
155
114
|
try:
|
156
115
|
func = getattr(self, self.CMD_PREFIX + command)
|
157
|
-
return lambda:
|
116
|
+
return lambda: print_help(command, func)
|
158
117
|
except AttributeError:
|
159
118
|
pass
|
160
119
|
|
@@ -164,6 +123,16 @@ class TargetCmd(cmd.Cmd):
|
|
164
123
|
def check_compatible(target: Target) -> bool:
|
165
124
|
return True
|
166
125
|
|
126
|
+
def register_aliases(self) -> None:
|
127
|
+
for name in self.get_names():
|
128
|
+
if name.startswith(self.CMD_PREFIX):
|
129
|
+
func = getattr(self.__class__, name)
|
130
|
+
for alias_name in getattr(func, "__aliases__", []):
|
131
|
+
if not alias_name.startswith(self.CMD_PREFIX):
|
132
|
+
alias_name = self.CMD_PREFIX + alias_name
|
133
|
+
|
134
|
+
clone_alias(self.__class__, func, alias_name)
|
135
|
+
|
167
136
|
def get_names(self) -> list[str]:
|
168
137
|
names = cmd.Cmd.get_names(self)
|
169
138
|
|
@@ -175,44 +144,49 @@ class TargetCmd(cmd.Cmd):
|
|
175
144
|
|
176
145
|
return names
|
177
146
|
|
178
|
-
def
|
147
|
+
def _handle_command(self, line: str) -> bool | None:
|
148
|
+
"""Check whether custom handling of the cmd can be performed and if so, do it.
|
149
|
+
|
150
|
+
If a custom handling of the cmd was performed, return the result (a boolean indicating whether the shell should
|
151
|
+
exit). If not, return None. Can be overridden by subclasses to perform further / other 'custom command' checks.
|
152
|
+
"""
|
179
153
|
if line == "EOF":
|
180
154
|
return True
|
181
155
|
|
182
|
-
# Override default command execution to first attempt complex
|
183
|
-
# command execution, and then target plugin command execution
|
156
|
+
# Override default command execution to first attempt complex command execution
|
184
157
|
command, command_args_str, line = self.parseline(line)
|
185
158
|
|
186
|
-
|
159
|
+
if hasattr(self, self.CMD_PREFIX + command):
|
187
160
|
return self._exec_command(command, command_args_str)
|
188
|
-
except AttributeError:
|
189
|
-
pass
|
190
161
|
|
191
|
-
if
|
192
|
-
|
162
|
+
# Return None if no custom command was found to be run
|
163
|
+
return None
|
193
164
|
|
194
|
-
|
165
|
+
def default(self, line: str) -> bool:
|
166
|
+
if (should_exit := self._handle_command(line)) is not None:
|
167
|
+
return should_exit
|
168
|
+
|
169
|
+
# Fallback to default
|
170
|
+
cmd.Cmd.default(self, line)
|
171
|
+
return False
|
195
172
|
|
196
173
|
def emptyline(self) -> None:
|
197
174
|
"""This function forces Python's cmd.Cmd module to behave like a regular shell.
|
198
175
|
|
199
176
|
When entering an empty command, the cmd module will by default repeat the previous command.
|
200
177
|
By defining an empty ``emptyline`` function we make sure no command is executed instead.
|
201
|
-
|
178
|
+
Resources:
|
179
|
+
- https://stackoverflow.com/a/16479030
|
180
|
+
- https://github.com/python/cpython/blob/3.12/Lib/cmd.py#L10
|
202
181
|
"""
|
203
182
|
pass
|
204
183
|
|
205
|
-
def _exec(
|
206
|
-
self, func: Callable[[list[str], TextIO], bool], command_args_str: str, no_cyber: bool = False
|
207
|
-
) -> Optional[bool]:
|
184
|
+
def _exec(self, func: Callable[[list[str], TextIO], bool], command_args_str: str, no_cyber: bool = False) -> bool:
|
208
185
|
"""Command execution helper that chains initial command and piped subprocesses (if any) together."""
|
209
186
|
|
210
187
|
argparts = []
|
211
188
|
if command_args_str is not None:
|
212
|
-
|
213
|
-
lexer.wordchars += "$"
|
214
|
-
lexer.whitespace_split = True
|
215
|
-
argparts = list(lexer)
|
189
|
+
argparts = arg_str_to_arg_list(command_args_str)
|
216
190
|
|
217
191
|
if "|" in argparts:
|
218
192
|
pipeidx = argparts.index("|")
|
@@ -223,15 +197,16 @@ class TargetCmd(cmd.Cmd):
|
|
223
197
|
except OSError as e:
|
224
198
|
# in case of a failure in a subprocess
|
225
199
|
print(e)
|
200
|
+
return False
|
226
201
|
else:
|
227
202
|
ctx = contextlib.nullcontext()
|
228
|
-
if self.
|
203
|
+
if self.cyber and not no_cyber:
|
229
204
|
ctx = cyber.cyber(color=None, run_at_end=True)
|
230
205
|
|
231
206
|
with ctx:
|
232
207
|
return func(argparts, sys.stdout)
|
233
208
|
|
234
|
-
def _exec_command(self, command: str, command_args_str: str) ->
|
209
|
+
def _exec_command(self, command: str, command_args_str: str) -> bool:
|
235
210
|
"""Command execution helper for ``cmd_`` commands."""
|
236
211
|
cmdfunc = getattr(self, self.CMD_PREFIX + command)
|
237
212
|
argparser = generate_argparse_for_bound_method(cmdfunc, usage_tmpl=f"{command} {{usage}}")
|
@@ -240,21 +215,122 @@ class TargetCmd(cmd.Cmd):
|
|
240
215
|
try:
|
241
216
|
args = argparser.parse_args(argparts)
|
242
217
|
except SystemExit:
|
243
|
-
return
|
218
|
+
return False
|
244
219
|
return cmdfunc(args, stdout)
|
245
220
|
|
246
221
|
# These commands enter a subshell, which doesn't work well with cyber
|
247
222
|
no_cyber = cmdfunc.__func__ in (TargetCli.cmd_registry, TargetCli.cmd_enter)
|
248
223
|
return self._exec(_exec_, command_args_str, no_cyber)
|
249
224
|
|
250
|
-
def
|
225
|
+
def do_man(self, line: str) -> bool:
|
226
|
+
"""alias for help"""
|
227
|
+
return self.do_help(line)
|
228
|
+
|
229
|
+
def complete_man(self, *args) -> list[str]:
|
230
|
+
return cmd.Cmd.complete_help(self, *args)
|
231
|
+
|
232
|
+
def do_clear(self, line: str) -> bool:
|
233
|
+
"""clear the terminal screen"""
|
234
|
+
os.system("cls||clear")
|
235
|
+
return False
|
236
|
+
|
237
|
+
def do_cls(self, line: str) -> bool:
|
238
|
+
"""alias for clear"""
|
239
|
+
return self.do_clear(line)
|
240
|
+
|
241
|
+
def do_exit(self, line: str) -> bool:
|
242
|
+
"""exit shell"""
|
243
|
+
return True
|
244
|
+
|
245
|
+
def do_cyber(self, line: str) -> bool:
|
246
|
+
"""cyber"""
|
247
|
+
self.cyber = not self.cyber
|
248
|
+
word, color = {False: ("D I S E N", cyber.Color.RED), True: ("E N", cyber.Color.YELLOW)}[self.cyber]
|
249
|
+
with cyber.cyber(color=color):
|
250
|
+
print(f"C Y B E R - M O D E - {word} G A G E D")
|
251
|
+
return False
|
252
|
+
|
253
|
+
def do_debug(self, line: str) -> bool:
|
254
|
+
"""toggle debug mode"""
|
255
|
+
self.debug = not self.debug
|
256
|
+
if self.debug:
|
257
|
+
print("Debug mode on")
|
258
|
+
else:
|
259
|
+
print("Debug mode off")
|
260
|
+
return False
|
261
|
+
|
262
|
+
|
263
|
+
class TargetCmd(ExtendedCmd):
|
264
|
+
DEFAULT_HISTFILE = "~/.dissect_history"
|
265
|
+
DEFAULT_HISTFILESIZE = 10_000
|
266
|
+
DEFAULT_HISTDIR = None
|
267
|
+
DEFAULT_HISTDIRFMT = ".dissect_history_{uid}_{target}"
|
268
|
+
|
269
|
+
def __init__(self, target: Target):
|
270
|
+
self.target = target
|
271
|
+
|
272
|
+
# history file
|
273
|
+
self.histfilesize = getattr(target._config, "HISTFILESIZE", self.DEFAULT_HISTFILESIZE)
|
274
|
+
self.histdir = getattr(target._config, "HISTDIR", self.DEFAULT_HISTDIR)
|
275
|
+
|
276
|
+
if self.histdir:
|
277
|
+
self.histdirfmt = getattr(target._config, "HISTDIRFMT", self.DEFAULT_HISTDIRFMT)
|
278
|
+
self.histfile = pathlib.Path(self.histdir).resolve() / pathlib.Path(
|
279
|
+
self.histdirfmt.format(uid=os.getuid(), target=target.name)
|
280
|
+
)
|
281
|
+
else:
|
282
|
+
self.histfile = pathlib.Path(getattr(target._config, "HISTFILE", self.DEFAULT_HISTFILE)).expanduser()
|
283
|
+
|
284
|
+
# prompt format
|
285
|
+
if ps1 := getattr(target._config, "PS1", None):
|
286
|
+
if "{cwd}" in ps1 and "{base}" in ps1:
|
287
|
+
self.prompt_ps1 = ps1
|
288
|
+
|
289
|
+
elif getattr(target._config, "NO_COLOR", None) or os.getenv("NO_COLOR"):
|
290
|
+
self.prompt_ps1 = "{base}:{cwd}$ "
|
291
|
+
|
292
|
+
else:
|
293
|
+
self.prompt_ps1 = "\x1b[1;32m{base}\x1b[0m:\x1b[1;34m{cwd}\x1b[0m$ "
|
294
|
+
|
295
|
+
super().__init__(self.target.props.get("cyber"))
|
296
|
+
|
297
|
+
def preloop(self) -> None:
|
298
|
+
if readline and self.histfile.exists():
|
299
|
+
try:
|
300
|
+
readline.read_history_file(self.histfile)
|
301
|
+
except Exception as e:
|
302
|
+
log.debug("Error reading history file: %s", e)
|
303
|
+
|
304
|
+
def postloop(self) -> None:
|
305
|
+
if readline:
|
306
|
+
readline.set_history_length(self.histfilesize)
|
307
|
+
try:
|
308
|
+
readline.write_history_file(self.histfile)
|
309
|
+
except Exception as e:
|
310
|
+
log.debug("Error writing history file: %s", e)
|
311
|
+
|
312
|
+
def _handle_command(self, line: str) -> bool | None:
|
313
|
+
if (should_exit := super()._handle_command(line)) is not None:
|
314
|
+
return should_exit
|
315
|
+
|
316
|
+
# The parent class has already attempted complex command execution, we now attempt target plugin command
|
317
|
+
# execution
|
318
|
+
command, command_args_str, line = self.parseline(line)
|
319
|
+
|
320
|
+
if plugins := list(find_and_filter_plugins(self.target, command, [])):
|
321
|
+
return self._exec_target(plugins, command_args_str)
|
322
|
+
|
323
|
+
# We didn't execute a function on the target
|
324
|
+
return None
|
325
|
+
|
326
|
+
def _exec_target(self, funcs: list[PluginFunction], command_args_str: str) -> bool:
|
251
327
|
"""Command exection helper for target plugins."""
|
252
328
|
|
253
|
-
def _exec_(argparts: list[str], stdout: TextIO) ->
|
329
|
+
def _exec_(argparts: list[str], stdout: TextIO) -> None:
|
254
330
|
try:
|
255
331
|
output, value, _ = execute_function_on_target(self.target, func, argparts)
|
256
332
|
except SystemExit:
|
257
|
-
return
|
333
|
+
return
|
258
334
|
|
259
335
|
if output == "record":
|
260
336
|
# if the command results are piped to another process,
|
@@ -276,45 +352,21 @@ class TargetCmd(cmd.Cmd):
|
|
276
352
|
else:
|
277
353
|
print(value, file=stdout)
|
278
354
|
|
279
|
-
result = None
|
280
355
|
for func in funcs:
|
281
356
|
try:
|
282
|
-
|
357
|
+
self._exec(_exec_, command_args_str)
|
283
358
|
except PluginError as err:
|
284
359
|
if self.debug:
|
285
360
|
raise err
|
286
361
|
self.target.log.error(err)
|
287
362
|
|
288
|
-
|
363
|
+
# Keep the shell open
|
364
|
+
return False
|
289
365
|
|
290
|
-
def do_python(self, line: str) ->
|
366
|
+
def do_python(self, line: str) -> bool:
|
291
367
|
"""drop into a Python shell"""
|
292
368
|
python_shell([self.target])
|
293
|
-
|
294
|
-
def do_clear(self, line: str) -> Optional[bool]:
|
295
|
-
"""clear the terminal screen"""
|
296
|
-
os.system("cls||clear")
|
297
|
-
|
298
|
-
def do_cyber(self, line: str) -> Optional[bool]:
|
299
|
-
"""cyber"""
|
300
|
-
self.target.props["cyber"] = not bool(self.target.props.get("cyber"))
|
301
|
-
word, color = {False: ("D I S E N", cyber.Color.RED), True: ("E N", cyber.Color.YELLOW)}[
|
302
|
-
self.target.props["cyber"]
|
303
|
-
]
|
304
|
-
with cyber.cyber(color=color):
|
305
|
-
print(f"C Y B E R - M O D E - {word} G A G E D")
|
306
|
-
|
307
|
-
def do_exit(self, line: str) -> Optional[bool]:
|
308
|
-
"""exit shell"""
|
309
|
-
return True
|
310
|
-
|
311
|
-
def do_debug(self, line: str) -> Optional[bool]:
|
312
|
-
"""toggle debug mode"""
|
313
|
-
self.debug = not self.debug
|
314
|
-
if self.debug:
|
315
|
-
print("Debug mode on")
|
316
|
-
else:
|
317
|
-
print("Debug mode off")
|
369
|
+
return False
|
318
370
|
|
319
371
|
|
320
372
|
class TargetHubCli(cmd.Cmd):
|
@@ -337,24 +389,26 @@ class TargetHubCli(cmd.Cmd):
|
|
337
389
|
|
338
390
|
self._clicache = {}
|
339
391
|
|
340
|
-
def default(self, line: str) ->
|
392
|
+
def default(self, line: str) -> bool:
|
341
393
|
if line == "EOF":
|
342
394
|
return True
|
343
395
|
|
344
|
-
|
396
|
+
cmd.Cmd.default(self, line)
|
397
|
+
return False
|
345
398
|
|
346
399
|
def emptyline(self) -> None:
|
347
400
|
pass
|
348
401
|
|
349
|
-
def do_exit(self, line: str) ->
|
402
|
+
def do_exit(self, line: str) -> bool:
|
350
403
|
"""exit shell"""
|
351
404
|
return True
|
352
405
|
|
353
|
-
def do_list(self, line: str) ->
|
406
|
+
def do_list(self, line: str) -> bool:
|
354
407
|
"""list the loaded targets"""
|
355
408
|
print("\n".join([f"{i:2d}: {e}" for i, e in enumerate(self._names)]))
|
409
|
+
return False
|
356
410
|
|
357
|
-
def do_enter(self, line: str) ->
|
411
|
+
def do_enter(self, line: str) -> bool:
|
358
412
|
"""enter a target by number or name"""
|
359
413
|
|
360
414
|
if line.isdigit():
|
@@ -364,24 +418,25 @@ class TargetHubCli(cmd.Cmd):
|
|
364
418
|
idx = self._names_lower.index(line.lower())
|
365
419
|
except ValueError:
|
366
420
|
print("Unknown name")
|
367
|
-
return
|
421
|
+
return False
|
368
422
|
|
369
423
|
if idx >= len(self.targets):
|
370
424
|
print("Unknown target")
|
371
|
-
return
|
425
|
+
return False
|
372
426
|
|
373
427
|
try:
|
374
428
|
cli = self._clicache[idx]
|
375
429
|
except KeyError:
|
376
430
|
target = self.targets[idx]
|
377
431
|
if not self._targetcli.check_compatible(target):
|
378
|
-
return
|
432
|
+
return False
|
379
433
|
|
380
434
|
cli = self._targetcli(self.targets[idx])
|
381
435
|
self._clicache[idx] = cli
|
382
436
|
|
383
437
|
print(f"Entering {idx}: {self._names[idx]}")
|
384
438
|
run_cli(cli)
|
439
|
+
return False
|
385
440
|
|
386
441
|
def complete_enter(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
|
387
442
|
if not text:
|
@@ -395,32 +450,41 @@ class TargetHubCli(cmd.Cmd):
|
|
395
450
|
|
396
451
|
return compl
|
397
452
|
|
398
|
-
def do_python(self, line: str) ->
|
453
|
+
def do_python(self, line: str) -> bool:
|
399
454
|
"""drop into a Python shell"""
|
400
455
|
python_shell(self.targets)
|
456
|
+
return False
|
401
457
|
|
402
458
|
|
403
459
|
class TargetCli(TargetCmd):
|
404
460
|
"""CLI for interacting with a target and browsing the filesystem."""
|
405
461
|
|
406
462
|
def __init__(self, target: Target):
|
463
|
+
self.prompt_base = _target_name(target)
|
464
|
+
|
407
465
|
TargetCmd.__init__(self, target)
|
408
|
-
self.prompt_base = target.name
|
409
|
-
self._clicache = {}
|
410
466
|
|
467
|
+
self._clicache = {}
|
411
468
|
self.cwd = None
|
412
469
|
self.chdir("/")
|
413
470
|
|
414
471
|
@property
|
415
472
|
def prompt(self) -> str:
|
416
|
-
return
|
473
|
+
return self.prompt_ps1.format(base=self.prompt_base, cwd=self.cwd)
|
417
474
|
|
418
|
-
def completedefault(self, text: str, line: str, begidx: int, endidx: int):
|
419
|
-
path = line[:begidx].rsplit(" ")[-1]
|
475
|
+
def completedefault(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
|
476
|
+
path = self.resolve_path(line[:begidx].rsplit(" ")[-1])
|
420
477
|
textlower = text.lower()
|
421
478
|
|
422
|
-
|
423
|
-
|
479
|
+
suggestions = []
|
480
|
+
for fpath, fname in ls_scandir(path):
|
481
|
+
if not fname.lower().startswith(textlower):
|
482
|
+
continue
|
483
|
+
|
484
|
+
# Add a trailing slash to directories, to allow for easier traversal of the filesystem
|
485
|
+
suggestion = f"{fname}/" if fpath.is_dir() else fname
|
486
|
+
suggestions.append(suggestion)
|
487
|
+
return suggestions
|
424
488
|
|
425
489
|
def resolve_path(self, path: str) -> fsutil.TargetPath:
|
426
490
|
if not path:
|
@@ -448,7 +512,7 @@ class TargetCli(TargetCmd):
|
|
448
512
|
# component
|
449
513
|
print(err)
|
450
514
|
|
451
|
-
def check_file(self, path: str) ->
|
515
|
+
def check_file(self, path: str) -> fsutil.TargetPath | None:
|
452
516
|
path = self.resolve_path(path)
|
453
517
|
if not path.exists():
|
454
518
|
print(f"{path}: No such file")
|
@@ -464,7 +528,7 @@ class TargetCli(TargetCmd):
|
|
464
528
|
|
465
529
|
return path
|
466
530
|
|
467
|
-
def check_dir(self, path: str) ->
|
531
|
+
def check_dir(self, path: str) -> fsutil.TargetPath | None:
|
468
532
|
path = self.resolve_path(path)
|
469
533
|
if not path.exists():
|
470
534
|
print(f"{path}: No such directory")
|
@@ -480,70 +544,51 @@ class TargetCli(TargetCmd):
|
|
480
544
|
|
481
545
|
return path
|
482
546
|
|
547
|
+
def check_path(self, path: str) -> fsutil.TargetPath | None:
|
548
|
+
path = self.resolve_path(path)
|
549
|
+
if not path.exists():
|
550
|
+
print(f"{path}: No such file or directory")
|
551
|
+
return
|
552
|
+
|
553
|
+
return path
|
554
|
+
|
483
555
|
def chdir(self, path: str) -> None:
|
484
556
|
"""Change directory to the given path."""
|
485
557
|
if path := self.check_dir(path):
|
486
558
|
self.cwd = path
|
487
559
|
|
488
|
-
def
|
489
|
-
"""List a directory for the given path."""
|
490
|
-
path = self.resolve_path(path)
|
491
|
-
result = []
|
492
|
-
|
493
|
-
if path.exists() and path.is_dir():
|
494
|
-
for file_ in path.iterdir():
|
495
|
-
file_type = None
|
496
|
-
if color:
|
497
|
-
if file_.is_symlink():
|
498
|
-
file_type = "ln"
|
499
|
-
elif file_.is_dir():
|
500
|
-
file_type = "di"
|
501
|
-
elif file_.is_file():
|
502
|
-
file_type = "fi"
|
503
|
-
|
504
|
-
result.append((file_, fmt_ls_colors(file_type, file_.name) if color else file_.name))
|
505
|
-
|
506
|
-
# If we happen to scan an NTFS filesystem see if any of the
|
507
|
-
# entries has an alternative data stream and also list them.
|
508
|
-
entry = file_.get()
|
509
|
-
if isinstance(entry, LayerFilesystemEntry):
|
510
|
-
if entry.entries.fs.__type__ == "ntfs":
|
511
|
-
attrs = entry.lattr()
|
512
|
-
for data_stream in attrs.DATA:
|
513
|
-
if data_stream.name != "":
|
514
|
-
name = f"{file_.name}:{data_stream.name}"
|
515
|
-
result.append((file_, fmt_ls_colors(file_type, name) if color else name))
|
516
|
-
|
517
|
-
result.sort(key=lambda e: e[0].name)
|
518
|
-
|
519
|
-
return result
|
520
|
-
|
521
|
-
def do_cd(self, line: str) -> Optional[bool]:
|
560
|
+
def do_cd(self, line: str) -> bool:
|
522
561
|
"""change directory"""
|
523
562
|
self.chdir(line)
|
563
|
+
return False
|
524
564
|
|
525
|
-
def do_pwd(self, line: str) ->
|
565
|
+
def do_pwd(self, line: str) -> bool:
|
526
566
|
"""print current directory"""
|
527
567
|
print(self.cwd)
|
568
|
+
return False
|
528
569
|
|
529
|
-
def do_disks(self, line: str) ->
|
570
|
+
def do_disks(self, line: str) -> bool:
|
530
571
|
"""print target disks"""
|
531
572
|
for d in self.target.disks:
|
532
573
|
print(str(d))
|
574
|
+
return False
|
533
575
|
|
534
|
-
def do_volumes(self, line: str) ->
|
576
|
+
def do_volumes(self, line: str) -> bool:
|
535
577
|
"""print target volumes"""
|
536
578
|
for v in self.target.volumes:
|
537
579
|
print(str(v))
|
580
|
+
return False
|
538
581
|
|
539
|
-
def do_filesystems(self, line: str) ->
|
582
|
+
def do_filesystems(self, line: str) -> bool:
|
540
583
|
"""print target filesystems"""
|
541
584
|
for fs in self.target.filesystems:
|
542
585
|
print(str(fs))
|
586
|
+
return False
|
543
587
|
|
544
|
-
def do_info(self, line: str) ->
|
588
|
+
def do_info(self, line: str) -> bool:
|
545
589
|
"""print target information"""
|
546
|
-
|
590
|
+
print_target_info(self.target)
|
591
|
+
return False
|
547
592
|
|
548
593
|
@arg("path", nargs="?")
|
549
594
|
@arg("-l", action="store_true")
|
@@ -552,129 +597,137 @@ class TargetCli(TargetCmd):
|
|
552
597
|
@arg("-R", "--recursive", action="store_true", help="recursively list subdirectories encountered")
|
553
598
|
@arg("-c", action="store_true", dest="use_ctime", help="show time when file status was last changed")
|
554
599
|
@arg("-u", action="store_true", dest="use_atime", help="show time of last access")
|
555
|
-
|
600
|
+
@alias("l")
|
601
|
+
@alias("dir")
|
602
|
+
def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
556
603
|
"""list directory contents"""
|
557
604
|
|
558
605
|
path = self.resolve_path(args.path)
|
559
606
|
|
560
607
|
if args.use_ctime and args.use_atime:
|
561
608
|
print("can't specify -c and -u at the same time")
|
562
|
-
return
|
609
|
+
return False
|
563
610
|
|
564
|
-
if not path or not
|
565
|
-
return
|
611
|
+
if not path or not self.check_dir(path):
|
612
|
+
return False
|
566
613
|
|
567
|
-
|
614
|
+
if path.is_file():
|
615
|
+
print(args.path) # mimic ls behaviour
|
616
|
+
return False
|
568
617
|
|
569
|
-
|
570
|
-
|
571
|
-
subdirs = []
|
618
|
+
print_ls(path, 0, stdout, args.l, args.human_readable, args.recursive, args.use_ctime, args.use_atime)
|
619
|
+
return False
|
572
620
|
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
if not args.l:
|
582
|
-
for target_path, name in contents:
|
583
|
-
print(name, file=stdout)
|
584
|
-
if target_path.is_dir():
|
585
|
-
subdirs.append(target_path)
|
586
|
-
else:
|
587
|
-
if len(contents) > 1:
|
588
|
-
print(f"total {len(contents)}", file=stdout)
|
589
|
-
for target_path, name in contents:
|
590
|
-
self.print_extensive_file_stat(args=args, stdout=stdout, target_path=target_path, name=name)
|
591
|
-
if target_path.is_dir():
|
592
|
-
subdirs.append(target_path)
|
593
|
-
|
594
|
-
if args.recursive and subdirs:
|
595
|
-
for subdir in subdirs:
|
596
|
-
self._print_ls(args, subdir, depth + 1, stdout)
|
597
|
-
|
598
|
-
def print_extensive_file_stat(
|
599
|
-
self, args: argparse.Namespace, stdout: TextIO, target_path: fsutil.TargetPath, name: str
|
600
|
-
) -> None:
|
601
|
-
"""Print the file status."""
|
602
|
-
try:
|
603
|
-
entry = target_path.get()
|
604
|
-
stat = entry.lstat()
|
605
|
-
symlink = f" -> {entry.readlink()}" if entry.is_symlink() else ""
|
606
|
-
show_time = stat.st_mtime
|
607
|
-
if args.use_ctime:
|
608
|
-
show_time = stat.st_ctime
|
609
|
-
elif args.use_atime:
|
610
|
-
show_time = stat.st_atime
|
611
|
-
utc_time = datetime.datetime.utcfromtimestamp(show_time).isoformat()
|
612
|
-
|
613
|
-
print(
|
614
|
-
f"{stat_modestr(stat)} {stat.st_uid:4d} {stat.st_gid:4d} {stat.st_size:6d} {utc_time} {name}{symlink}",
|
615
|
-
file=stdout,
|
616
|
-
)
|
621
|
+
@arg("path", nargs="?")
|
622
|
+
def cmd_ll(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
623
|
+
"""alias for ls -la"""
|
624
|
+
args = extend_args(args, self.cmd_ls)
|
625
|
+
args.l = True # noqa: E741
|
626
|
+
args.a = True
|
627
|
+
return self.cmd_ls(args, stdout)
|
617
628
|
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
629
|
+
@arg("path", nargs="?")
|
630
|
+
def cmd_tree(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
631
|
+
"""alias for ls -R"""
|
632
|
+
args = extend_args(args, self.cmd_ls)
|
633
|
+
args.recursive = True
|
634
|
+
return self.cmd_ls(args, stdout)
|
623
635
|
|
624
636
|
@arg("path", nargs="?")
|
625
|
-
@arg("-name", default="*")
|
626
|
-
@arg("-iname")
|
627
|
-
|
637
|
+
@arg("-name", default="*", help="path to match with")
|
638
|
+
@arg("-iname", help="like -name, but the match is case insensitive")
|
639
|
+
@arg("-atime", type=int, help="file was last accessed n*24 hours ago")
|
640
|
+
@arg("-mtime", type=int, help="file was last modified n*24 hours ago")
|
641
|
+
@arg("-ctime", type=int, help="file (windows) or metadata (unix) was last changed n*24 hours ago")
|
642
|
+
@arg("-btime", type=int, help="file was born n*24 hours ago (ext4)")
|
643
|
+
def cmd_find(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
628
644
|
"""search for files in a directory hierarchy"""
|
629
645
|
path = self.resolve_path(args.path)
|
630
|
-
if not path:
|
631
|
-
return
|
646
|
+
if not path or not self.check_dir(path):
|
647
|
+
return False
|
648
|
+
|
649
|
+
matches = []
|
650
|
+
now = datetime.now(tz=timezone.utc)
|
651
|
+
do_time_compare = any([args.mtime, args.atime])
|
632
652
|
|
633
653
|
if args.iname:
|
634
654
|
pattern = re.compile(fnmatch.translate(args.iname), re.IGNORECASE)
|
635
655
|
for f in path.rglob("*"):
|
636
656
|
if pattern.match(f.name):
|
637
|
-
print(f, file=stdout)
|
638
|
-
|
639
|
-
elif args.name:
|
657
|
+
matches.append(f) if do_time_compare else print(f, file=stdout)
|
658
|
+
else:
|
640
659
|
for f in path.rglob(args.name):
|
660
|
+
matches.append(f) if do_time_compare else print(f, file=stdout)
|
661
|
+
|
662
|
+
def compare(now: datetime, then_ts: float, offset: int) -> bool:
|
663
|
+
then = datetime.fromtimestamp(then_ts, tz=timezone.utc)
|
664
|
+
return now - timedelta(hours=offset * 24) > then
|
665
|
+
|
666
|
+
if do_time_compare:
|
667
|
+
for f in matches:
|
668
|
+
s = f.lstat()
|
669
|
+
|
670
|
+
if args.mtime and compare(now, s.st_mtime, offset=args.mtime):
|
671
|
+
continue
|
672
|
+
|
673
|
+
if args.atime and compare(now, s.st_atime, offset=args.atime):
|
674
|
+
continue
|
675
|
+
|
676
|
+
if args.ctime and compare(now, s.st_ctime, offset=args.ctime):
|
677
|
+
continue
|
678
|
+
|
679
|
+
if args.btime and compare(now, s.st_birthtime, offset=args.btime):
|
680
|
+
continue
|
681
|
+
|
641
682
|
print(f, file=stdout)
|
642
683
|
|
684
|
+
return False
|
685
|
+
|
643
686
|
@arg("path")
|
644
687
|
@arg("-L", "--dereference", action="store_true")
|
645
|
-
def cmd_stat(self, args: argparse.Namespace, stdout: TextIO) ->
|
688
|
+
def cmd_stat(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
646
689
|
"""display file status"""
|
647
690
|
path = self.resolve_path(args.path)
|
648
|
-
if not path:
|
649
|
-
return
|
691
|
+
if not path or not self.check_path(path):
|
692
|
+
return False
|
693
|
+
|
694
|
+
print_stat(path, stdout, args.dereference)
|
695
|
+
return False
|
696
|
+
|
697
|
+
@arg("path")
|
698
|
+
@arg("-d", "--dump", action="store_true")
|
699
|
+
@arg("-R", "--recursive", action="store_true")
|
700
|
+
@alias("getfattr")
|
701
|
+
def cmd_attr(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
702
|
+
"""display file attributes"""
|
703
|
+
path = self.resolve_path(args.path)
|
704
|
+
if not path or not self.check_path(path):
|
705
|
+
return False
|
706
|
+
|
707
|
+
try:
|
708
|
+
if attr := path.get().attr():
|
709
|
+
print_xattr(path.name, attr, stdout)
|
710
|
+
except Exception:
|
711
|
+
pass
|
712
|
+
|
713
|
+
if args.recursive:
|
714
|
+
for child in path.rglob("*"):
|
715
|
+
try:
|
716
|
+
if child_attr := child.get().attr():
|
717
|
+
print_xattr(child, child_attr, stdout)
|
718
|
+
print()
|
719
|
+
except Exception:
|
720
|
+
pass
|
650
721
|
|
651
|
-
|
652
|
-
|
653
|
-
s = path.stat() if args.dereference else path.lstat()
|
654
|
-
|
655
|
-
res = STAT_TEMPLATE.format(
|
656
|
-
path=path,
|
657
|
-
symlink=symlink,
|
658
|
-
size=s.st_size,
|
659
|
-
filetype="",
|
660
|
-
inode=s.st_ino,
|
661
|
-
nlink=s.st_nlink,
|
662
|
-
modeord=oct(stat.S_IMODE(s.st_mode)),
|
663
|
-
modestr=stat_modestr(s),
|
664
|
-
uid=s.st_uid,
|
665
|
-
gid=s.st_gid,
|
666
|
-
atime=datetime.datetime.utcfromtimestamp(s.st_atime).isoformat(),
|
667
|
-
mtime=datetime.datetime.utcfromtimestamp(s.st_mtime).isoformat(),
|
668
|
-
ctime=datetime.datetime.utcfromtimestamp(s.st_ctime).isoformat(),
|
669
|
-
)
|
670
|
-
print(res, file=stdout)
|
722
|
+
return False
|
671
723
|
|
672
724
|
@arg("path")
|
673
|
-
def cmd_file(self, args: argparse.Namespace, stdout: TextIO) ->
|
725
|
+
def cmd_file(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
674
726
|
"""determine file type"""
|
727
|
+
|
675
728
|
path = self.check_file(args.path)
|
676
729
|
if not path:
|
677
|
-
return
|
730
|
+
return False
|
678
731
|
|
679
732
|
fh = path.open()
|
680
733
|
|
@@ -686,11 +739,12 @@ class TargetCli(TargetCmd):
|
|
686
739
|
p.wait()
|
687
740
|
filetype = p.stdout.read().decode().split(":", 1)[1].strip()
|
688
741
|
print(f"{path}: {filetype}", file=stdout)
|
742
|
+
return False
|
689
743
|
|
690
744
|
@arg("path", nargs="+")
|
691
745
|
@arg("-o", "--out", default=".")
|
692
746
|
@arg("-v", "--verbose", action="store_true")
|
693
|
-
def cmd_save(self, args: argparse.Namespace, stdout: TextIO) ->
|
747
|
+
def cmd_save(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
694
748
|
"""save a common file or directory to the host filesystem"""
|
695
749
|
dst_path = pathlib.Path(args.out).resolve()
|
696
750
|
|
@@ -719,7 +773,7 @@ class TargetCli(TargetCmd):
|
|
719
773
|
return diverging_path
|
720
774
|
|
721
775
|
def save_path(
|
722
|
-
src_path: pathlib.Path, dst_path: pathlib.Path, create_dst_subdir:
|
776
|
+
src_path: pathlib.Path, dst_path: pathlib.Path, create_dst_subdir: pathlib.Path | None = None
|
723
777
|
) -> None:
|
724
778
|
"""Save a common file or directory in src_path to dst_path.
|
725
779
|
|
@@ -798,8 +852,8 @@ class TargetCli(TargetCmd):
|
|
798
852
|
try:
|
799
853
|
first_src_path = next(src_paths)
|
800
854
|
except StopIteration:
|
801
|
-
print(f"{path}:
|
802
|
-
return
|
855
|
+
print(f"{path}: No such file or directory")
|
856
|
+
return False
|
803
857
|
|
804
858
|
try:
|
805
859
|
second_src_path = next(src_paths)
|
@@ -820,10 +874,18 @@ class TargetCli(TargetCmd):
|
|
820
874
|
extra_dir = get_diverging_path(src_path, reference_path).parent
|
821
875
|
save_path(src_path, dst_path, create_dst_subdir=extra_dir)
|
822
876
|
|
877
|
+
return False
|
878
|
+
|
823
879
|
@arg("path")
|
824
|
-
|
880
|
+
@alias("type")
|
881
|
+
def cmd_cat(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
825
882
|
"""print file content"""
|
826
|
-
paths = self.resolve_glob_path(args.path)
|
883
|
+
paths = list(self.resolve_glob_path(args.path))
|
884
|
+
|
885
|
+
if not paths:
|
886
|
+
print(f"{args.path}: No such file or directory")
|
887
|
+
return False
|
888
|
+
|
827
889
|
stdout = stdout.buffer
|
828
890
|
for path in paths:
|
829
891
|
path = self.check_file(path)
|
@@ -834,11 +896,17 @@ class TargetCli(TargetCmd):
|
|
834
896
|
shutil.copyfileobj(fh, stdout)
|
835
897
|
stdout.flush()
|
836
898
|
print("")
|
899
|
+
return False
|
837
900
|
|
838
901
|
@arg("path")
|
839
|
-
def cmd_zcat(self, args: argparse.Namespace, stdout: TextIO) ->
|
902
|
+
def cmd_zcat(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
840
903
|
"""print file content from compressed files"""
|
841
|
-
paths = self.resolve_glob_path(args.path)
|
904
|
+
paths = list(self.resolve_glob_path(args.path))
|
905
|
+
|
906
|
+
if not paths:
|
907
|
+
print(f"{args.path}: No such file or directory")
|
908
|
+
return False
|
909
|
+
|
842
910
|
stdout = stdout.buffer
|
843
911
|
for path in paths:
|
844
912
|
path = self.check_file(path)
|
@@ -849,45 +917,70 @@ class TargetCli(TargetCmd):
|
|
849
917
|
shutil.copyfileobj(fh, stdout)
|
850
918
|
stdout.flush()
|
851
919
|
|
920
|
+
return False
|
921
|
+
|
852
922
|
@arg("path")
|
853
|
-
|
854
|
-
|
923
|
+
@arg("-n", "--length", type=int, default=16 * 20, help="amount of bytes to read")
|
924
|
+
@arg("-s", "--skip", type=int, default=0, help="skip offset bytes from the beginning")
|
925
|
+
@arg("-p", "--hex", action="store_true", default=False, help="output in plain hexdump style")
|
926
|
+
@arg("-C", "--canonical", action="store_true")
|
927
|
+
@alias("xxd")
|
928
|
+
def cmd_hexdump(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
929
|
+
"""print a hexdump of a file"""
|
855
930
|
path = self.check_file(args.path)
|
856
931
|
if not path:
|
857
|
-
return
|
932
|
+
return False
|
933
|
+
|
934
|
+
fh = path.open("rb")
|
935
|
+
if args.skip > 0:
|
936
|
+
fh.seek(args.skip + 1)
|
937
|
+
|
938
|
+
if args.hex:
|
939
|
+
print(fh.read(args.length).hex(), file=stdout)
|
940
|
+
else:
|
941
|
+
print(hexdump(fh.read(args.length), output="string"), file=stdout)
|
858
942
|
|
859
|
-
|
943
|
+
return False
|
860
944
|
|
861
945
|
@arg("path")
|
862
|
-
|
946
|
+
@alias("digest")
|
947
|
+
@alias("shasum")
|
948
|
+
def cmd_hash(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
863
949
|
"""print the MD5, SHA1 and SHA256 hashes of a file"""
|
864
950
|
path = self.check_file(args.path)
|
865
951
|
if not path:
|
866
|
-
return
|
952
|
+
return False
|
867
953
|
|
868
954
|
md5, sha1, sha256 = path.get().hash()
|
869
955
|
print(f"MD5:\t{md5}\nSHA1:\t{sha1}\nSHA256:\t{sha256}", file=stdout)
|
956
|
+
return False
|
870
957
|
|
871
958
|
@arg("path")
|
872
|
-
|
959
|
+
@alias("head")
|
960
|
+
@alias("more")
|
961
|
+
def cmd_less(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
873
962
|
"""open the first 10 MB of a file with less"""
|
874
963
|
path = self.check_file(args.path)
|
875
964
|
if not path:
|
876
|
-
return
|
965
|
+
return False
|
877
966
|
|
878
967
|
pydoc.pager(path.open("rt", errors="ignore").read(10 * 1024 * 1024))
|
968
|
+
return False
|
879
969
|
|
880
970
|
@arg("path")
|
881
|
-
|
971
|
+
@alias("zhead")
|
972
|
+
@alias("zmore")
|
973
|
+
def cmd_zless(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
882
974
|
"""open the first 10 MB of a compressed file with zless"""
|
883
975
|
path = self.check_file(args.path)
|
884
976
|
if not path:
|
885
|
-
return
|
977
|
+
return False
|
886
978
|
|
887
979
|
pydoc.pager(fsutil.open_decompress(path, "rt").read(10 * 1024 * 1024))
|
980
|
+
return False
|
888
981
|
|
889
982
|
@arg("path", nargs="+")
|
890
|
-
def cmd_readlink(self, args: argparse.Namespace, stdout: TextIO) ->
|
983
|
+
def cmd_readlink(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
891
984
|
"""print resolved symbolic links or canonical file names"""
|
892
985
|
for path in args.path:
|
893
986
|
path = self.resolve_path(path)
|
@@ -896,12 +989,14 @@ class TargetCli(TargetCmd):
|
|
896
989
|
|
897
990
|
print(path.get().readlink(), file=stdout)
|
898
991
|
|
992
|
+
return False
|
993
|
+
|
899
994
|
@arg("path", nargs="?", help="load a hive from the given path")
|
900
|
-
def cmd_registry(self, args: argparse.Namespace, stdout: TextIO) ->
|
995
|
+
def cmd_registry(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
901
996
|
"""drop into a registry shell"""
|
902
997
|
if self.target.os == "linux":
|
903
998
|
run_cli(UnixConfigTreeCli(self.target))
|
904
|
-
return
|
999
|
+
return False
|
905
1000
|
|
906
1001
|
hive = None
|
907
1002
|
|
@@ -909,7 +1004,7 @@ class TargetCli(TargetCmd):
|
|
909
1004
|
if args.path:
|
910
1005
|
path = self.check_file(args.path)
|
911
1006
|
if not path:
|
912
|
-
return
|
1007
|
+
return False
|
913
1008
|
|
914
1009
|
hive = regutil.RegfHive(path)
|
915
1010
|
clikey = f"registry_{path}"
|
@@ -918,26 +1013,26 @@ class TargetCli(TargetCmd):
|
|
918
1013
|
cli = self._clicache[clikey]
|
919
1014
|
except KeyError:
|
920
1015
|
if not hive and not RegistryCli.check_compatible(self.target):
|
921
|
-
return
|
1016
|
+
return False
|
922
1017
|
|
923
1018
|
cli = RegistryCli(self.target, hive)
|
924
1019
|
self._clicache[clikey] = cli
|
925
1020
|
|
926
1021
|
run_cli(cli)
|
927
|
-
|
928
|
-
# Print an additional empty newline after exit
|
929
1022
|
print()
|
1023
|
+
return False
|
930
1024
|
|
931
1025
|
@arg("targets", metavar="TARGETS", nargs="*", help="targets to load")
|
932
1026
|
@arg("-p", "--python", action="store_true", help="(I)Python shell")
|
933
1027
|
@arg("-r", "--registry", action="store_true", help="registry shell")
|
934
|
-
def cmd_enter(self, args: argparse.Namespace, stdout: TextIO) ->
|
1028
|
+
def cmd_enter(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
935
1029
|
"""load one or more files as sub-targets and drop into a sub-shell"""
|
936
1030
|
paths = [self.resolve_path(path) for path in args.targets]
|
937
1031
|
|
938
1032
|
if args.python:
|
939
1033
|
# Quick path that doesn't require CLI caching
|
940
|
-
|
1034
|
+
open_shell(paths, args.python, args.registry)
|
1035
|
+
return False
|
941
1036
|
|
942
1037
|
clikey = tuple(str(path) for path in paths)
|
943
1038
|
try:
|
@@ -946,33 +1041,32 @@ class TargetCli(TargetCmd):
|
|
946
1041
|
targets = list(Target.open_all(paths))
|
947
1042
|
cli = create_cli(targets, RegistryCli if args.registry else TargetCli)
|
948
1043
|
if not cli:
|
949
|
-
return
|
1044
|
+
return False
|
950
1045
|
|
951
1046
|
self._clicache[clikey] = cli
|
952
1047
|
|
953
1048
|
run_cli(cli)
|
954
|
-
|
955
|
-
# Print an additional empty newline after exit
|
956
1049
|
print()
|
1050
|
+
return False
|
957
1051
|
|
958
1052
|
|
959
1053
|
class UnixConfigTreeCli(TargetCli):
|
960
1054
|
def __init__(self, target: Target):
|
961
1055
|
TargetCmd.__init__(self, target)
|
962
1056
|
self.config_tree = target.etc()
|
963
|
-
self.prompt_base = target
|
1057
|
+
self.prompt_base = _target_name(target)
|
964
1058
|
|
965
1059
|
self.cwd = None
|
966
1060
|
self.chdir("/")
|
967
1061
|
|
968
1062
|
@property
|
969
1063
|
def prompt(self) -> str:
|
970
|
-
return f"{self.prompt_base}
|
1064
|
+
return f"(config tree) {self.prompt_base}:{self.cwd}$ "
|
971
1065
|
|
972
1066
|
def check_compatible(target: Target) -> bool:
|
973
1067
|
return target.has_function("etc")
|
974
1068
|
|
975
|
-
def resolve_path(self, path:
|
1069
|
+
def resolve_path(self, path: str | fsutil.TargetPath | None) -> fsutil.TargetPath:
|
976
1070
|
if not path:
|
977
1071
|
return self.cwd
|
978
1072
|
|
@@ -1006,12 +1100,12 @@ class UnixConfigTreeCli(TargetCli):
|
|
1006
1100
|
class RegistryCli(TargetCmd):
|
1007
1101
|
"""CLI for browsing the registry."""
|
1008
1102
|
|
1009
|
-
def __init__(self, target: Target, registry:
|
1010
|
-
|
1011
|
-
self.registry = registry or target.registry
|
1103
|
+
def __init__(self, target: Target, registry: regutil.RegfHive | None = None):
|
1104
|
+
self.prompt_base = _target_name(target)
|
1012
1105
|
|
1013
|
-
self
|
1106
|
+
TargetCmd.__init__(self, target)
|
1014
1107
|
|
1108
|
+
self.registry = registry or target.registry
|
1015
1109
|
self.cwd = None
|
1016
1110
|
self.chdir("\\")
|
1017
1111
|
|
@@ -1024,8 +1118,7 @@ class RegistryCli(TargetCmd):
|
|
1024
1118
|
|
1025
1119
|
@property
|
1026
1120
|
def prompt(self) -> str:
|
1027
|
-
|
1028
|
-
return f"{self.prompt_base}/registry {prompt_end}> "
|
1121
|
+
return "(registry) " + self.prompt_ps1.format(base=self.prompt_base, cwd=self.cwd)
|
1029
1122
|
|
1030
1123
|
def completedefault(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
|
1031
1124
|
path = line[:begidx].rsplit(" ")[-1]
|
@@ -1065,9 +1158,7 @@ class RegistryCli(TargetCmd):
|
|
1065
1158
|
if self.check_key(path):
|
1066
1159
|
self.cwd = "\\" + path.strip("\\")
|
1067
1160
|
|
1068
|
-
def scandir(
|
1069
|
-
self, path: str, color: bool = False
|
1070
|
-
) -> list[tuple[Union[regutil.RegistryKey, regutil.RegistryValue], str]]:
|
1161
|
+
def scandir(self, path: str, color: bool = False) -> list[tuple[regutil.RegistryKey | regutil.RegistryValue, str]]:
|
1071
1162
|
try:
|
1072
1163
|
key = self.resolve_key(path)
|
1073
1164
|
except RegistryError:
|
@@ -1083,56 +1174,95 @@ class RegistryCli(TargetCmd):
|
|
1083
1174
|
r.sort(key=lambda e: e[0].name)
|
1084
1175
|
return r
|
1085
1176
|
|
1086
|
-
def do_cd(self, line: str) ->
|
1177
|
+
def do_cd(self, line: str) -> bool:
|
1087
1178
|
"""change subkey"""
|
1179
|
+
if line == "..":
|
1180
|
+
try:
|
1181
|
+
self.resolve_key(self.cwd + "\\..")
|
1182
|
+
except RegistryError:
|
1183
|
+
self.do_up(line)
|
1184
|
+
return False
|
1185
|
+
|
1088
1186
|
self.chdir(line)
|
1187
|
+
return False
|
1089
1188
|
|
1090
|
-
def do_up(self, line: str) ->
|
1189
|
+
def do_up(self, line: str) -> bool:
|
1091
1190
|
"""go up a subkey"""
|
1092
1191
|
parent = self.cwd.rpartition("\\")[0]
|
1093
1192
|
if not parent:
|
1094
1193
|
parent = "\\"
|
1095
1194
|
self.chdir(parent)
|
1195
|
+
return False
|
1096
1196
|
|
1097
|
-
def do_pwd(self, line: str) ->
|
1197
|
+
def do_pwd(self, line: str) -> bool:
|
1098
1198
|
"""print current path"""
|
1099
|
-
print(self.cwd)
|
1199
|
+
print(self.cwd.lstrip("\\"))
|
1200
|
+
return False
|
1100
1201
|
|
1101
|
-
def do_recommend(self, line: str) ->
|
1202
|
+
def do_recommend(self, line: str) -> bool:
|
1102
1203
|
"""recommend a key"""
|
1103
1204
|
print(random.choice([name for _, name in self.scandir(None)]))
|
1205
|
+
return False
|
1104
1206
|
|
1105
1207
|
@arg("path", nargs="?")
|
1106
|
-
def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) ->
|
1208
|
+
def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
1107
1209
|
key = self.check_key(args.path)
|
1108
1210
|
if not key:
|
1109
|
-
return
|
1211
|
+
return False
|
1110
1212
|
|
1111
1213
|
r = self.scandir(key, color=True)
|
1112
1214
|
print("\n".join([name for _, name in r]), file=stdout)
|
1215
|
+
return False
|
1113
1216
|
|
1114
1217
|
@arg("value")
|
1115
|
-
def cmd_cat(self, args: argparse.Namespace, stdout: TextIO) ->
|
1218
|
+
def cmd_cat(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
1116
1219
|
value = self.check_value(args.value)
|
1117
1220
|
if not value:
|
1118
|
-
return
|
1221
|
+
return False
|
1119
1222
|
|
1120
1223
|
print(repr(value.value), file=stdout)
|
1224
|
+
return False
|
1225
|
+
|
1226
|
+
@arg("value")
|
1227
|
+
@arg("-p", "--hex", action="store_true")
|
1228
|
+
@alias("xxd")
|
1229
|
+
def cmd_hexdump(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
1230
|
+
value = self.check_value(args.value)
|
1231
|
+
if not value:
|
1232
|
+
return False
|
1121
1233
|
|
1234
|
+
if args.hex:
|
1235
|
+
print(value.value.hex(), file=stdout)
|
1236
|
+
else:
|
1237
|
+
print(hexdump(value.value, output="string"), file=stdout)
|
1122
1238
|
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1239
|
+
return False
|
1240
|
+
|
1241
|
+
|
1242
|
+
def arg_str_to_arg_list(args: str) -> list[str]:
|
1243
|
+
"""Convert a commandline string to a list of command line arguments."""
|
1244
|
+
lexer = shlex.shlex(args, posix=True, punctuation_chars=True)
|
1245
|
+
lexer.wordchars += "$"
|
1246
|
+
lexer.whitespace_split = True
|
1247
|
+
return list(lexer)
|
1129
1248
|
|
1130
|
-
try:
|
1131
|
-
return LS_COLORS[fsutil.splitext(name)[1]].format(name)
|
1132
|
-
except KeyError:
|
1133
|
-
pass
|
1134
1249
|
|
1135
|
-
|
1250
|
+
def extend_args(args: argparse.Namespace, func: Callable) -> argparse.Namespace:
|
1251
|
+
"""Extend the arguments of the given ``func`` with the provided ``argparse.Namespace``."""
|
1252
|
+
for short, kwargs in func.__args__:
|
1253
|
+
name = kwargs.get("dest", short[-1]).lstrip("-").replace("-", "_")
|
1254
|
+
if not hasattr(args, name):
|
1255
|
+
setattr(args, name, None)
|
1256
|
+
return args
|
1257
|
+
|
1258
|
+
|
1259
|
+
def _target_name(target: Target) -> str:
|
1260
|
+
"""Return a target name for cmd.Cmd base prompts."""
|
1261
|
+
|
1262
|
+
if target.has_function("domain") and target.domain:
|
1263
|
+
return f"{target.name}.{target.domain}"
|
1264
|
+
|
1265
|
+
return target.name
|
1136
1266
|
|
1137
1267
|
|
1138
1268
|
@contextmanager
|
@@ -1200,15 +1330,7 @@ def build_pipe_stdout(pipe_parts: list[str]) -> Iterator[TextIO]:
|
|
1200
1330
|
yield pipe_stdin
|
1201
1331
|
|
1202
1332
|
|
1203
|
-
def
|
1204
|
-
"""Helper method for generating a mode string from a numerical mode value."""
|
1205
|
-
is_dir = "d" if stat.S_ISDIR(st.st_mode) else "-"
|
1206
|
-
dic = {"7": "rwx", "6": "rw-", "5": "r-x", "4": "r--", "0": "---"}
|
1207
|
-
perm = str(oct(st.st_mode)[-3:])
|
1208
|
-
return is_dir + "".join(dic.get(x, x) for x in perm)
|
1209
|
-
|
1210
|
-
|
1211
|
-
def open_shell(targets: list[Union[str, pathlib.Path]], python: bool, registry: bool) -> None:
|
1333
|
+
def open_shell(targets: list[str | pathlib.Path], python: bool, registry: bool) -> None:
|
1212
1334
|
"""Helper method for starting a regular, Python or registry shell for one or multiple targets."""
|
1213
1335
|
targets = list(Target.open_all(targets))
|
1214
1336
|
|
@@ -1246,7 +1368,7 @@ def python_shell(targets: list[Target]) -> None:
|
|
1246
1368
|
print()
|
1247
1369
|
|
1248
1370
|
|
1249
|
-
def create_cli(targets: list[Target], cli_cls: type[TargetCmd]) ->
|
1371
|
+
def create_cli(targets: list[Target], cli_cls: type[TargetCmd]) -> cmd.Cmd | None:
|
1250
1372
|
"""Helper method for instatiating the appropriate CLI."""
|
1251
1373
|
if len(targets) == 1:
|
1252
1374
|
target = targets[0]
|
@@ -1275,6 +1397,9 @@ def run_cli(cli: cmd.Cmd) -> None:
|
|
1275
1397
|
return
|
1276
1398
|
|
1277
1399
|
except KeyboardInterrupt:
|
1400
|
+
# Run postloop so the interrupted command is added to the history file
|
1401
|
+
cli.postloop()
|
1402
|
+
|
1278
1403
|
# Add a line when pressing ctrl+c, so the next one starts at a new line
|
1279
1404
|
print()
|
1280
1405
|
|
@@ -1286,6 +1411,8 @@ def run_cli(cli: cmd.Cmd) -> None:
|
|
1286
1411
|
print(f"*** Unhandled error: {e}")
|
1287
1412
|
print("If you wish to see the full debug trace, enable debug mode.")
|
1288
1413
|
|
1414
|
+
cli.postloop()
|
1415
|
+
|
1289
1416
|
|
1290
1417
|
@catch_sigpipe
|
1291
1418
|
def main() -> None:
|
@@ -1311,8 +1438,7 @@ def main() -> None:
|
|
1311
1438
|
args.targets = args_to_uri(args.targets, args.loader, rest) if args.loader else args.targets
|
1312
1439
|
process_generic_arguments(args)
|
1313
1440
|
|
1314
|
-
# For the shell tool we want -q to log slightly more then just CRITICAL
|
1315
|
-
# messages.
|
1441
|
+
# For the shell tool we want -q to log slightly more then just CRITICAL messages.
|
1316
1442
|
if args.quiet:
|
1317
1443
|
logging.getLogger("dissect").setLevel(level=logging.ERROR)
|
1318
1444
|
|