secator 0.1.1__py2.py3-none-any.whl → 0.3.0__py2.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.
Potentially problematic release.
This version of secator might be problematic. Click here for more details.
- secator/cli.py +441 -334
- secator/decorators.py +2 -2
- secator/definitions.py +64 -16
- secator/exporters/txt.py +1 -1
- secator/installer.py +192 -0
- secator/runners/command.py +27 -27
- secator/tasks/dalfox.py +1 -0
- secator/tasks/dnsx.py +1 -0
- secator/tasks/dnsxbrute.py +1 -0
- secator/tasks/feroxbuster.py +1 -0
- secator/tasks/ffuf.py +2 -2
- secator/tasks/gau.py +1 -0
- secator/tasks/gospider.py +1 -0
- secator/tasks/grype.py +1 -0
- secator/tasks/httpx.py +1 -0
- secator/tasks/katana.py +2 -1
- secator/tasks/mapcidr.py +1 -0
- secator/tasks/msfconsole.py +1 -1
- secator/tasks/naabu.py +2 -1
- secator/tasks/nuclei.py +1 -0
- secator/tasks/subfinder.py +1 -0
- secator/tasks/wpscan.py +1 -1
- secator/utils_test.py +1 -0
- {secator-0.1.1.dist-info → secator-0.3.0.dist-info}/METADATA +23 -1
- {secator-0.1.1.dist-info → secator-0.3.0.dist-info}/RECORD +28 -27
- {secator-0.1.1.dist-info → secator-0.3.0.dist-info}/WHEEL +0 -0
- {secator-0.1.1.dist-info → secator-0.3.0.dist-info}/entry_points.txt +0 -0
- {secator-0.1.1.dist-info → secator-0.3.0.dist-info}/licenses/LICENSE +0 -0
secator/cli.py
CHANGED
|
@@ -12,15 +12,15 @@ from rich.rule import Rule
|
|
|
12
12
|
|
|
13
13
|
from secator.config import ConfigLoader
|
|
14
14
|
from secator.decorators import OrderedGroup, register_runner
|
|
15
|
-
from secator.definitions import (ASCII, CVES_FOLDER, DATA_FOLDER,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
from secator.definitions import (ASCII, BUILD_ADDON_ENABLED, CVES_FOLDER, DATA_FOLDER, DEV_ADDON_ENABLED, # noqa: F401
|
|
16
|
+
DEV_PACKAGE, GOOGLE_ADDON_ENABLED, VERSION_LATEST, LIB_FOLDER, MONGODB_ADDON_ENABLED,
|
|
17
|
+
VERSION_OBSOLETE, OPT_NOT_SUPPORTED, PAYLOADS_FOLDER, REDIS_ADDON_ENABLED, REVSHELLS_FOLDER, ROOT_FOLDER,
|
|
18
|
+
TRACE_ADDON_ENABLED, VERSION, VERSION_STR, WORKER_ADDON_ENABLED)
|
|
19
|
+
from secator.installer import ToolInstaller
|
|
19
20
|
from secator.rich import console
|
|
20
21
|
from secator.runners import Command
|
|
21
22
|
from secator.serializers.dataclass import loads_dataclass
|
|
22
|
-
from secator.utils import
|
|
23
|
-
flatten, print_results_table)
|
|
23
|
+
from secator.utils import debug, detect_host, discover_tasks, find_list_item, flatten, print_results_table
|
|
24
24
|
|
|
25
25
|
click.rich_click.USE_RICH_MARKUP = True
|
|
26
26
|
|
|
@@ -28,10 +28,15 @@ ALL_TASKS = discover_tasks()
|
|
|
28
28
|
ALL_CONFIGS = ConfigLoader.load_all()
|
|
29
29
|
ALL_WORKFLOWS = ALL_CONFIGS.workflow
|
|
30
30
|
ALL_SCANS = ALL_CONFIGS.scan
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def print_version():
|
|
34
|
+
console.print(f'[bold gold3]Current version[/]: {VERSION}', highlight=False)
|
|
35
|
+
console.print(f'[bold gold3]Latest version[/]: {VERSION_LATEST}', highlight=False)
|
|
36
|
+
console.print(f'[bold gold3]Python binary[/]: {sys.executable}')
|
|
37
|
+
if DEV_PACKAGE:
|
|
38
|
+
console.print(f'[bold gold3]Root folder[/]: {ROOT_FOLDER}')
|
|
39
|
+
console.print(f'[bold gold3]Lib folder[/]: {LIB_FOLDER}')
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
#-----#
|
|
@@ -39,16 +44,18 @@ DEFAULT_CMD_OPTS = {
|
|
|
39
44
|
#-----#
|
|
40
45
|
|
|
41
46
|
@click.group(cls=OrderedGroup, invoke_without_command=True)
|
|
42
|
-
@click.option('--no-banner', '-nb', is_flag=True, default=False)
|
|
43
47
|
@click.option('--version', '-version', is_flag=True, default=False)
|
|
44
48
|
@click.pass_context
|
|
45
|
-
def cli(ctx,
|
|
49
|
+
def cli(ctx, version):
|
|
46
50
|
"""Secator CLI."""
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
console.print(ASCII, highlight=False)
|
|
52
|
+
if VERSION_OBSOLETE:
|
|
53
|
+
console.print(
|
|
54
|
+
'[bold red]:warning: secator version is outdated: '
|
|
55
|
+
f'run "secator update" to install the newest version ({VERSION_LATEST}).\n')
|
|
49
56
|
if ctx.invoked_subcommand is None:
|
|
50
57
|
if version:
|
|
51
|
-
|
|
58
|
+
print_version()
|
|
52
59
|
else:
|
|
53
60
|
ctx.get_help()
|
|
54
61
|
|
|
@@ -142,11 +149,329 @@ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
|
|
|
142
149
|
if reload:
|
|
143
150
|
patterns = "celery.py;tasks/*.py;runners/*.py;serializers/*.py;output_types/*.py;hooks/*.py;exporters/*.py"
|
|
144
151
|
cmd = f'watchmedo auto-restart --directory=./ --patterns="{patterns}" --recursive -- {cmd}'
|
|
145
|
-
Command.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
Command.execute(cmd, name='secator worker')
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
#-------#
|
|
156
|
+
# UTILS #
|
|
157
|
+
#-------#
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@cli.group(aliases=['u'])
|
|
161
|
+
def util():
|
|
162
|
+
"""Run a utility."""
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@util.command()
|
|
167
|
+
@click.option('--timeout', type=float, default=0.2, help='Proxy timeout (in seconds)')
|
|
168
|
+
@click.option('--number', '-n', type=int, default=1, help='Number of proxies')
|
|
169
|
+
def proxy(timeout, number):
|
|
170
|
+
"""Get random proxies from FreeProxy."""
|
|
171
|
+
proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
|
|
172
|
+
for _ in range(number):
|
|
173
|
+
url = proxy.get()
|
|
174
|
+
print(url)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@util.command()
|
|
178
|
+
@click.argument('name', type=str, default=None, required=False)
|
|
179
|
+
@click.option('--host', '-h', type=str, default=None, help='Specify LHOST for revshell, otherwise autodetected.')
|
|
180
|
+
@click.option('--port', '-p', type=int, default=9001, show_default=True, help='Specify PORT for revshell')
|
|
181
|
+
@click.option('--interface', '-i', type=str, help='Interface to use to detect IP')
|
|
182
|
+
@click.option('--listen', '-l', is_flag=True, default=False, help='Spawn netcat listener on specified port')
|
|
183
|
+
@click.option('--force', is_flag=True)
|
|
184
|
+
def revshell(name, host, port, interface, listen, force):
|
|
185
|
+
"""Show reverse shell source codes and run netcat listener (-l)."""
|
|
186
|
+
if host is None: # detect host automatically
|
|
187
|
+
host = detect_host(interface)
|
|
188
|
+
if not host:
|
|
189
|
+
console.print(
|
|
190
|
+
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces.',
|
|
191
|
+
style='bold red')
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Download reverse shells JSON from repo
|
|
195
|
+
revshells_json = f'{REVSHELLS_FOLDER}/revshells.json'
|
|
196
|
+
if not os.path.exists(revshells_json) or force:
|
|
197
|
+
ret = Command.execute(
|
|
198
|
+
f'wget https://raw.githubusercontent.com/freelabz/secator/main/scripts/revshells.json && mv revshells.json {REVSHELLS_FOLDER}', # noqa: E501
|
|
199
|
+
cls_attributes={'shell': True}
|
|
200
|
+
)
|
|
201
|
+
if not ret.return_code == 0:
|
|
202
|
+
sys.exit(1)
|
|
203
|
+
|
|
204
|
+
# Parse JSON into shells
|
|
205
|
+
with open(revshells_json) as f:
|
|
206
|
+
shells = json.loads(f.read())
|
|
207
|
+
for sh in shells:
|
|
208
|
+
sh['alias'] = '_'.join(sh['name'].lower()
|
|
209
|
+
.replace('-c', '')
|
|
210
|
+
.replace('-e', '')
|
|
211
|
+
.replace('-i', '')
|
|
212
|
+
.replace('c#', 'cs')
|
|
213
|
+
.replace('#', '')
|
|
214
|
+
.replace('(', '')
|
|
215
|
+
.replace(')', '')
|
|
216
|
+
.strip()
|
|
217
|
+
.split(' ')).replace('_1', '')
|
|
218
|
+
cmd = re.sub(r"\s\s+", "", sh.get('command', ''), flags=re.UNICODE)
|
|
219
|
+
cmd = cmd.replace('\n', ' ')
|
|
220
|
+
sh['cmd_short'] = (cmd[:30] + '..') if len(cmd) > 30 else cmd
|
|
221
|
+
|
|
222
|
+
shell = [
|
|
223
|
+
shell for shell in shells if shell['name'] == name or shell['alias'] == name
|
|
224
|
+
]
|
|
225
|
+
if not shell:
|
|
226
|
+
console.print('Available shells:', style='bold yellow')
|
|
227
|
+
shells_str = [
|
|
228
|
+
'[bold magenta]{alias:<20}[/][dim white]{name:<20}[/][dim gold3]{cmd_short:<20}[/]'.format(**sh)
|
|
229
|
+
for sh in shells
|
|
230
|
+
]
|
|
231
|
+
console.print('\n'.join(shells_str))
|
|
232
|
+
else:
|
|
233
|
+
shell = shell[0]
|
|
234
|
+
command = shell['command']
|
|
235
|
+
alias = shell['alias']
|
|
236
|
+
name = shell['name']
|
|
237
|
+
command_str = Template(command).render(ip=host, port=port, shell='bash')
|
|
238
|
+
console.print(Rule(f'[bold gold3]{alias}[/] - [bold red]{name} REMOTE SHELL', style='bold red', align='left'))
|
|
239
|
+
lang = shell.get('lang') or 'sh'
|
|
240
|
+
if len(command.splitlines()) == 1:
|
|
241
|
+
console.print()
|
|
242
|
+
print(f'\033[0;36m{command_str}')
|
|
243
|
+
else:
|
|
244
|
+
md = Markdown(f'```{lang}\n{command_str}\n```')
|
|
245
|
+
console.print(md)
|
|
246
|
+
console.print(f'Save this script as rev.{lang} and run it on your target', style='dim italic')
|
|
247
|
+
console.print()
|
|
248
|
+
console.print(Rule(style='bold red'))
|
|
249
|
+
|
|
250
|
+
if listen:
|
|
251
|
+
console.print(f'Starting netcat listener on port {port} ...', style='bold gold3')
|
|
252
|
+
cmd = f'nc -lvnp {port}'
|
|
253
|
+
Command.execute(cmd)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@util.command()
|
|
257
|
+
@click.option('--directory', '-d', type=str, default=PAYLOADS_FOLDER, show_default=True, help='HTTP server directory')
|
|
258
|
+
@click.option('--host', '-h', type=str, default=None, help='HTTP host')
|
|
259
|
+
@click.option('--port', '-p', type=int, default=9001, help='HTTP server port')
|
|
260
|
+
@click.option('--interface', '-i', type=str, default=None, help='Interface to use to auto-detect host IP')
|
|
261
|
+
def serve(directory, host, port, interface):
|
|
262
|
+
"""Run HTTP server to serve payloads."""
|
|
263
|
+
LSE_URL = 'https://github.com/diego-treitos/linux-smart-enumeration/releases/latest/download/lse.sh'
|
|
264
|
+
LINPEAS_URL = 'https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh'
|
|
265
|
+
SUDOKILLER_URL = 'https://raw.githubusercontent.com/TH3xACE/SUDO_KILLER/V3/SUDO_KILLERv3.sh'
|
|
266
|
+
PAYLOADS = [
|
|
267
|
+
{
|
|
268
|
+
'fname': 'lse.sh',
|
|
269
|
+
'description': 'Linux Smart Enumeration',
|
|
270
|
+
'command': f'wget {LSE_URL} -O lse.sh && chmod 700 lse.sh'
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
'fname': 'linpeas.sh',
|
|
274
|
+
'description': 'Linux Privilege Escalation Awesome Script',
|
|
275
|
+
'command': f'wget {LINPEAS_URL} -O linpeas.sh && chmod 700 linpeas.sh'
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
'fname': 'sudo_killer.sh',
|
|
279
|
+
'description': 'SUDO_KILLER',
|
|
280
|
+
'command': f'wget {SUDOKILLER_URL} -O sudo_killer.sh && chmod 700 sudo_killer.sh'
|
|
281
|
+
}
|
|
282
|
+
]
|
|
283
|
+
for ix, payload in enumerate(PAYLOADS):
|
|
284
|
+
descr = payload.get('description', '')
|
|
285
|
+
fname = payload['fname']
|
|
286
|
+
if not os.path.exists(f'{directory}/{fname}'):
|
|
287
|
+
with console.status(f'[bold yellow][{ix}/{len(PAYLOADS)}] Downloading {fname} [dim]({descr})[/] ...[/]'):
|
|
288
|
+
cmd = payload['command']
|
|
289
|
+
console.print(f'[bold magenta]{fname} [dim]({descr})[/] ...[/]', )
|
|
290
|
+
Command.execute(cmd, cls_attributes={'shell': True}, cwd=directory)
|
|
291
|
+
console.print()
|
|
292
|
+
|
|
293
|
+
console.print(Rule())
|
|
294
|
+
console.print(f'Available payloads in {directory}: ', style='bold yellow')
|
|
295
|
+
for fname in os.listdir(directory):
|
|
296
|
+
if not host:
|
|
297
|
+
host = detect_host(interface)
|
|
298
|
+
if not host:
|
|
299
|
+
console.print(
|
|
300
|
+
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.',
|
|
301
|
+
style='bold red')
|
|
302
|
+
return
|
|
303
|
+
payload = find_list_item(PAYLOADS, fname, key='fname', default={})
|
|
304
|
+
fdescr = payload.get('description', 'No description')
|
|
305
|
+
console.print(f'{fname} [dim]({fdescr})[/]', style='bold magenta')
|
|
306
|
+
console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
|
|
307
|
+
console.print('')
|
|
308
|
+
console.print(Rule())
|
|
309
|
+
console.print(f'Started HTTP server on port {port}, waiting for incoming connections ...', style='bold yellow')
|
|
310
|
+
Command.execute(f'{sys.executable} -m http.server {port}', cwd=directory)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@util.command()
|
|
314
|
+
@click.argument('record_name', type=str, default=None)
|
|
315
|
+
@click.option('--script', '-s', type=str, default=None, help='Script to run. See scripts/stories/ for examples.')
|
|
316
|
+
@click.option('--interactive', '-i', is_flag=True, default=False, help='Interactive record.')
|
|
317
|
+
@click.option('--width', '-w', type=int, default=None, help='Recording width')
|
|
318
|
+
@click.option('--height', '-h', type=int, default=None, help='Recording height')
|
|
319
|
+
@click.option('--output-dir', type=str, default=f'{ROOT_FOLDER}/images')
|
|
320
|
+
def record(record_name, script, interactive, width, height, output_dir):
|
|
321
|
+
"""Record secator session using asciinema."""
|
|
322
|
+
# 120 x 30 is a good ratio for GitHub
|
|
323
|
+
width = width or console.size.width
|
|
324
|
+
height = height or console.size.height
|
|
325
|
+
attrs = {
|
|
326
|
+
'shell': False,
|
|
327
|
+
'env': {
|
|
328
|
+
'RECORD': '1',
|
|
329
|
+
'LINES': str(height),
|
|
330
|
+
'PS1': '$ ',
|
|
331
|
+
'COLUMNS': str(width),
|
|
332
|
+
'TERM': 'xterm-256color'
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
output_cast_path = f'{output_dir}/{record_name}.cast'
|
|
336
|
+
output_gif_path = f'{output_dir}/{record_name}.gif'
|
|
337
|
+
|
|
338
|
+
# Run automated 'story' script with asciinema-automation
|
|
339
|
+
if script:
|
|
340
|
+
# If existing cast file, remove it
|
|
341
|
+
if os.path.exists(output_cast_path):
|
|
342
|
+
os.unlink(output_cast_path)
|
|
343
|
+
console.print(f'Removed existing {output_cast_path}', style='bold green')
|
|
344
|
+
|
|
345
|
+
with console.status('[bold gold3]Recording with asciinema ...[/]'):
|
|
346
|
+
Command.execute(
|
|
347
|
+
f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
|
|
348
|
+
cls_attributes=attrs,
|
|
349
|
+
raw=True,
|
|
350
|
+
)
|
|
351
|
+
console.print(f'Generated {output_cast_path}', style='bold green')
|
|
352
|
+
elif interactive:
|
|
353
|
+
os.environ.update(attrs['env'])
|
|
354
|
+
Command.execute(f'asciinema rec -c /bin/bash --stdin --overwrite {output_cast_path}')
|
|
355
|
+
|
|
356
|
+
# Resize cast file
|
|
357
|
+
if os.path.exists(output_cast_path):
|
|
358
|
+
with console.status('[bold gold3]Cleaning up .cast and set custom settings ...'):
|
|
359
|
+
with open(output_cast_path, 'r') as f:
|
|
360
|
+
lines = f.readlines()
|
|
361
|
+
updated_lines = []
|
|
362
|
+
for ix, line in enumerate(lines):
|
|
363
|
+
tmp_line = json.loads(line)
|
|
364
|
+
if ix == 0:
|
|
365
|
+
tmp_line['width'] = width
|
|
366
|
+
tmp_line['height'] = height
|
|
367
|
+
tmp_line['env']['SHELL'] = '/bin/sh'
|
|
368
|
+
lines[0] = json.dumps(tmp_line) + '\n'
|
|
369
|
+
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
370
|
+
elif tmp_line[2].endswith(' \r'):
|
|
371
|
+
tmp_line[2] = tmp_line[2].replace(' \r', '')
|
|
372
|
+
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
373
|
+
else:
|
|
374
|
+
updated_lines.append(line)
|
|
375
|
+
with open(output_cast_path, 'w') as f:
|
|
376
|
+
f.writelines(updated_lines)
|
|
377
|
+
console.print('')
|
|
378
|
+
|
|
379
|
+
# Edit cast file to reduce long timeouts
|
|
380
|
+
with console.status('[bold gold3] Editing cast file to reduce long commands ...'):
|
|
381
|
+
Command.execute(
|
|
382
|
+
f'asciinema-edit quantize --range 1 {output_cast_path} --out {output_cast_path}.tmp',
|
|
383
|
+
cls_attributes=attrs,
|
|
384
|
+
raw=True,
|
|
385
|
+
)
|
|
386
|
+
if os.path.exists(f'{output_cast_path}.tmp'):
|
|
387
|
+
os.replace(f'{output_cast_path}.tmp', output_cast_path)
|
|
388
|
+
console.print(f'Edited {output_cast_path}', style='bold green')
|
|
389
|
+
|
|
390
|
+
# Convert to GIF
|
|
391
|
+
with console.status(f'[bold gold3]Converting to {output_gif_path} ...[/]'):
|
|
392
|
+
Command.execute(
|
|
393
|
+
f'agg {output_cast_path} {output_gif_path}',
|
|
394
|
+
cls_attributes=attrs,
|
|
395
|
+
)
|
|
396
|
+
console.print(f'Generated {output_gif_path}', style='bold green')
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@util.group('build')
|
|
400
|
+
def build():
|
|
401
|
+
"""Build secator."""
|
|
402
|
+
if not DEV_PACKAGE:
|
|
403
|
+
console.print('[bold red]You MUST use a development version of secator to make builds.[/]')
|
|
404
|
+
sys.exit(1)
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@build.command('pypi')
|
|
409
|
+
def build_pypi():
|
|
410
|
+
"""Build secator PyPI package."""
|
|
411
|
+
if not BUILD_ADDON_ENABLED:
|
|
412
|
+
console.print('[bold red]Missing build addon: please run `secator install addons build`')
|
|
413
|
+
sys.exit(1)
|
|
414
|
+
with console.status('[bold gold3]Building PyPI package...[/]'):
|
|
415
|
+
ret = Command.execute(f'{sys.executable} -m hatch build', name='hatch build', cwd=ROOT_FOLDER)
|
|
416
|
+
sys.exit(ret.return_code)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@build.command('docker')
|
|
420
|
+
@click.option('--tag', '-t', type=str, default=None, help='Specific tag')
|
|
421
|
+
@click.option('--latest', '-l', is_flag=True, default=False, help='Latest tag')
|
|
422
|
+
def build_docker(tag, latest):
|
|
423
|
+
"""Build secator Docker image."""
|
|
424
|
+
if not tag:
|
|
425
|
+
tag = VERSION if latest else 'dev'
|
|
426
|
+
cmd = f'docker build -t freelabz/secator:{tag}'
|
|
427
|
+
if latest:
|
|
428
|
+
cmd += ' -t freelabz/secator:latest'
|
|
429
|
+
cmd += ' .'
|
|
430
|
+
with console.status('[bold gold3]Building Docker image...[/]'):
|
|
431
|
+
ret = Command.execute(cmd, name='docker build', cwd=ROOT_FOLDER)
|
|
432
|
+
sys.exit(ret.return_code)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@util.group('publish')
|
|
436
|
+
def publish():
|
|
437
|
+
"""Publish secator."""
|
|
438
|
+
if not DEV_PACKAGE:
|
|
439
|
+
console.print('[bold red]You MUST use a development version of secator to publish builds.[/]')
|
|
440
|
+
sys.exit(1)
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@publish.command('pypi')
|
|
445
|
+
def publish_pypi():
|
|
446
|
+
"""Publish secator PyPI package."""
|
|
447
|
+
if not BUILD_ADDON_ENABLED:
|
|
448
|
+
console.print('[bold red]Missing build addon: please run `secator install addons build`')
|
|
449
|
+
sys.exit(1)
|
|
450
|
+
os.environ['HATCH_INDEX_USER'] = '__token__'
|
|
451
|
+
hatch_token = os.environ.get('HATCH_INDEX_AUTH')
|
|
452
|
+
if not hatch_token:
|
|
453
|
+
console.print('[bold red]Missing PyPI auth token (HATCH_INDEX_AUTH env variable).')
|
|
454
|
+
sys.exit(1)
|
|
455
|
+
with console.status('[bold gold3]Publishing PyPI package...[/]'):
|
|
456
|
+
ret = Command.execute(f'{sys.executable} -m hatch publish', name='hatch publish', cwd=ROOT_FOLDER)
|
|
457
|
+
sys.exit(ret.return_code)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@publish.command('docker')
|
|
461
|
+
@click.option('--tag', '-t', default=None, help='Specific tag')
|
|
462
|
+
@click.option('--latest', '-l', is_flag=True, default=False, help='Latest tag')
|
|
463
|
+
def publish_docker(tag, latest):
|
|
464
|
+
"""Publish secator Docker image."""
|
|
465
|
+
if not tag:
|
|
466
|
+
tag = VERSION if latest else 'dev'
|
|
467
|
+
cmd = f'docker push freelabz/secator:{tag}'
|
|
468
|
+
cmd2 = 'docker push freelabz/secator:latest'
|
|
469
|
+
with console.status(f'[bold gold3]Publishing Docker image {tag}...[/]'):
|
|
470
|
+
ret = Command.execute(cmd, name=f'docker push ({tag})', cwd=ROOT_FOLDER)
|
|
471
|
+
if latest:
|
|
472
|
+
ret2 = Command.execute(cmd2, name='docker push (latest)')
|
|
473
|
+
sys.exit(max(ret.return_code, ret2.return_code))
|
|
474
|
+
sys.exit(ret.return_code)
|
|
150
475
|
|
|
151
476
|
|
|
152
477
|
#--------#
|
|
@@ -156,7 +481,7 @@ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
|
|
|
156
481
|
|
|
157
482
|
@cli.group(aliases=['r'])
|
|
158
483
|
def report():
|
|
159
|
-
"""
|
|
484
|
+
"""View previous reports."""
|
|
160
485
|
pass
|
|
161
486
|
|
|
162
487
|
|
|
@@ -211,14 +536,10 @@ def which(command):
|
|
|
211
536
|
Returns:
|
|
212
537
|
secator.Command: Command instance.
|
|
213
538
|
"""
|
|
214
|
-
return Command.
|
|
215
|
-
f'which {command}',
|
|
216
|
-
quiet=True,
|
|
217
|
-
print_errors=False
|
|
218
|
-
)
|
|
539
|
+
return Command.execute(f'which {command}', quiet=True, print_errors=False)
|
|
219
540
|
|
|
220
541
|
|
|
221
|
-
def
|
|
542
|
+
def get_version_cls(cls):
|
|
222
543
|
"""Get version for a Command.
|
|
223
544
|
|
|
224
545
|
Args:
|
|
@@ -245,27 +566,23 @@ def get_version(version_cmd):
|
|
|
245
566
|
str: Version string.
|
|
246
567
|
"""
|
|
247
568
|
regex = r'[0-9]+\.[0-9]+\.?[0-9]*\.?[a-zA-Z]*'
|
|
248
|
-
ret = Command.
|
|
249
|
-
version_cmd,
|
|
250
|
-
quiet=True,
|
|
251
|
-
print_errors=False
|
|
252
|
-
)
|
|
569
|
+
ret = Command.execute(version_cmd, quiet=True, print_errors=False)
|
|
253
570
|
match = re.findall(regex, ret.output)
|
|
254
571
|
if not match:
|
|
255
572
|
return 'n/a'
|
|
256
573
|
return match[0]
|
|
257
574
|
|
|
258
575
|
|
|
259
|
-
@cli.command(name='health'
|
|
576
|
+
@cli.command(name='health')
|
|
260
577
|
@click.option('--json', '-json', is_flag=True, default=False, help='JSON lines output')
|
|
261
578
|
@click.option('--debug', '-debug', is_flag=True, default=False, help='Debug health output')
|
|
262
579
|
def health(json, debug):
|
|
263
|
-
"""
|
|
580
|
+
"""[dim]Get health status.[/]"""
|
|
264
581
|
tools = [cls for cls in ALL_TASKS]
|
|
265
582
|
status = {'tools': {}, 'languages': {}, 'secator': {}}
|
|
266
583
|
|
|
267
584
|
def print_status(cmd, return_code, version=None, bin=None, category=None):
|
|
268
|
-
s = '[bold green]ok
|
|
585
|
+
s = '[bold green]ok [/]' if return_code == 0 else '[bold red]missing [/]'
|
|
269
586
|
s = f'[bold magenta]{cmd:<15}[/] {s} '
|
|
270
587
|
if return_code == 0 and version:
|
|
271
588
|
if version == 'N/A':
|
|
@@ -279,35 +596,49 @@ def health(json, debug):
|
|
|
279
596
|
console.print(s, highlight=False)
|
|
280
597
|
|
|
281
598
|
# Check secator
|
|
282
|
-
|
|
599
|
+
if not json:
|
|
600
|
+
console.print(':wrench: [bold gold3]Checking secator ...[/]')
|
|
283
601
|
ret = which('secator')
|
|
284
602
|
if not json:
|
|
285
603
|
print_status('secator', ret.return_code, VERSION, ret.output, None)
|
|
286
|
-
status['secator'] = {'installed': ret.return_code == 0}
|
|
604
|
+
status['secator'] = {'installed': ret.return_code == 0, 'version': VERSION}
|
|
287
605
|
|
|
288
606
|
# Check languages
|
|
289
|
-
|
|
290
|
-
|
|
607
|
+
if not json:
|
|
608
|
+
console.print('\n:wrench: [bold gold3]Checking installed languages ...[/]')
|
|
609
|
+
version_cmds = {'go': 'version', 'python3': '--version', 'ruby': '--version'}
|
|
291
610
|
for lang, version_flag in version_cmds.items():
|
|
292
611
|
ret = which(lang)
|
|
293
|
-
|
|
612
|
+
version = get_version(f'{lang} {version_flag}')
|
|
294
613
|
if not json:
|
|
295
|
-
print_status(lang, ret.return_code,
|
|
296
|
-
status['languages'][lang] = {'installed': ret.return_code == 0}
|
|
614
|
+
print_status(lang, ret.return_code, version, ret.output, 'langs')
|
|
615
|
+
status['languages'][lang] = {'installed': ret.return_code == 0, 'version': version}
|
|
297
616
|
|
|
298
617
|
# Check tools
|
|
299
|
-
|
|
618
|
+
if not json:
|
|
619
|
+
console.print('\n:wrench: [bold gold3]Checking installed tools ...[/]')
|
|
300
620
|
for tool in tools:
|
|
301
621
|
cmd = tool.cmd.split(' ')[0]
|
|
302
622
|
ret = which(cmd)
|
|
303
|
-
|
|
623
|
+
version = get_version_cls(tool)
|
|
304
624
|
if not json:
|
|
305
|
-
print_status(tool.__name__, ret.return_code,
|
|
306
|
-
status['tools'][tool.__name__] = {'installed': ret.return_code == 0}
|
|
625
|
+
print_status(tool.__name__, ret.return_code, version, ret.output, 'tools')
|
|
626
|
+
status['tools'][tool.__name__] = {'installed': ret.return_code == 0, 'version': version}
|
|
627
|
+
|
|
628
|
+
# Check addons
|
|
629
|
+
if not json:
|
|
630
|
+
console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]')
|
|
631
|
+
for addon in ['google', 'mongodb', 'redis', 'dev', 'trace', 'build']:
|
|
632
|
+
addon_var = globals()[f'{addon.upper()}_ADDON_ENABLED']
|
|
633
|
+
ret = 0 if addon_var == 1 else 1
|
|
634
|
+
bin = None if addon_var == 0 else ' '
|
|
635
|
+
if not json:
|
|
636
|
+
print_status(addon, ret, 'N/A', bin, 'addons')
|
|
307
637
|
|
|
308
638
|
# Print JSON health
|
|
309
639
|
if json:
|
|
310
|
-
|
|
640
|
+
import json as _json
|
|
641
|
+
print(_json.dumps(status))
|
|
311
642
|
|
|
312
643
|
#---------#
|
|
313
644
|
# INSTALL #
|
|
@@ -316,12 +647,7 @@ def health(json, debug):
|
|
|
316
647
|
|
|
317
648
|
def run_install(cmd, title, next_steps=None):
|
|
318
649
|
with console.status(f'[bold yellow] Installing {title}...'):
|
|
319
|
-
ret = Command.
|
|
320
|
-
cmd,
|
|
321
|
-
cls_attributes={'shell': True},
|
|
322
|
-
print_cmd=True,
|
|
323
|
-
print_line=True
|
|
324
|
-
)
|
|
650
|
+
ret = Command.execute(cmd, cls_attributes={'shell': True}, print_cmd=True, print_line=True)
|
|
325
651
|
if ret.return_code != 0:
|
|
326
652
|
console.print(f':exclamation_mark: Failed to install {title}.', style='bold red')
|
|
327
653
|
else:
|
|
@@ -333,9 +659,9 @@ def run_install(cmd, title, next_steps=None):
|
|
|
333
659
|
sys.exit(ret.return_code)
|
|
334
660
|
|
|
335
661
|
|
|
336
|
-
@cli.group(
|
|
662
|
+
@cli.group()
|
|
337
663
|
def install():
|
|
338
|
-
"
|
|
664
|
+
"""[dim]Install langs, tools and addons.[/]"""
|
|
339
665
|
pass
|
|
340
666
|
|
|
341
667
|
|
|
@@ -349,7 +675,7 @@ def addons():
|
|
|
349
675
|
def install_worker():
|
|
350
676
|
"Install worker addon."
|
|
351
677
|
run_install(
|
|
352
|
-
cmd=f'{sys.executable} -m pip install
|
|
678
|
+
cmd=f'{sys.executable} -m pip install .[worker]',
|
|
353
679
|
title='worker addon',
|
|
354
680
|
next_steps=[
|
|
355
681
|
'Run "secator worker" to run a Celery worker using the file system as a backend and broker.',
|
|
@@ -363,7 +689,7 @@ def install_worker():
|
|
|
363
689
|
def install_google():
|
|
364
690
|
"Install google addon."
|
|
365
691
|
run_install(
|
|
366
|
-
cmd=f'{sys.executable} -m pip install
|
|
692
|
+
cmd=f'{sys.executable} -m pip install .[google]',
|
|
367
693
|
title='google addon',
|
|
368
694
|
next_steps=[
|
|
369
695
|
'Set the "GOOGLE_CREDENTIALS_PATH" and "GOOGLE_DRIVE_PARENT_FOLDER_ID" environment variables.',
|
|
@@ -376,7 +702,7 @@ def install_google():
|
|
|
376
702
|
def install_mongodb():
|
|
377
703
|
"Install mongodb addon."
|
|
378
704
|
run_install(
|
|
379
|
-
cmd=f'{sys.executable} -m pip install
|
|
705
|
+
cmd=f'{sys.executable} -m pip install .[mongodb]',
|
|
380
706
|
title='mongodb addon',
|
|
381
707
|
next_steps=[
|
|
382
708
|
'[dim]\[optional][/] Run "docker run --name mongo -p 27017:27017 -d mongo:latest" to run a local MongoDB instance.',
|
|
@@ -390,7 +716,7 @@ def install_mongodb():
|
|
|
390
716
|
def install_redis():
|
|
391
717
|
"Install redis addon."
|
|
392
718
|
run_install(
|
|
393
|
-
cmd=f'{sys.executable} -m pip install
|
|
719
|
+
cmd=f'{sys.executable} -m pip install .[redis]',
|
|
394
720
|
title='redis addon',
|
|
395
721
|
next_steps=[
|
|
396
722
|
'[dim]\[optional][/] Run "docker run --name redis -p 6379:6379 -d redis" to run a local Redis instance.',
|
|
@@ -416,6 +742,34 @@ def install_dev():
|
|
|
416
742
|
)
|
|
417
743
|
|
|
418
744
|
|
|
745
|
+
@addons.command('trace')
|
|
746
|
+
def install_trace():
|
|
747
|
+
"Install trace addon."
|
|
748
|
+
run_install(
|
|
749
|
+
cmd=f'{sys.executable} -m pip install secator[trace]',
|
|
750
|
+
title='dev addon',
|
|
751
|
+
next_steps=[
|
|
752
|
+
'Run "secator test lint" to run lint tests.',
|
|
753
|
+
'Run "secator test unit" to run unit tests.',
|
|
754
|
+
'Run "secator test integration" to run integration tests.',
|
|
755
|
+
]
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
@addons.command('build')
|
|
760
|
+
def install_build():
|
|
761
|
+
"Install build addon."
|
|
762
|
+
run_install(
|
|
763
|
+
cmd=f'{sys.executable} -m pip install secator[build]',
|
|
764
|
+
title='build addon',
|
|
765
|
+
next_steps=[
|
|
766
|
+
'Run "secator test lint" to run lint tests.',
|
|
767
|
+
'Run "secator test unit" to run unit tests.',
|
|
768
|
+
'Run "secator test integration" to run integration tests.',
|
|
769
|
+
]
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
|
|
419
773
|
@install.group()
|
|
420
774
|
def langs():
|
|
421
775
|
"Install languages."
|
|
@@ -427,7 +781,10 @@ def install_go():
|
|
|
427
781
|
"""Install Go."""
|
|
428
782
|
run_install(
|
|
429
783
|
cmd='wget -O - https://raw.githubusercontent.com/freelabz/secator/main/scripts/install_go.sh | sudo sh',
|
|
430
|
-
title='Go'
|
|
784
|
+
title='Go',
|
|
785
|
+
next_steps=[
|
|
786
|
+
'Add ~/go/bin to your $PATH'
|
|
787
|
+
]
|
|
431
788
|
)
|
|
432
789
|
|
|
433
790
|
|
|
@@ -452,28 +809,20 @@ def install_tools(cmds):
|
|
|
452
809
|
|
|
453
810
|
for ix, cls in enumerate(tools):
|
|
454
811
|
with console.status(f'[bold yellow][{ix}/{len(tools)}] Installing {cls.__name__} ...'):
|
|
455
|
-
|
|
812
|
+
ToolInstaller.install(cls)
|
|
456
813
|
console.print()
|
|
457
814
|
|
|
458
815
|
|
|
459
816
|
@install.command('cves')
|
|
460
817
|
@click.option('--force', is_flag=True)
|
|
461
818
|
def install_cves(force):
|
|
462
|
-
"""Install CVEs
|
|
819
|
+
"""Install CVEs (enables passive vulnerability search)."""
|
|
463
820
|
cve_json_path = f'{CVES_FOLDER}/circl-cve-search-expanded.json'
|
|
464
821
|
if not os.path.exists(cve_json_path) or force:
|
|
465
822
|
with console.status('[bold yellow]Downloading zipped CVEs from cve.circl.lu ...[/]'):
|
|
466
|
-
Command.
|
|
467
|
-
'wget https://cve.circl.lu/static/circl-cve-search-expanded.json.gz',
|
|
468
|
-
cwd=CVES_FOLDER,
|
|
469
|
-
**DEFAULT_CMD_OPTS
|
|
470
|
-
)
|
|
823
|
+
Command.execute('wget https://cve.circl.lu/static/circl-cve-search-expanded.json.gz', cwd=CVES_FOLDER)
|
|
471
824
|
with console.status('[bold yellow]Unzipping CVEs ...[/]'):
|
|
472
|
-
Command.
|
|
473
|
-
f'gunzip {CVES_FOLDER}/circl-cve-search-expanded.json.gz',
|
|
474
|
-
cwd=CVES_FOLDER,
|
|
475
|
-
**DEFAULT_CMD_OPTS
|
|
476
|
-
)
|
|
825
|
+
Command.execute(f'gunzip {CVES_FOLDER}/circl-cve-search-expanded.json.gz', cwd=CVES_FOLDER)
|
|
477
826
|
with console.status(f'[bold yellow]Installing CVEs to {CVES_FOLDER} ...[/]'):
|
|
478
827
|
with open(cve_json_path, 'r') as f:
|
|
479
828
|
for line in f:
|
|
@@ -486,14 +835,30 @@ def install_cves(force):
|
|
|
486
835
|
console.print(':tada: CVEs installed successfully !', style='bold green')
|
|
487
836
|
|
|
488
837
|
|
|
838
|
+
#--------#
|
|
839
|
+
# UPDATE #
|
|
840
|
+
#--------#
|
|
841
|
+
|
|
842
|
+
@cli.command('update')
|
|
843
|
+
def update():
|
|
844
|
+
"""[dim]Update to latest version.[/]"""
|
|
845
|
+
if not VERSION_OBSOLETE:
|
|
846
|
+
console.print(f'[bold green]secator is already at the newest version {VERSION_LATEST}[/]')
|
|
847
|
+
console.print(f'[bold gold3]:wrench: Updating secator from {VERSION} to {VERSION_LATEST} ...[/]')
|
|
848
|
+
if 'pipx' in sys.executable:
|
|
849
|
+
Command.execute(f'pipx install secator=={VERSION_LATEST} --force')
|
|
850
|
+
else:
|
|
851
|
+
Command.execute(f'pip install secator=={VERSION_LATEST}')
|
|
852
|
+
|
|
853
|
+
|
|
489
854
|
#-------#
|
|
490
855
|
# ALIAS #
|
|
491
856
|
#-------#
|
|
492
857
|
|
|
493
858
|
|
|
494
|
-
@cli.group(
|
|
859
|
+
@cli.group()
|
|
495
860
|
def alias():
|
|
496
|
-
"""
|
|
861
|
+
"""[dim]Configure aliases.[/]"""
|
|
497
862
|
pass
|
|
498
863
|
|
|
499
864
|
|
|
@@ -577,259 +942,6 @@ def list_aliases(silent):
|
|
|
577
942
|
return aliases
|
|
578
943
|
|
|
579
944
|
|
|
580
|
-
#-------#
|
|
581
|
-
# UTILS #
|
|
582
|
-
#-------#
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
@cli.group(aliases=['u'])
|
|
586
|
-
def utils():
|
|
587
|
-
"""Utilities."""
|
|
588
|
-
pass
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
@utils.command()
|
|
592
|
-
@click.option('--timeout', type=float, default=0.2, help='Proxy timeout (in seconds)')
|
|
593
|
-
@click.option('--number', '-n', type=int, default=1, help='Number of proxies')
|
|
594
|
-
def get_proxy(timeout, number):
|
|
595
|
-
"""Get a random proxy."""
|
|
596
|
-
proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
|
|
597
|
-
for _ in range(number):
|
|
598
|
-
url = proxy.get()
|
|
599
|
-
print(url)
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
@utils.command()
|
|
603
|
-
@click.argument('name', type=str, default=None, required=False)
|
|
604
|
-
@click.option('--host', '-h', type=str, default=None, help='Specify LHOST for revshell, otherwise autodetected.')
|
|
605
|
-
@click.option('--port', '-p', type=int, default=9001, show_default=True, help='Specify PORT for revshell')
|
|
606
|
-
@click.option('--interface', '-i', type=str, help='Interface to use to detect IP')
|
|
607
|
-
@click.option('--listen', '-l', is_flag=True, default=False, help='Spawn netcat listener on specified port')
|
|
608
|
-
def revshells(name, host, port, interface, listen):
|
|
609
|
-
"""Show reverse shell source codes and run netcat listener."""
|
|
610
|
-
if host is None: # detect host automatically
|
|
611
|
-
host = detect_host(interface)
|
|
612
|
-
if not host:
|
|
613
|
-
console.print(
|
|
614
|
-
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces.',
|
|
615
|
-
style='bold red')
|
|
616
|
-
return
|
|
617
|
-
|
|
618
|
-
with open(f'{SCRIPTS_FOLDER}/revshells.json') as f:
|
|
619
|
-
shells = json.loads(f.read())
|
|
620
|
-
for sh in shells:
|
|
621
|
-
sh['alias'] = '_'.join(sh['name'].lower()
|
|
622
|
-
.replace('-c', '')
|
|
623
|
-
.replace('-e', '')
|
|
624
|
-
.replace('-i', '')
|
|
625
|
-
.replace('c#', 'cs')
|
|
626
|
-
.replace('#', '')
|
|
627
|
-
.replace('(', '')
|
|
628
|
-
.replace(')', '')
|
|
629
|
-
.strip()
|
|
630
|
-
.split(' ')).replace('_1', '')
|
|
631
|
-
cmd = re.sub(r"\s\s+", "", sh.get('command', ''), flags=re.UNICODE)
|
|
632
|
-
cmd = cmd.replace('\n', ' ')
|
|
633
|
-
sh['cmd_short'] = (cmd[:30] + '..') if len(cmd) > 30 else cmd
|
|
634
|
-
|
|
635
|
-
shell = [
|
|
636
|
-
shell for shell in shells if shell['name'] == name or shell['alias'] == name
|
|
637
|
-
]
|
|
638
|
-
if not shell:
|
|
639
|
-
console.print('Available shells:', style='bold yellow')
|
|
640
|
-
shells_str = [
|
|
641
|
-
'[bold magenta]{alias:<20}[/][dim white]{name:<20}[/][dim gold3]{cmd_short:<20}[/]'.format(**sh)
|
|
642
|
-
for sh in shells
|
|
643
|
-
]
|
|
644
|
-
console.print('\n'.join(shells_str))
|
|
645
|
-
else:
|
|
646
|
-
shell = shell[0]
|
|
647
|
-
command = shell['command']
|
|
648
|
-
alias = shell['alias']
|
|
649
|
-
name = shell['name']
|
|
650
|
-
command_str = Template(command).render(ip=host, port=port, shell='bash')
|
|
651
|
-
console.print(Rule(f'[bold gold3]{alias}[/] - [bold red]{name} REMOTE SHELL', style='bold red', align='left'))
|
|
652
|
-
lang = shell.get('lang') or 'sh'
|
|
653
|
-
if len(command.splitlines()) == 1:
|
|
654
|
-
console.print()
|
|
655
|
-
print(f'\033[0;36m{command_str}')
|
|
656
|
-
else:
|
|
657
|
-
md = Markdown(f'```{lang}\n{command_str}\n```')
|
|
658
|
-
console.print(md)
|
|
659
|
-
console.print(f'Save this script as rev.{lang} and run it on your target', style='dim italic')
|
|
660
|
-
console.print()
|
|
661
|
-
console.print(Rule(style='bold red'))
|
|
662
|
-
|
|
663
|
-
if listen:
|
|
664
|
-
console.print(f'Starting netcat listener on port {port} ...', style='bold gold3')
|
|
665
|
-
cmd = f'nc -lvnp {port}'
|
|
666
|
-
Command.run_command(
|
|
667
|
-
cmd,
|
|
668
|
-
**DEFAULT_CMD_OPTS
|
|
669
|
-
)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
@utils.command()
|
|
673
|
-
@click.option('--directory', '-d', type=str, default=PAYLOADS_FOLDER, show_default=True, help='HTTP server directory')
|
|
674
|
-
@click.option('--host', '-h', type=str, default=None, help='HTTP host')
|
|
675
|
-
@click.option('--port', '-p', type=int, default=9001, help='HTTP server port')
|
|
676
|
-
@click.option('--interface', '-i', type=str, default=None, help='Interface to use to auto-detect host IP')
|
|
677
|
-
def serve(directory, host, port, interface):
|
|
678
|
-
"""Serve payloads in HTTP server."""
|
|
679
|
-
LSE_URL = 'https://github.com/diego-treitos/linux-smart-enumeration/releases/latest/download/lse.sh'
|
|
680
|
-
LINPEAS_URL = 'https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh'
|
|
681
|
-
SUDOKILLER_URL = 'https://raw.githubusercontent.com/TH3xACE/SUDO_KILLER/master/SUDO_KILLERv2.4.2.sh'
|
|
682
|
-
PAYLOADS = [
|
|
683
|
-
{
|
|
684
|
-
'fname': 'lse.sh',
|
|
685
|
-
'description': 'Linux Smart Enumeration',
|
|
686
|
-
'command': f'wget {LSE_URL} -O lse.sh && chmod 700 lse.sh'
|
|
687
|
-
},
|
|
688
|
-
{
|
|
689
|
-
'fname': 'linpeas.sh',
|
|
690
|
-
'description': 'Linux Privilege Escalation Awesome Script',
|
|
691
|
-
'command': f'wget {LINPEAS_URL} -O linpeas.sh && chmod 700 linpeas.sh'
|
|
692
|
-
},
|
|
693
|
-
{
|
|
694
|
-
'fname': 'sudo_killer.sh',
|
|
695
|
-
'description': 'SUDO_KILLER',
|
|
696
|
-
'command': f'wget {SUDOKILLER_URL} -O sudo_killer.sh && chmod 700 sudo_killer.sh'
|
|
697
|
-
}
|
|
698
|
-
]
|
|
699
|
-
for ix, payload in enumerate(PAYLOADS):
|
|
700
|
-
descr = payload.get('description', '')
|
|
701
|
-
fname = payload['fname']
|
|
702
|
-
if not os.path.exists(f'{directory}/{fname}'):
|
|
703
|
-
with console.status(f'[bold yellow][{ix}/{len(PAYLOADS)}] Downloading {fname} [dim]({descr})[/] ...[/]'):
|
|
704
|
-
cmd = payload['command']
|
|
705
|
-
console.print(f'[bold magenta]{fname} [dim]({descr})[/] ...[/]', )
|
|
706
|
-
opts = DEFAULT_CMD_OPTS.copy()
|
|
707
|
-
opts['no_capture'] = False
|
|
708
|
-
Command.run_command(
|
|
709
|
-
cmd,
|
|
710
|
-
cls_attributes={'shell': True},
|
|
711
|
-
cwd=directory,
|
|
712
|
-
**opts
|
|
713
|
-
)
|
|
714
|
-
console.print()
|
|
715
|
-
|
|
716
|
-
console.print(Rule())
|
|
717
|
-
console.print(f'Available payloads in {directory}: ', style='bold yellow')
|
|
718
|
-
opts = DEFAULT_CMD_OPTS.copy()
|
|
719
|
-
opts['print_cmd'] = False
|
|
720
|
-
for fname in os.listdir(directory):
|
|
721
|
-
if not host:
|
|
722
|
-
host = detect_host(interface)
|
|
723
|
-
if not host:
|
|
724
|
-
console.print(
|
|
725
|
-
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.',
|
|
726
|
-
style='bold red')
|
|
727
|
-
return
|
|
728
|
-
payload = find_list_item(PAYLOADS, fname, key='fname', default={})
|
|
729
|
-
fdescr = payload.get('description', 'No description')
|
|
730
|
-
console.print(f'{fname} [dim]({fdescr})[/]', style='bold magenta')
|
|
731
|
-
console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
|
|
732
|
-
console.print('')
|
|
733
|
-
console.print(Rule())
|
|
734
|
-
console.print('Starting HTTP server ...', style='bold yellow')
|
|
735
|
-
Command.run_command(
|
|
736
|
-
f'{sys.executable} -m http.server {port}',
|
|
737
|
-
cwd=directory,
|
|
738
|
-
**DEFAULT_CMD_OPTS
|
|
739
|
-
)
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
@utils.command()
|
|
743
|
-
@click.argument('record_name', type=str, default=None)
|
|
744
|
-
@click.option('--script', '-s', type=str, default=None, help='Script to run. See scripts/stories/ for examples.')
|
|
745
|
-
@click.option('--interactive', '-i', is_flag=True, default=False, help='Interactive record.')
|
|
746
|
-
@click.option('--width', '-w', type=int, default=None, help='Recording width')
|
|
747
|
-
@click.option('--height', '-h', type=int, default=None, help='Recording height')
|
|
748
|
-
@click.option('--output-dir', type=str, default=f'{ROOT_FOLDER}/images')
|
|
749
|
-
def record(record_name, script, interactive, width, height, output_dir):
|
|
750
|
-
"""Record secator session using asciinema."""
|
|
751
|
-
# 120 x 30 is a good ratio for GitHub
|
|
752
|
-
width = width or console.size.width
|
|
753
|
-
height = height or console.size.height
|
|
754
|
-
attrs = {
|
|
755
|
-
'shell': False,
|
|
756
|
-
'env': {
|
|
757
|
-
'RECORD': '1',
|
|
758
|
-
'LINES': str(height),
|
|
759
|
-
'PS1': '$ ',
|
|
760
|
-
'COLUMNS': str(width),
|
|
761
|
-
'TERM': 'xterm-256color'
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
output_cast_path = f'{output_dir}/{record_name}.cast'
|
|
765
|
-
output_gif_path = f'{output_dir}/{record_name}.gif'
|
|
766
|
-
|
|
767
|
-
# Run automated 'story' script with asciinema-automation
|
|
768
|
-
if script:
|
|
769
|
-
# If existing cast file, remove it
|
|
770
|
-
if os.path.exists(output_cast_path):
|
|
771
|
-
os.unlink(output_cast_path)
|
|
772
|
-
console.print(f'Removed existing {output_cast_path}', style='bold green')
|
|
773
|
-
|
|
774
|
-
with console.status('[bold gold3]Recording with asciinema ...[/]'):
|
|
775
|
-
Command.run_command(
|
|
776
|
-
f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
|
|
777
|
-
cls_attributes=attrs,
|
|
778
|
-
raw=True,
|
|
779
|
-
**DEFAULT_CMD_OPTS,
|
|
780
|
-
)
|
|
781
|
-
console.print(f'Generated {output_cast_path}', style='bold green')
|
|
782
|
-
elif interactive:
|
|
783
|
-
os.environ.update(attrs['env'])
|
|
784
|
-
Command.run_command(
|
|
785
|
-
f'asciinema rec -c /bin/bash --stdin --overwrite {output_cast_path}',
|
|
786
|
-
)
|
|
787
|
-
|
|
788
|
-
# Resize cast file
|
|
789
|
-
if os.path.exists(output_cast_path):
|
|
790
|
-
with console.status('[bold gold3]Cleaning up .cast and set custom settings ...'):
|
|
791
|
-
with open(output_cast_path, 'r') as f:
|
|
792
|
-
lines = f.readlines()
|
|
793
|
-
updated_lines = []
|
|
794
|
-
for ix, line in enumerate(lines):
|
|
795
|
-
tmp_line = json.loads(line)
|
|
796
|
-
if ix == 0:
|
|
797
|
-
tmp_line['width'] = width
|
|
798
|
-
tmp_line['height'] = height
|
|
799
|
-
tmp_line['env']['SHELL'] = '/bin/sh'
|
|
800
|
-
lines[0] = json.dumps(tmp_line) + '\n'
|
|
801
|
-
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
802
|
-
elif tmp_line[2].endswith(' \r'):
|
|
803
|
-
tmp_line[2] = tmp_line[2].replace(' \r', '')
|
|
804
|
-
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
805
|
-
else:
|
|
806
|
-
updated_lines.append(line)
|
|
807
|
-
with open(output_cast_path, 'w') as f:
|
|
808
|
-
f.writelines(updated_lines)
|
|
809
|
-
console.print('')
|
|
810
|
-
|
|
811
|
-
# Edit cast file to reduce long timeouts
|
|
812
|
-
with console.status('[bold gold3] Editing cast file to reduce long commands ...'):
|
|
813
|
-
Command.run_command(
|
|
814
|
-
f'asciinema-edit quantize --range 1 {output_cast_path} --out {output_cast_path}.tmp',
|
|
815
|
-
cls_attributes=attrs,
|
|
816
|
-
raw=True,
|
|
817
|
-
**DEFAULT_CMD_OPTS,
|
|
818
|
-
)
|
|
819
|
-
if os.path.exists(f'{output_cast_path}.tmp'):
|
|
820
|
-
os.replace(f'{output_cast_path}.tmp', output_cast_path)
|
|
821
|
-
console.print(f'Edited {output_cast_path}', style='bold green')
|
|
822
|
-
|
|
823
|
-
# Convert to GIF
|
|
824
|
-
with console.status(f'[bold gold3]Converting to {output_gif_path} ...[/]'):
|
|
825
|
-
Command.run_command(
|
|
826
|
-
f'agg {output_cast_path} {output_gif_path}',
|
|
827
|
-
cls_attributes=attrs,
|
|
828
|
-
**DEFAULT_CMD_OPTS,
|
|
829
|
-
)
|
|
830
|
-
console.print(f'Generated {output_gif_path}', style='bold green')
|
|
831
|
-
|
|
832
|
-
|
|
833
945
|
#------#
|
|
834
946
|
# TEST #
|
|
835
947
|
#------#
|
|
@@ -837,7 +949,7 @@ def record(record_name, script, interactive, width, height, output_dir):
|
|
|
837
949
|
|
|
838
950
|
@cli.group(cls=OrderedGroup)
|
|
839
951
|
def test():
|
|
840
|
-
"""
|
|
952
|
+
"""[dim]Run tests."""
|
|
841
953
|
if not DEV_PACKAGE:
|
|
842
954
|
console.print('[bold red]You MUST use a development version of secator to run tests.[/]')
|
|
843
955
|
sys.exit(1)
|
|
@@ -854,12 +966,7 @@ def run_test(cmd, name):
|
|
|
854
966
|
cmd: Command to run.
|
|
855
967
|
name: Name of the test.
|
|
856
968
|
"""
|
|
857
|
-
result = Command.
|
|
858
|
-
cmd,
|
|
859
|
-
name=name + ' tests',
|
|
860
|
-
cwd=ROOT_FOLDER,
|
|
861
|
-
**DEFAULT_CMD_OPTS
|
|
862
|
-
)
|
|
969
|
+
result = Command.execute(cmd, name=name + ' tests', cwd=ROOT_FOLDER)
|
|
863
970
|
if result.return_code == 0:
|
|
864
971
|
console.print(f':tada: {name.capitalize()} tests passed !', style='bold green')
|
|
865
972
|
sys.exit(result.return_code)
|