secator 0.1.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/.gitignore +162 -0
- secator/__init__.py +0 -0
- secator/celery.py +421 -0
- secator/cli.py +927 -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 +346 -0
- secator/definitions.py +183 -0
- secator/exporters/__init__.py +12 -0
- secator/exporters/_base.py +3 -0
- secator/exporters/csv.py +29 -0
- secator/exporters/gdrive.py +118 -0
- secator/exporters/json.py +14 -0
- secator/exporters/table.py +7 -0
- secator/exporters/txt.py +24 -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 +95 -0
- secator/rich.py +123 -0
- secator/runners/__init__.py +12 -0
- secator/runners/_base.py +873 -0
- secator/runners/_helpers.py +154 -0
- secator/runners/command.py +674 -0
- secator/runners/scan.py +67 -0
- secator/runners/task.py +107 -0
- secator/runners/workflow.py +137 -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 +66 -0
- secator/tasks/dirsearch.py +88 -0
- secator/tasks/dnsx.py +56 -0
- secator/tasks/dnsxbrute.py +34 -0
- secator/tasks/feroxbuster.py +89 -0
- secator/tasks/ffuf.py +85 -0
- secator/tasks/fping.py +44 -0
- secator/tasks/gau.py +43 -0
- secator/tasks/gf.py +34 -0
- secator/tasks/gospider.py +71 -0
- secator/tasks/grype.py +78 -0
- secator/tasks/h8mail.py +80 -0
- secator/tasks/httpx.py +104 -0
- secator/tasks/katana.py +128 -0
- secator/tasks/maigret.py +78 -0
- secator/tasks/mapcidr.py +32 -0
- secator/tasks/msfconsole.py +176 -0
- secator/tasks/naabu.py +52 -0
- secator/tasks/nmap.py +341 -0
- secator/tasks/nuclei.py +97 -0
- secator/tasks/searchsploit.py +53 -0
- secator/tasks/subfinder.py +40 -0
- secator/tasks/wpscan.py +177 -0
- secator/utils.py +404 -0
- secator/utils_test.py +183 -0
- secator-0.1.0.dist-info/METADATA +379 -0
- secator-0.1.0.dist-info/RECORD +99 -0
- secator-0.1.0.dist-info/WHEEL +5 -0
- secator-0.1.0.dist-info/entry_points.txt +2 -0
- secator-0.1.0.dist-info/licenses/LICENSE +60 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Attack tasks."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
|
|
7
|
+
from secator.decorators import task
|
|
8
|
+
from secator.definitions import (DELAY, FOLLOW_REDIRECT, HEADER, HOST,
|
|
9
|
+
OPT_NOT_SUPPORTED, PROXY, RATE_LIMIT, RETRIES,
|
|
10
|
+
DATA_FOLDER, THREADS, TIMEOUT, USER_AGENT)
|
|
11
|
+
from secator.tasks._categories import VulnMulti
|
|
12
|
+
from secator.utils import get_file_timestamp
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@task()
|
|
18
|
+
class msfconsole(VulnMulti):
|
|
19
|
+
"""CLI to access and work with the Metasploit Framework."""
|
|
20
|
+
cmd = 'msfconsole --quiet'
|
|
21
|
+
version_flag = OPT_NOT_SUPPORTED
|
|
22
|
+
input_type = HOST
|
|
23
|
+
input_chunk_size = 1
|
|
24
|
+
output_types = []
|
|
25
|
+
output_return_type = str
|
|
26
|
+
opt_prefix = '--'
|
|
27
|
+
opts = {
|
|
28
|
+
'resource': {'type': str, 'help': 'Metasploit resource script.', 'short': 'r'},
|
|
29
|
+
'execute_command': {'type': str, 'help': 'Metasploit command.', 'short': 'x'},
|
|
30
|
+
'environment': {'type': str, 'help': 'Environment variables string KEY=VALUE.', 'short': 'e'}
|
|
31
|
+
}
|
|
32
|
+
opt_key_map = {
|
|
33
|
+
'x': 'execute_command',
|
|
34
|
+
'r': 'resource',
|
|
35
|
+
HEADER: OPT_NOT_SUPPORTED,
|
|
36
|
+
DELAY: OPT_NOT_SUPPORTED,
|
|
37
|
+
FOLLOW_REDIRECT: OPT_NOT_SUPPORTED,
|
|
38
|
+
PROXY: OPT_NOT_SUPPORTED,
|
|
39
|
+
RATE_LIMIT: OPT_NOT_SUPPORTED,
|
|
40
|
+
RETRIES: OPT_NOT_SUPPORTED,
|
|
41
|
+
THREADS: OPT_NOT_SUPPORTED,
|
|
42
|
+
TIMEOUT: OPT_NOT_SUPPORTED,
|
|
43
|
+
USER_AGENT: OPT_NOT_SUPPORTED,
|
|
44
|
+
THREADS: OPT_NOT_SUPPORTED,
|
|
45
|
+
}
|
|
46
|
+
encoding = 'ansi'
|
|
47
|
+
ignore_return_code = True
|
|
48
|
+
# install_cmd = 'wget -O - https://raw.githubusercontent.com/freelabz/secator/main/scripts/msfinstall.sh | sh'
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def validate_input(self, input):
|
|
52
|
+
"""No list input supported for this command. Pass a single input instead."""
|
|
53
|
+
if isinstance(input, list):
|
|
54
|
+
return False
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def on_init(self):
|
|
59
|
+
command = self.get_opt_value('execute_command')
|
|
60
|
+
script_path = self.get_opt_value('resource')
|
|
61
|
+
environment = self.run_opts.pop('environment', '')
|
|
62
|
+
env_vars = {}
|
|
63
|
+
if environment:
|
|
64
|
+
env_vars = dict(map(lambda x: x.split('='), environment.strip().split(',')))
|
|
65
|
+
env_vars['RHOST'] = self.input
|
|
66
|
+
env_vars['RHOSTS'] = self.input
|
|
67
|
+
|
|
68
|
+
# Passing msfconsole command directly, simply add RHOST / RHOSTS from host input and run then exit
|
|
69
|
+
if command:
|
|
70
|
+
self.run_opts['msfconsole.execute_command'] = (
|
|
71
|
+
f'setg RHOST {self.input}; '
|
|
72
|
+
f'setg RHOSTS {self.input}; '
|
|
73
|
+
f'{command.format(**env_vars)}; '
|
|
74
|
+
f'exit;'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Passing resource script, replace vars inside by our environment variables if any, write to a temp file, and
|
|
78
|
+
# pass this temp file instead of the original one.
|
|
79
|
+
elif script_path:
|
|
80
|
+
|
|
81
|
+
# Read from original resource script
|
|
82
|
+
with open(script_path, 'r') as f:
|
|
83
|
+
content = f.read().replace('exit', '') + 'exit'
|
|
84
|
+
|
|
85
|
+
# Make a copy and replace vars inside by env vars passed on the CLI
|
|
86
|
+
timestr = get_file_timestamp()
|
|
87
|
+
out_path = f'{DATA_FOLDER}/msfconsole_{timestr}.rc'
|
|
88
|
+
logger.debug(
|
|
89
|
+
f'Writing formatted resource script to new temp file {out_path}'
|
|
90
|
+
)
|
|
91
|
+
with open(out_path, 'w') as f:
|
|
92
|
+
content = content.format(**env_vars)
|
|
93
|
+
f.write(content)
|
|
94
|
+
|
|
95
|
+
script_name = script_path.split('/')[-1]
|
|
96
|
+
self._print(Panel(content, title=f'[bold magenta]{script_name}', expand=False))
|
|
97
|
+
|
|
98
|
+
# Override original command with new resource script
|
|
99
|
+
self.run_opts['msfconsole.resource'] = out_path
|
|
100
|
+
|
|
101
|
+
# Nothing passed, error out
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError('At least one of "inline_script" or "resource_script" must be passed.')
|
|
104
|
+
|
|
105
|
+
# Clear host input
|
|
106
|
+
self.input = ''
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# TODO: This is better as it goes through an RPC API to communicate with
|
|
110
|
+
# metasploit rpc server, but it does not give any output.
|
|
111
|
+
# Seems like output is available only in Metasploit Pro, so keeping this in case
|
|
112
|
+
# we add support for it later.
|
|
113
|
+
#
|
|
114
|
+
# from pymetasploit3.msfrpc import MsfRpcClient
|
|
115
|
+
# class msfrpcd():
|
|
116
|
+
#
|
|
117
|
+
# opts = {
|
|
118
|
+
# 'uri': {'type': str, 'default': '/api/', 'help': 'msfrpcd API uri'},
|
|
119
|
+
# 'port': {'type': int, 'default': 55553, 'help': 'msfrpcd port'},
|
|
120
|
+
# 'server': {'type': str, 'default': 'localhost', 'help': 'msfrpcd host'},
|
|
121
|
+
# 'token': {'type': str, 'help': 'msfrpcd token'},
|
|
122
|
+
# 'username': {'type': str, 'default': 'msf', 'help': 'msfrpcd username'},
|
|
123
|
+
# 'password': {'type': str, 'default': 'test', 'help': 'msfrpcd password'},
|
|
124
|
+
# 'module': {'type': str, 'required': True, 'help': 'Metasploit module to run'}
|
|
125
|
+
# }
|
|
126
|
+
#
|
|
127
|
+
# def __init__(self, input, ctx={}, **run_opts):
|
|
128
|
+
# self.module = run_opts.pop('module')
|
|
129
|
+
# pw = run_opts.pop('password')
|
|
130
|
+
# self.run_opts = run_opts
|
|
131
|
+
# self.RHOST = input
|
|
132
|
+
# self.RHOSTS = input
|
|
133
|
+
# self.LHOST = self.get_lhost()
|
|
134
|
+
# # self.start_msgrpc()
|
|
135
|
+
# self.client = MsfRpcClient(pw, ssl=True, **run_opts)
|
|
136
|
+
#
|
|
137
|
+
# # def start_msgrpc(self):
|
|
138
|
+
# # code, out = run_command(f'msfrpcd -P {self.password}')
|
|
139
|
+
# # logger.info(out)
|
|
140
|
+
#
|
|
141
|
+
# def get_lhost(self):
|
|
142
|
+
# try:
|
|
143
|
+
# u = miniupnpc.UPnP()
|
|
144
|
+
# u.discoverdelay = 200
|
|
145
|
+
# u.discover()
|
|
146
|
+
# u.selectigd()
|
|
147
|
+
# return u.externalipaddress()
|
|
148
|
+
# except Exception:
|
|
149
|
+
# return 'localhost'
|
|
150
|
+
#
|
|
151
|
+
# def run(self):
|
|
152
|
+
# """Run a metasploit module.
|
|
153
|
+
#
|
|
154
|
+
# Args:
|
|
155
|
+
# modtype: Module type amongst 'auxiliary', 'exploit', 'post',
|
|
156
|
+
# 'encoder', 'nop', 'payload'.
|
|
157
|
+
# modname: Module name e.g 'auxiliary/scanner/ftp/ftp_version
|
|
158
|
+
# kwargs (dict): Module kwargs e.g RHOSTS, LHOST
|
|
159
|
+
# Returns:
|
|
160
|
+
# dict: Job results.
|
|
161
|
+
# """
|
|
162
|
+
# modtype = self.module.split('/')[0].rstrip('s')
|
|
163
|
+
# job = self.client.modules.execute(
|
|
164
|
+
# modtype,
|
|
165
|
+
# self.module,
|
|
166
|
+
# RHOST=self.RHOST,
|
|
167
|
+
# RHOSTS=self.RHOSTS,
|
|
168
|
+
# LHOST=self.LHOST)
|
|
169
|
+
# if job.get('error', False):
|
|
170
|
+
# logger.error(job['error_message'])
|
|
171
|
+
# job_info = self.client.jobs.info_by_uuid(job['uuid'])
|
|
172
|
+
# while (job_info['status'] in ['running', 'ready']):
|
|
173
|
+
# job_info = self.client.jobs.info_by_uuid(job['uuid'])
|
|
174
|
+
# job_info.update(job)
|
|
175
|
+
# print(type(job_info['result']['127.0.0.1']))
|
|
176
|
+
# return job_info
|
secator/tasks/naabu.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from secator.decorators import task
|
|
2
|
+
from secator.definitions import (DELAY, HOST, OPT_NOT_SUPPORTED, PORT, PORTS,
|
|
3
|
+
PROXY, RATE_LIMIT, RETRIES, STATE, THREADS,
|
|
4
|
+
TIMEOUT, TOP_PORTS)
|
|
5
|
+
from secator.output_types import Port
|
|
6
|
+
from secator.tasks._categories import ReconPort
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@task()
|
|
10
|
+
class naabu(ReconPort):
|
|
11
|
+
"""Port scanning tool written in Go."""
|
|
12
|
+
cmd = 'naabu -Pn -silent'
|
|
13
|
+
input_flag = '-host'
|
|
14
|
+
file_flag = '-list'
|
|
15
|
+
json_flag = '-json'
|
|
16
|
+
opts = {
|
|
17
|
+
PORTS: {'type': str, 'short': 'p', 'help': 'Ports'},
|
|
18
|
+
TOP_PORTS: {'type': str, 'short': 'tp', 'help': 'Top ports'},
|
|
19
|
+
'scan_type': {'type': str, 'help': 'Scan type (SYN (s)/CONNECT(c))'},
|
|
20
|
+
# 'health_check': {'is_flag': True, 'short': 'hc', 'help': 'Health check'}
|
|
21
|
+
}
|
|
22
|
+
opt_key_map = {
|
|
23
|
+
DELAY: OPT_NOT_SUPPORTED,
|
|
24
|
+
PROXY: 'proxy',
|
|
25
|
+
RATE_LIMIT: 'rate',
|
|
26
|
+
RETRIES: 'retries',
|
|
27
|
+
TIMEOUT: 'timeout',
|
|
28
|
+
THREADS: 'c',
|
|
29
|
+
|
|
30
|
+
# naabu opts
|
|
31
|
+
PORTS: 'port',
|
|
32
|
+
'scan_type': 's',
|
|
33
|
+
# 'health_check': 'hc'
|
|
34
|
+
}
|
|
35
|
+
opt_value_map = {
|
|
36
|
+
TIMEOUT: lambda x: x*1000 if x and x > 0 else None, # convert to milliseconds
|
|
37
|
+
RETRIES: lambda x: 1 if x == 0 else x,
|
|
38
|
+
PROXY: lambda x: x.replace('socks5://', '')
|
|
39
|
+
}
|
|
40
|
+
output_map = {
|
|
41
|
+
Port: {
|
|
42
|
+
PORT: lambda x: x['port'],
|
|
43
|
+
HOST: lambda x: x['host'] if 'host' in x else x['ip'],
|
|
44
|
+
STATE: lambda x: 'open'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
output_types = [Port]
|
|
48
|
+
install_cmd = 'sudo apt install -y libpcap-dev && go install -v github.com/projectdiscovery/naabu/v2/cmd/naabu@latest'
|
|
49
|
+
proxychains = False
|
|
50
|
+
proxy_socks5 = True
|
|
51
|
+
proxy_http = False
|
|
52
|
+
profile = 'io'
|
secator/tasks/nmap.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import xmltodict
|
|
6
|
+
|
|
7
|
+
from secator.decorators import task
|
|
8
|
+
from secator.definitions import (CONFIDENCE, CVSS_SCORE, DELAY,
|
|
9
|
+
DESCRIPTION, EXTRA_DATA, FOLLOW_REDIRECT,
|
|
10
|
+
HEADER, HOST, ID, IP, MATCHED_AT, NAME,
|
|
11
|
+
OPT_NOT_SUPPORTED, OUTPUT_PATH, PORT, PORTS, PROVIDER,
|
|
12
|
+
PROXY, RATE_LIMIT, REFERENCE, REFERENCES,
|
|
13
|
+
RETRIES, SCRIPT, SERVICE_NAME, STATE, TAGS,
|
|
14
|
+
THREADS, TIMEOUT, USER_AGENT)
|
|
15
|
+
from secator.output_types import Exploit, Port, Vulnerability
|
|
16
|
+
from secator.tasks._categories import VulnMulti
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@task()
|
|
22
|
+
class nmap(VulnMulti):
|
|
23
|
+
"""Network Mapper is a free and open source utility for network discovery and security auditing."""
|
|
24
|
+
cmd = 'nmap -sT -sV -Pn'
|
|
25
|
+
input_flag = None
|
|
26
|
+
input_chunk_size = 1
|
|
27
|
+
file_flag = '-iL'
|
|
28
|
+
opt_prefix = '--'
|
|
29
|
+
output_types = [Port, Vulnerability, Exploit]
|
|
30
|
+
opts = {
|
|
31
|
+
PORTS: {'type': str, 'help': 'Ports to scan', 'short': 'p'},
|
|
32
|
+
SCRIPT: {'type': str, 'default': 'vulners', 'help': 'NSE scripts'},
|
|
33
|
+
# 'tcp_connect': {'type': bool, 'short': 'sT', 'default': False, 'help': 'TCP Connect scan'},
|
|
34
|
+
'tcp_syn_stealth': {'is_flag': True, 'short': 'sS', 'default': False, 'help': 'TCP SYN Stealth'},
|
|
35
|
+
'output_path': {'type': str, 'short': 'oX', 'default': None, 'help': 'Output XML file path'}
|
|
36
|
+
}
|
|
37
|
+
opt_key_map = {
|
|
38
|
+
HEADER: OPT_NOT_SUPPORTED,
|
|
39
|
+
DELAY: 'scan-delay',
|
|
40
|
+
FOLLOW_REDIRECT: OPT_NOT_SUPPORTED,
|
|
41
|
+
PROXY: None, # TODO: supports --proxies but not in TCP mode [https://github.com/nmap/nmap/issues/1098]
|
|
42
|
+
RATE_LIMIT: 'max-rate',
|
|
43
|
+
RETRIES: 'max-retries',
|
|
44
|
+
THREADS: OPT_NOT_SUPPORTED,
|
|
45
|
+
TIMEOUT: 'max-rtt-timeout',
|
|
46
|
+
USER_AGENT: OPT_NOT_SUPPORTED,
|
|
47
|
+
|
|
48
|
+
# Nmap opts
|
|
49
|
+
PORTS: '-p',
|
|
50
|
+
'output_path': '-oX'
|
|
51
|
+
}
|
|
52
|
+
opt_value_map = {
|
|
53
|
+
PORTS: lambda x: ','.join([str(p) for p in x]) if isinstance(x, list) else x
|
|
54
|
+
}
|
|
55
|
+
install_cmd = (
|
|
56
|
+
'sudo apt install -y nmap && sudo git clone https://github.com/scipag/vulscan /opt/scipag_vulscan || true && '
|
|
57
|
+
'sudo ln -s /opt/scipag_vulscan /usr/share/nmap/scripts/vulscan || true'
|
|
58
|
+
)
|
|
59
|
+
proxychains = True
|
|
60
|
+
proxychains_flavor = 'proxychains4'
|
|
61
|
+
proxy_socks5 = False
|
|
62
|
+
proxy_http = False
|
|
63
|
+
profile = 'io'
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def on_init(self):
|
|
67
|
+
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
68
|
+
if not output_path:
|
|
69
|
+
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.xml'
|
|
70
|
+
self.output_path = output_path
|
|
71
|
+
self.cmd += f' -oX {self.output_path}'
|
|
72
|
+
|
|
73
|
+
def yielder(self):
|
|
74
|
+
yield from super().yielder()
|
|
75
|
+
if self.return_code != 0:
|
|
76
|
+
return
|
|
77
|
+
self.results = []
|
|
78
|
+
note = f'nmap XML results saved to {self.output_path}'
|
|
79
|
+
if self.print_line:
|
|
80
|
+
self._print(note)
|
|
81
|
+
if os.path.exists(self.output_path):
|
|
82
|
+
nmap_data = self.xml_to_json()
|
|
83
|
+
yield from nmap_data
|
|
84
|
+
|
|
85
|
+
def xml_to_json(self):
|
|
86
|
+
results = []
|
|
87
|
+
with open(self.output_path, 'r') as f:
|
|
88
|
+
content = f.read()
|
|
89
|
+
try:
|
|
90
|
+
results = xmltodict.parse(content) # parse XML to dict
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.exception(e)
|
|
93
|
+
logger.error(
|
|
94
|
+
f'Cannot parse nmap XML output {self.output_path} to valid JSON.')
|
|
95
|
+
results['_host'] = self.input
|
|
96
|
+
return nmapData(results)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class nmapData(dict):
|
|
100
|
+
|
|
101
|
+
def __iter__(self):
|
|
102
|
+
for host in self._get_hosts():
|
|
103
|
+
hostname = self._get_hostname(host)
|
|
104
|
+
ip = self._get_ip(host)
|
|
105
|
+
for port in self._get_ports(host):
|
|
106
|
+
# Get port number
|
|
107
|
+
port_number = port['@portid']
|
|
108
|
+
if not port_number or not port_number.isdigit():
|
|
109
|
+
continue
|
|
110
|
+
port_number = int(port_number)
|
|
111
|
+
|
|
112
|
+
# Get port state
|
|
113
|
+
state = port.get('state', {}).get('@state', '')
|
|
114
|
+
|
|
115
|
+
# Get extra data
|
|
116
|
+
extra_data = self._get_extra_data(port)
|
|
117
|
+
|
|
118
|
+
# Grab CPEs
|
|
119
|
+
cpes = extra_data.get('cpe', [])
|
|
120
|
+
|
|
121
|
+
# Grab service name
|
|
122
|
+
service_name = ''
|
|
123
|
+
if 'product' in extra_data:
|
|
124
|
+
service_name = extra_data['product']
|
|
125
|
+
elif 'name' in extra_data:
|
|
126
|
+
service_name = extra_data['name']
|
|
127
|
+
if 'version' in extra_data:
|
|
128
|
+
version = extra_data['version']
|
|
129
|
+
service_name += f'/{version}'
|
|
130
|
+
|
|
131
|
+
# Get script output
|
|
132
|
+
scripts = self._get_scripts(port)
|
|
133
|
+
|
|
134
|
+
# Yield port data
|
|
135
|
+
port = {
|
|
136
|
+
PORT: port_number,
|
|
137
|
+
HOST: hostname,
|
|
138
|
+
STATE: state,
|
|
139
|
+
SERVICE_NAME: service_name,
|
|
140
|
+
IP: ip,
|
|
141
|
+
EXTRA_DATA: extra_data
|
|
142
|
+
}
|
|
143
|
+
yield port
|
|
144
|
+
|
|
145
|
+
# Parse each script output to get vulns
|
|
146
|
+
for script in scripts:
|
|
147
|
+
script_id = script['id']
|
|
148
|
+
output = script['output']
|
|
149
|
+
extra_data = {'script': script_id}
|
|
150
|
+
if service_name:
|
|
151
|
+
extra_data['service_name'] = service_name
|
|
152
|
+
funcmap = {
|
|
153
|
+
'vulscan': self._parse_vulscan_output,
|
|
154
|
+
'vulners': self._parse_vulners_output,
|
|
155
|
+
}
|
|
156
|
+
func = funcmap.get(script_id)
|
|
157
|
+
metadata = {
|
|
158
|
+
MATCHED_AT: f'{hostname}:{port_number}',
|
|
159
|
+
IP: ip,
|
|
160
|
+
EXTRA_DATA: extra_data,
|
|
161
|
+
}
|
|
162
|
+
if not func:
|
|
163
|
+
# logger.debug(f'Script output parser for "{script_id}" is not supported YET.')
|
|
164
|
+
continue
|
|
165
|
+
for vuln in func(output, cpes=cpes):
|
|
166
|
+
vuln.update(metadata)
|
|
167
|
+
yield vuln
|
|
168
|
+
|
|
169
|
+
#---------------------#
|
|
170
|
+
# XML FILE EXTRACTORS #
|
|
171
|
+
#---------------------#
|
|
172
|
+
def _get_hosts(self):
|
|
173
|
+
hosts = self.get('nmaprun', {}).get('host', {})
|
|
174
|
+
if isinstance(hosts, dict):
|
|
175
|
+
hosts = [hosts]
|
|
176
|
+
return hosts
|
|
177
|
+
|
|
178
|
+
def _get_ports(self, host_cfg):
|
|
179
|
+
ports = host_cfg.get('ports', {}).get('port', [])
|
|
180
|
+
if isinstance(ports, dict):
|
|
181
|
+
ports = [ports]
|
|
182
|
+
return ports
|
|
183
|
+
|
|
184
|
+
def _get_hostname(self, host_cfg):
|
|
185
|
+
hostnames = host_cfg.get('hostnames', {})
|
|
186
|
+
hostname = self['_host']
|
|
187
|
+
if hostnames:
|
|
188
|
+
hostnames = hostnames.get('hostname', [])
|
|
189
|
+
if isinstance(hostnames, dict):
|
|
190
|
+
hostnames = [hostnames]
|
|
191
|
+
if hostnames:
|
|
192
|
+
hostname = hostnames[0]['@name']
|
|
193
|
+
else:
|
|
194
|
+
hostname = host_cfg.get('address', {}).get('@addr', None)
|
|
195
|
+
return hostname
|
|
196
|
+
|
|
197
|
+
def _get_ip(self, host_cfg):
|
|
198
|
+
return host_cfg.get('address', {}).get('@addr', None)
|
|
199
|
+
|
|
200
|
+
def _get_extra_data(self, port_cfg):
|
|
201
|
+
extra_datas = {
|
|
202
|
+
k.lstrip('@'): v
|
|
203
|
+
for k, v in port_cfg.get('service', {}).items()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Strip product / version strings
|
|
207
|
+
if 'product' in extra_datas:
|
|
208
|
+
extra_datas['product'] = extra_datas['product'].lower()
|
|
209
|
+
|
|
210
|
+
if 'version' in extra_datas:
|
|
211
|
+
version_split = extra_datas['version'].split(' ')
|
|
212
|
+
version = None
|
|
213
|
+
os = None
|
|
214
|
+
if len(version_split) == 3:
|
|
215
|
+
version, os, extra_version = tuple(version_split)
|
|
216
|
+
version = f'{version}-{extra_version}'
|
|
217
|
+
elif len(version_split) == 2:
|
|
218
|
+
version, os = tuple(version_split)
|
|
219
|
+
elif len(version_split) == 1:
|
|
220
|
+
version = version_split[0]
|
|
221
|
+
else:
|
|
222
|
+
version = extra_datas['version']
|
|
223
|
+
if os:
|
|
224
|
+
extra_datas['os'] = os
|
|
225
|
+
if version:
|
|
226
|
+
extra_datas['version'] = version
|
|
227
|
+
|
|
228
|
+
# Grab CPEs
|
|
229
|
+
cpes = extra_datas.get('cpe', [])
|
|
230
|
+
if not isinstance(cpes, list):
|
|
231
|
+
cpes = [cpes]
|
|
232
|
+
extra_datas['cpe'] = cpes
|
|
233
|
+
|
|
234
|
+
return extra_datas
|
|
235
|
+
|
|
236
|
+
def _get_scripts(self, port_cfg):
|
|
237
|
+
scripts = port_cfg.get('script', [])
|
|
238
|
+
if isinstance(scripts, dict):
|
|
239
|
+
scripts = [scripts]
|
|
240
|
+
scripts = [
|
|
241
|
+
{k.lstrip('@'): v for k, v in script.items()}
|
|
242
|
+
for script in scripts
|
|
243
|
+
]
|
|
244
|
+
return scripts
|
|
245
|
+
|
|
246
|
+
#--------------#
|
|
247
|
+
# VULN PARSERS #
|
|
248
|
+
#--------------#
|
|
249
|
+
def _parse_vulscan_output(self, out, cpes=[]):
|
|
250
|
+
"""Parse nmap vulscan script output.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
out (str): Vulscan script output.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
list: List of Vulnerability dicts.
|
|
257
|
+
"""
|
|
258
|
+
provider_name = ''
|
|
259
|
+
for line in out.splitlines():
|
|
260
|
+
if not line:
|
|
261
|
+
continue
|
|
262
|
+
line = line.strip()
|
|
263
|
+
if not line.startswith('[') and line != 'No findings': # provider line
|
|
264
|
+
provider_name, _ = tuple(line.split(' - '))
|
|
265
|
+
continue
|
|
266
|
+
reg = r'\[([ A-Za-z0-9_@./#&+-]*)\] (.*)'
|
|
267
|
+
matches = re.match(reg, line)
|
|
268
|
+
if not matches:
|
|
269
|
+
continue
|
|
270
|
+
vuln_id, vuln_title = matches.groups()
|
|
271
|
+
vuln = {
|
|
272
|
+
ID: vuln_id,
|
|
273
|
+
NAME: vuln_id,
|
|
274
|
+
DESCRIPTION: vuln_title,
|
|
275
|
+
PROVIDER: provider_name,
|
|
276
|
+
TAGS: [vuln_id, provider_name]
|
|
277
|
+
}
|
|
278
|
+
if provider_name == 'MITRE CVE':
|
|
279
|
+
vuln_data = VulnMulti.lookup_cve(vuln['id'], cpes=cpes)
|
|
280
|
+
if vuln_data:
|
|
281
|
+
vuln.update(vuln_data)
|
|
282
|
+
yield vuln
|
|
283
|
+
else:
|
|
284
|
+
# logger.debug(f'Vulscan provider {provider_name} is not supported YET.')
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
def _parse_vulners_output(self, out, **kwargs):
|
|
288
|
+
cpes = []
|
|
289
|
+
provider_name = 'vulners'
|
|
290
|
+
for line in out.splitlines():
|
|
291
|
+
if not line:
|
|
292
|
+
continue
|
|
293
|
+
line = line.strip()
|
|
294
|
+
if line.startswith('cpe:'):
|
|
295
|
+
cpes.append(line)
|
|
296
|
+
continue
|
|
297
|
+
elems = tuple(line.split('\t'))
|
|
298
|
+
vuln = {}
|
|
299
|
+
|
|
300
|
+
if len(elems) == 4: # exploit
|
|
301
|
+
# TODO: Implement exploit processing
|
|
302
|
+
exploit_id, cvss_score, reference_url, _ = elems
|
|
303
|
+
name = exploit_id
|
|
304
|
+
# edb_id = name.split(':')[-1] if 'EDB-ID' in name else None
|
|
305
|
+
vuln = {
|
|
306
|
+
ID: exploit_id,
|
|
307
|
+
NAME: name,
|
|
308
|
+
PROVIDER: provider_name,
|
|
309
|
+
REFERENCE: reference_url,
|
|
310
|
+
'_type': 'exploit'
|
|
311
|
+
# CVSS_SCORE: cvss_score,
|
|
312
|
+
# CONFIDENCE: 'low'
|
|
313
|
+
}
|
|
314
|
+
# TODO: lookup exploit in ExploitDB to find related CVEs
|
|
315
|
+
# if edb_id:
|
|
316
|
+
# print(edb_id)
|
|
317
|
+
# vuln_data = VulnMulti.lookup_exploitdb(edb_id)
|
|
318
|
+
yield vuln
|
|
319
|
+
|
|
320
|
+
elif len(elems) == 3: # vuln
|
|
321
|
+
vuln_id, vuln_cvss, reference_url = tuple(line.split('\t'))
|
|
322
|
+
vuln_type = vuln_id.split('-')[0]
|
|
323
|
+
vuln = {
|
|
324
|
+
ID: vuln_id,
|
|
325
|
+
NAME: vuln_id,
|
|
326
|
+
PROVIDER: provider_name,
|
|
327
|
+
CVSS_SCORE: vuln_cvss,
|
|
328
|
+
REFERENCES: [reference_url],
|
|
329
|
+
TAGS: [],
|
|
330
|
+
CONFIDENCE: 'low'
|
|
331
|
+
}
|
|
332
|
+
if vuln_type == 'CVE':
|
|
333
|
+
vuln[TAGS].append('cve')
|
|
334
|
+
vuln_data = VulnMulti.lookup_cve(vuln_id, cpes=cpes)
|
|
335
|
+
if vuln_data:
|
|
336
|
+
vuln.update(vuln_data)
|
|
337
|
+
yield vuln
|
|
338
|
+
else:
|
|
339
|
+
logger.debug(f'Vulners parser for "{vuln_type}" is not implemented YET.')
|
|
340
|
+
else:
|
|
341
|
+
logger.error(f'Unrecognized vulners output: {elems}')
|
secator/tasks/nuclei.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from secator.decorators import task
|
|
2
|
+
from secator.definitions import (CONFIDENCE, CVSS_SCORE, DELAY, DESCRIPTION,
|
|
3
|
+
EXTRA_DATA, FOLLOW_REDIRECT, HEADER, ID, IP,
|
|
4
|
+
MATCHED_AT, NAME, OPT_NOT_SUPPORTED, PERCENT,
|
|
5
|
+
PROVIDER, PROXY, RATE_LIMIT, REFERENCES,
|
|
6
|
+
RETRIES, SEVERITY, TAGS, THREADS, TIMEOUT,
|
|
7
|
+
USER_AGENT, DEFAULT_NUCLEI_FLAGS)
|
|
8
|
+
from secator.output_types import Progress, Vulnerability
|
|
9
|
+
from secator.tasks._categories import VulnMulti
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@task()
|
|
13
|
+
class nuclei(VulnMulti):
|
|
14
|
+
"""Fast and customisable vulnerability scanner based on simple YAML based DSL."""
|
|
15
|
+
cmd = f'nuclei {DEFAULT_NUCLEI_FLAGS}'
|
|
16
|
+
file_flag = '-l'
|
|
17
|
+
input_flag = '-u'
|
|
18
|
+
json_flag = '-jsonl'
|
|
19
|
+
opts = {
|
|
20
|
+
'templates': {'type': str, 'short': 't', 'help': 'Templates'},
|
|
21
|
+
'tags': {'type': str, 'help': 'Tags'},
|
|
22
|
+
'exclude_tags': {'type': str, 'short': 'etags', 'help': 'Exclude tags'},
|
|
23
|
+
'exclude_severity': {'type': str, 'short': 'es', 'help': 'Exclude severity'},
|
|
24
|
+
'template_id': {'type': str, 'short': 'tid', 'help': 'Template id'},
|
|
25
|
+
'debug': {'type': str, 'help': 'Debug mode'},
|
|
26
|
+
}
|
|
27
|
+
opt_key_map = {
|
|
28
|
+
HEADER: 'header',
|
|
29
|
+
DELAY: OPT_NOT_SUPPORTED,
|
|
30
|
+
FOLLOW_REDIRECT: 'follow-redirects',
|
|
31
|
+
PROXY: 'proxy',
|
|
32
|
+
RATE_LIMIT: 'rate-limit',
|
|
33
|
+
RETRIES: 'retries',
|
|
34
|
+
THREADS: 'c',
|
|
35
|
+
TIMEOUT: 'timeout',
|
|
36
|
+
USER_AGENT: OPT_NOT_SUPPORTED,
|
|
37
|
+
|
|
38
|
+
# nuclei opts
|
|
39
|
+
'exclude_tags': 'exclude-tags',
|
|
40
|
+
'exclude_severity': 'exclude-severity',
|
|
41
|
+
'templates': 't'
|
|
42
|
+
}
|
|
43
|
+
opt_value_map = {
|
|
44
|
+
'tags': lambda x: ','.join(x) if isinstance(x, list) else x,
|
|
45
|
+
'templates': lambda x: ','.join(x) if isinstance(x, list) else x,
|
|
46
|
+
'exclude_tags': lambda x: ','.join(x) if isinstance(x, list) else x,
|
|
47
|
+
}
|
|
48
|
+
output_types = [Vulnerability, Progress]
|
|
49
|
+
output_map = {
|
|
50
|
+
Vulnerability: {
|
|
51
|
+
ID: lambda x: nuclei.id_extractor(x),
|
|
52
|
+
NAME: lambda x: nuclei.name_extractor(x),
|
|
53
|
+
DESCRIPTION: lambda x: x['info'].get('description'),
|
|
54
|
+
SEVERITY: lambda x: x['info'][SEVERITY],
|
|
55
|
+
CONFIDENCE: lambda x: 'high',
|
|
56
|
+
CVSS_SCORE: lambda x: x['info'].get('classification', {}).get('cvss-score') or 0,
|
|
57
|
+
MATCHED_AT: 'matched-at',
|
|
58
|
+
IP: 'ip',
|
|
59
|
+
TAGS: lambda x: x['info']['tags'],
|
|
60
|
+
REFERENCES: lambda x: x['info'].get('reference', []),
|
|
61
|
+
EXTRA_DATA: lambda x: nuclei.extra_data_extractor(x),
|
|
62
|
+
PROVIDER: 'nuclei',
|
|
63
|
+
},
|
|
64
|
+
Progress: {
|
|
65
|
+
PERCENT: lambda x: int(x['percent']),
|
|
66
|
+
EXTRA_DATA: lambda x: {k: v for k, v in x.items() if k not in ['duration', 'errors', 'percent']}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
ignore_return_code = True
|
|
70
|
+
install_cmd = 'go install -v github.com/projectdiscovery/nuclei/v2/cmd/nuclei@latest && nuclei update-templates'
|
|
71
|
+
proxychains = False
|
|
72
|
+
proxy_socks5 = True # kind of, leaks data when running network / dns templates
|
|
73
|
+
proxy_http = True # same
|
|
74
|
+
profile = 'cpu'
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def id_extractor(item):
|
|
78
|
+
cve_ids = item['info'].get('classification', {}).get('cve-id') or []
|
|
79
|
+
if len(cve_ids) > 0:
|
|
80
|
+
return cve_ids[0]
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def extra_data_extractor(item):
|
|
85
|
+
data = {}
|
|
86
|
+
data['data'] = item.get('extracted-results', [])
|
|
87
|
+
data['template_id'] = item['template-id']
|
|
88
|
+
data['template_url'] = item.get('template-url', '')
|
|
89
|
+
return data
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def name_extractor(item):
|
|
93
|
+
name = item['template-id']
|
|
94
|
+
matcher_name = item.get('matcher-name', '')
|
|
95
|
+
if matcher_name:
|
|
96
|
+
name += f':{matcher_name}'
|
|
97
|
+
return name
|