secator 0.22.0__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.
- secator/.gitignore +162 -0
- secator/__init__.py +0 -0
- secator/celery.py +453 -0
- secator/celery_signals.py +138 -0
- secator/celery_utils.py +320 -0
- secator/cli.py +2035 -0
- secator/cli_helper.py +395 -0
- secator/click.py +87 -0
- secator/config.py +670 -0
- secator/configs/__init__.py +0 -0
- secator/configs/profiles/__init__.py +0 -0
- secator/configs/profiles/aggressive.yaml +8 -0
- secator/configs/profiles/all_ports.yaml +7 -0
- secator/configs/profiles/full.yaml +31 -0
- secator/configs/profiles/http_headless.yaml +7 -0
- secator/configs/profiles/http_record.yaml +8 -0
- secator/configs/profiles/insane.yaml +8 -0
- secator/configs/profiles/paranoid.yaml +8 -0
- secator/configs/profiles/passive.yaml +11 -0
- secator/configs/profiles/polite.yaml +8 -0
- secator/configs/profiles/sneaky.yaml +8 -0
- secator/configs/profiles/tor.yaml +5 -0
- secator/configs/scans/__init__.py +0 -0
- secator/configs/scans/domain.yaml +31 -0
- secator/configs/scans/host.yaml +23 -0
- secator/configs/scans/network.yaml +30 -0
- secator/configs/scans/subdomain.yaml +27 -0
- secator/configs/scans/url.yaml +19 -0
- secator/configs/workflows/__init__.py +0 -0
- secator/configs/workflows/cidr_recon.yaml +48 -0
- secator/configs/workflows/code_scan.yaml +29 -0
- secator/configs/workflows/domain_recon.yaml +46 -0
- secator/configs/workflows/host_recon.yaml +95 -0
- secator/configs/workflows/subdomain_recon.yaml +120 -0
- secator/configs/workflows/url_bypass.yaml +15 -0
- secator/configs/workflows/url_crawl.yaml +98 -0
- secator/configs/workflows/url_dirsearch.yaml +62 -0
- secator/configs/workflows/url_fuzz.yaml +68 -0
- secator/configs/workflows/url_params_fuzz.yaml +66 -0
- secator/configs/workflows/url_secrets_hunt.yaml +23 -0
- secator/configs/workflows/url_vuln.yaml +91 -0
- secator/configs/workflows/user_hunt.yaml +29 -0
- secator/configs/workflows/wordpress.yaml +38 -0
- secator/cve.py +718 -0
- secator/decorators.py +7 -0
- secator/definitions.py +168 -0
- secator/exporters/__init__.py +14 -0
- secator/exporters/_base.py +3 -0
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +37 -0
- secator/exporters/gdrive.py +123 -0
- secator/exporters/json.py +16 -0
- secator/exporters/table.py +36 -0
- secator/exporters/txt.py +28 -0
- secator/hooks/__init__.py +0 -0
- secator/hooks/gcs.py +80 -0
- secator/hooks/mongodb.py +281 -0
- secator/installer.py +694 -0
- secator/loader.py +128 -0
- secator/output_types/__init__.py +49 -0
- secator/output_types/_base.py +108 -0
- secator/output_types/certificate.py +78 -0
- secator/output_types/domain.py +50 -0
- secator/output_types/error.py +42 -0
- secator/output_types/exploit.py +58 -0
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +47 -0
- secator/output_types/port.py +55 -0
- secator/output_types/progress.py +36 -0
- secator/output_types/record.py +36 -0
- secator/output_types/stat.py +41 -0
- secator/output_types/state.py +29 -0
- secator/output_types/subdomain.py +45 -0
- secator/output_types/tag.py +69 -0
- secator/output_types/target.py +38 -0
- secator/output_types/url.py +112 -0
- secator/output_types/user_account.py +41 -0
- secator/output_types/vulnerability.py +101 -0
- secator/output_types/warning.py +30 -0
- secator/report.py +140 -0
- secator/rich.py +130 -0
- secator/runners/__init__.py +14 -0
- secator/runners/_base.py +1240 -0
- secator/runners/_helpers.py +218 -0
- secator/runners/celery.py +18 -0
- secator/runners/command.py +1178 -0
- secator/runners/python.py +126 -0
- secator/runners/scan.py +87 -0
- secator/runners/task.py +81 -0
- secator/runners/workflow.py +168 -0
- secator/scans/__init__.py +29 -0
- secator/serializers/__init__.py +8 -0
- secator/serializers/dataclass.py +39 -0
- secator/serializers/json.py +45 -0
- secator/serializers/regex.py +25 -0
- secator/tasks/__init__.py +8 -0
- secator/tasks/_categories.py +487 -0
- secator/tasks/arjun.py +113 -0
- secator/tasks/arp.py +53 -0
- secator/tasks/arpscan.py +70 -0
- secator/tasks/bbot.py +372 -0
- secator/tasks/bup.py +118 -0
- secator/tasks/cariddi.py +193 -0
- secator/tasks/dalfox.py +87 -0
- secator/tasks/dirsearch.py +84 -0
- secator/tasks/dnsx.py +186 -0
- secator/tasks/feroxbuster.py +93 -0
- secator/tasks/ffuf.py +135 -0
- secator/tasks/fping.py +85 -0
- secator/tasks/gau.py +102 -0
- secator/tasks/getasn.py +60 -0
- secator/tasks/gf.py +36 -0
- secator/tasks/gitleaks.py +96 -0
- secator/tasks/gospider.py +84 -0
- secator/tasks/grype.py +109 -0
- secator/tasks/h8mail.py +75 -0
- secator/tasks/httpx.py +167 -0
- secator/tasks/jswhois.py +36 -0
- secator/tasks/katana.py +203 -0
- secator/tasks/maigret.py +87 -0
- secator/tasks/mapcidr.py +42 -0
- secator/tasks/msfconsole.py +179 -0
- secator/tasks/naabu.py +85 -0
- secator/tasks/nmap.py +487 -0
- secator/tasks/nuclei.py +151 -0
- secator/tasks/search_vulns.py +225 -0
- secator/tasks/searchsploit.py +109 -0
- secator/tasks/sshaudit.py +299 -0
- secator/tasks/subfinder.py +48 -0
- secator/tasks/testssl.py +283 -0
- secator/tasks/trivy.py +130 -0
- secator/tasks/trufflehog.py +240 -0
- secator/tasks/urlfinder.py +100 -0
- secator/tasks/wafw00f.py +106 -0
- secator/tasks/whois.py +34 -0
- secator/tasks/wpprobe.py +116 -0
- secator/tasks/wpscan.py +202 -0
- secator/tasks/x8.py +94 -0
- secator/tasks/xurlfind3r.py +83 -0
- secator/template.py +294 -0
- secator/thread.py +24 -0
- secator/tree.py +196 -0
- secator/utils.py +922 -0
- secator/utils_test.py +297 -0
- secator/workflows/__init__.py +29 -0
- secator-0.22.0.dist-info/METADATA +447 -0
- secator-0.22.0.dist-info/RECORD +150 -0
- secator-0.22.0.dist-info/WHEEL +4 -0
- secator-0.22.0.dist-info/entry_points.txt +2 -0
- secator-0.22.0.dist-info/licenses/LICENSE +60 -0
secator/tasks/nmap.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import re
|
|
5
|
+
import xmltodict
|
|
6
|
+
|
|
7
|
+
from secator.config import CONFIG
|
|
8
|
+
from secator.decorators import task
|
|
9
|
+
from secator.definitions import (CONFIDENCE, CIDR_RANGE, CVSS_SCORE, DELAY,
|
|
10
|
+
DESCRIPTION, EXTRA_DATA, FOLLOW_REDIRECT,
|
|
11
|
+
HEADER, HOST, ID, IP, PROTOCOL, MATCHED_AT, NAME,
|
|
12
|
+
OPT_NOT_SUPPORTED, OUTPUT_PATH, PORT, PORTS, PROVIDER,
|
|
13
|
+
PROXY, RATE_LIMIT, REFERENCE, REFERENCES, RETRIES, SCRIPT, SERVICE_NAME,
|
|
14
|
+
SEVERITY, STATE, TAGS, THREADS, TIMEOUT, TOP_PORTS, USER_AGENT)
|
|
15
|
+
from secator.output_types import Exploit, Port, Vulnerability, Info, Error, Ip
|
|
16
|
+
from secator.tasks._categories import ReconPort, VulnMulti
|
|
17
|
+
from secator.utils import debug, traceback_as_string
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@task()
|
|
23
|
+
class nmap(ReconPort):
|
|
24
|
+
"""Network Mapper is a free and open source utility for network discovery and security auditing."""
|
|
25
|
+
cmd = 'nmap'
|
|
26
|
+
input_types = [HOST, IP, CIDR_RANGE]
|
|
27
|
+
output_types = [Port, Ip, Vulnerability, Exploit]
|
|
28
|
+
tags = ['port', 'scan']
|
|
29
|
+
input_chunk_size = 1
|
|
30
|
+
file_flag = '-iL'
|
|
31
|
+
opt_prefix = '--'
|
|
32
|
+
opts = {
|
|
33
|
+
# Script scanning
|
|
34
|
+
SCRIPT: {'type': str, 'default': None, 'help': 'NSE scripts'},
|
|
35
|
+
'script_args': {'type': str, 'short': 'sargs', 'default': None, 'help': 'NSE script arguments (n1=v1,n2=v2,...)'},
|
|
36
|
+
|
|
37
|
+
# Host discovery
|
|
38
|
+
'skip_host_discovery': {'is_flag': True, 'short': 'Pn', 'default': False, 'help': 'Skip host discovery (no ping)'},
|
|
39
|
+
|
|
40
|
+
# Service and version detection
|
|
41
|
+
'version_detection': {'is_flag': True, 'short': 'sV', 'default': False, 'help': 'Enable version detection (slow)'},
|
|
42
|
+
'detect_all': {'is_flag': True, 'short': 'A', 'default': False, 'help': 'Enable OS detection, version detection, script scanning, and traceroute on open ports'}, # noqa: E501
|
|
43
|
+
'detect_os': {'is_flag': True, 'short': 'O', 'default': False, 'help': 'Enable OS detection', 'requires_sudo': True},
|
|
44
|
+
|
|
45
|
+
# Scan techniques
|
|
46
|
+
'tcp_syn_stealth': {'is_flag': True, 'short': 'sS', 'default': False, 'help': 'TCP SYN Stealth', 'requires_sudo': True}, # noqa: E501
|
|
47
|
+
'tcp_connect': {'is_flag': True, 'short': 'sT', 'default': False, 'help': 'TCP Connect scan'},
|
|
48
|
+
'udp_scan': {'is_flag': True, 'short': 'sU', 'default': False, 'help': 'UDP scan', 'requires_sudo': True},
|
|
49
|
+
'tcp_null_scan': {'is_flag': True, 'short': 'sN', 'default': False, 'help': 'TCP Null scan', 'requires_sudo': True},
|
|
50
|
+
'tcp_fin_scan': {'is_flag': True, 'short': 'sF', 'default': False, 'help': 'TCP FIN scan', 'requires_sudo': True},
|
|
51
|
+
'tcp_xmas_scan': {'is_flag': True, 'short': 'sX', 'default': False, 'help': 'TCP Xmas scan', 'requires_sudo': True},
|
|
52
|
+
'tcp_ack_scan': {'is_flag': True, 'short': 'sA', 'default': False, 'help': 'TCP ACK scan', 'requires_sudo': True},
|
|
53
|
+
'tcp_window_scan': {'is_flag': True, 'short': 'sW', 'default': False, 'help': 'TCP Window scan', 'requires_sudo': True}, # noqa: E501
|
|
54
|
+
'tcp_maimon_scan': {'is_flag': True, 'short': 'sM', 'default': False, 'help': 'TCP Maimon scan', 'requires_sudo': True}, # noqa: E501
|
|
55
|
+
'sctp_init_scan': {'is_flag': True, 'short': 'sY', 'default': False, 'help': 'SCTP Init scan', 'requires_sudo': True},
|
|
56
|
+
'sctp_cookie_echo_scan': {'is_flag': True, 'short': 'sZ', 'default': False, 'help': 'SCTP Cookie Echo scan', 'requires_sudo': True}, # noqa: E501
|
|
57
|
+
'ping_scan': {'is_flag': True, 'short': 'sn', 'default': False, 'help': 'Ping scan (disable port scan)'},
|
|
58
|
+
'ip_protocol_scan': {'type': str, 'short': 'sO', 'default': None, 'help': 'IP protocol scan', 'requires_sudo': True},
|
|
59
|
+
'script_scan': {'is_flag': True, 'short': 'sC', 'default': False, 'help': 'Enable default scanning'},
|
|
60
|
+
'zombie_host': {'type': str, 'short': 'sI', 'default': None, 'help': 'Use a zombie host for idle scan', 'requires_sudo': True}, # noqa: E501
|
|
61
|
+
'ftp_relay_host': {'type': str, 'short': 'sB', 'default': None, 'help': 'FTP bounce scan relay host'},
|
|
62
|
+
|
|
63
|
+
# Firewall / IDS evasion and spoofing
|
|
64
|
+
'spoof_source_port': {'type': int, 'short': 'g', 'default': None, 'help': 'Send packets from a specific port'},
|
|
65
|
+
'spoof_source_ip': {'type': str, 'short': 'S', 'default': None, 'help': 'Spoof source IP address'},
|
|
66
|
+
'spoof_source_mac': {'type': str, 'short': 'spoofmac', 'default': None, 'help': 'Spoof MAC address'},
|
|
67
|
+
'fragment': {'is_flag': True, 'short': 'fragment', 'default': False, 'help': 'Fragment packets', 'requires_sudo': True}, # noqa: E501
|
|
68
|
+
'mtu': {'type': int, 'short': 'mtu', 'default': None, 'help': 'Fragment packets with given MTU', 'requires_sudo': True}, # noqa: E501
|
|
69
|
+
'ttl': {'type': int, 'short': 'ttl', 'default': None, 'help': 'Set TTL', 'requires_sudo': True},
|
|
70
|
+
'badsum': {'is_flag': True, 'short': 'badsum', 'default': False, 'help': 'Create a bad checksum in the TCP header', 'requires_sudo': True}, # noqa: E501
|
|
71
|
+
'ipv6': {'is_flag': True, 'short': 'ipv6', 'default': False, 'help': 'Enable IPv6 scanning'},
|
|
72
|
+
|
|
73
|
+
# Host discovery
|
|
74
|
+
'traceroute': {'is_flag': True, 'short': 'traceroute', 'default': False, 'help': 'Traceroute', 'requires_sudo': True},
|
|
75
|
+
'disable_arp_ping': {'is_flag': True, 'short': 'dap', 'default': False, 'help': 'Disable ARP ping'},
|
|
76
|
+
|
|
77
|
+
# Misc
|
|
78
|
+
'output_path': {'type': str, 'short': 'oX', 'default': None, 'help': 'Output XML file path', 'internal': True, 'display': False}, # noqa: E501
|
|
79
|
+
'debug': {'is_flag': True, 'short': 'd', 'default': False, 'help': 'Enable debug mode'},
|
|
80
|
+
'verbose': {'is_flag': True, 'short': 'v', 'default': False, 'help': 'Enable verbose mode'},
|
|
81
|
+
'timing': {'type': int, 'short': 'T', 'default': None, 'help': 'Timing template (0: paranoid, 1: sneaky, 2: polite, 3: normal, 4: aggressive, 5: insane)'}, # noqa: E501
|
|
82
|
+
}
|
|
83
|
+
opt_key_map = {
|
|
84
|
+
HEADER: OPT_NOT_SUPPORTED,
|
|
85
|
+
DELAY: 'scan-delay',
|
|
86
|
+
FOLLOW_REDIRECT: OPT_NOT_SUPPORTED,
|
|
87
|
+
PROXY: None, # TODO: supports --proxies but not in TCP mode [https://github.com/nmap/nmap/issues/1098]
|
|
88
|
+
RATE_LIMIT: 'max-rate',
|
|
89
|
+
RETRIES: 'max-retries',
|
|
90
|
+
THREADS: OPT_NOT_SUPPORTED,
|
|
91
|
+
TIMEOUT: 'max-rtt-timeout',
|
|
92
|
+
USER_AGENT: OPT_NOT_SUPPORTED,
|
|
93
|
+
PORTS: '-p',
|
|
94
|
+
TOP_PORTS: 'top-ports',
|
|
95
|
+
|
|
96
|
+
# Nmap opts
|
|
97
|
+
'skip_host_discovery': '-Pn',
|
|
98
|
+
'version_detection': '-sV',
|
|
99
|
+
'detect_all': '-A',
|
|
100
|
+
'detect_os': '-O',
|
|
101
|
+
'tcp_syn_stealth': '-sS',
|
|
102
|
+
'tcp_connect': '-sT',
|
|
103
|
+
'tcp_window_scan': '-sW',
|
|
104
|
+
'tcp_maimon_scan': '-sM',
|
|
105
|
+
'udp_scan': '-sU',
|
|
106
|
+
'tcp_null_scan': '-sN',
|
|
107
|
+
'tcp_fin_scan': '-sF',
|
|
108
|
+
'tcp_xmas_scan': '-sX',
|
|
109
|
+
'tcp_ack_scan': '-sA',
|
|
110
|
+
'sctp_init_scan': '-sY',
|
|
111
|
+
'sctp_cookie_echo_scan': '-sZ',
|
|
112
|
+
'ping_scan': '-sn',
|
|
113
|
+
'ip_protocol_scan': '-sO',
|
|
114
|
+
'script_scan': '-sC',
|
|
115
|
+
'zombie_host': '-sI',
|
|
116
|
+
'ftp_relay_host': '-b',
|
|
117
|
+
'spoof_source_port': '-g',
|
|
118
|
+
'spoof_source_ip': '-S',
|
|
119
|
+
'spoof_source_mac': '--spoof-mac',
|
|
120
|
+
'fragment': '-f',
|
|
121
|
+
'mtu': '--mtu',
|
|
122
|
+
'ttl': '--ttl',
|
|
123
|
+
'badsum': '--badsum',
|
|
124
|
+
'ipv6': '-6',
|
|
125
|
+
'traceroute': '--traceroute',
|
|
126
|
+
'disable_arp_ping': '--disable-arp-ping',
|
|
127
|
+
'output_path': '-oX',
|
|
128
|
+
}
|
|
129
|
+
opt_value_map = {
|
|
130
|
+
PORTS: lambda x: ','.join([str(p) for p in x]) if isinstance(x, list) else x
|
|
131
|
+
}
|
|
132
|
+
install_cmd_pre = {
|
|
133
|
+
'apt|pacman|brew': ['nmap'],
|
|
134
|
+
'apk': ['nmap', 'nmap-scripts'],
|
|
135
|
+
}
|
|
136
|
+
install_cmd = (
|
|
137
|
+
'sudo git clone --depth 1 --single-branch https://github.com/scipag/vulscan /opt/scipag_vulscan || true && '
|
|
138
|
+
'sudo ln -s /opt/scipag_vulscan /usr/share/nmap/scripts/vulscan || true'
|
|
139
|
+
)
|
|
140
|
+
proxychains = True
|
|
141
|
+
proxychains_flavor = 'proxychains4'
|
|
142
|
+
proxy_socks5 = False
|
|
143
|
+
proxy_http = False
|
|
144
|
+
profile = 'io'
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def on_cmd(self):
|
|
148
|
+
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
149
|
+
if not output_path:
|
|
150
|
+
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.xml'
|
|
151
|
+
self.output_path = output_path
|
|
152
|
+
self.cmd += f' -oX {shlex.quote(self.output_path)}'
|
|
153
|
+
tcp_syn_stealth = self.cmd_options.get('tcp_syn_stealth')
|
|
154
|
+
tcp_connect = self.cmd_options.get('tcp_connect')
|
|
155
|
+
if tcp_connect and tcp_syn_stealth:
|
|
156
|
+
self._print(
|
|
157
|
+
'Options -sT (SYN stealth scan) and -sS (CONNECT scan) are conflicting. Keeping only -sS.',
|
|
158
|
+
'bold gold3')
|
|
159
|
+
self.cmd = self.cmd.replace('-sT ', '')
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def on_cmd_done(self):
|
|
163
|
+
if not os.path.exists(self.output_path):
|
|
164
|
+
yield Error(message=f'Could not find XML results in {self.output_path}')
|
|
165
|
+
return
|
|
166
|
+
yield Info(message=f'XML results saved to {self.output_path}')
|
|
167
|
+
yield from self.xml_to_json()
|
|
168
|
+
|
|
169
|
+
def xml_to_json(self):
|
|
170
|
+
results = []
|
|
171
|
+
with open(self.output_path, 'r') as f:
|
|
172
|
+
content = f.read()
|
|
173
|
+
try:
|
|
174
|
+
results = xmltodict.parse(content) # parse XML to dict
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
yield Error(
|
|
177
|
+
message=f'Cannot parse XML output {self.output_path} to valid JSON.',
|
|
178
|
+
traceback=traceback_as_string(exc)
|
|
179
|
+
)
|
|
180
|
+
yield from nmapData(results)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class nmapData(dict):
|
|
184
|
+
|
|
185
|
+
def __iter__(self):
|
|
186
|
+
datas = []
|
|
187
|
+
ips = []
|
|
188
|
+
for host in self._get_hosts():
|
|
189
|
+
hostname = self._get_hostname(host)
|
|
190
|
+
ip = self._get_ip(host)
|
|
191
|
+
if ip and ip not in ips:
|
|
192
|
+
yield Ip(ip=ip, alive=True, host=hostname, extra_data={'protocol': 'tcp'})
|
|
193
|
+
ips.append(ip)
|
|
194
|
+
for port in self._get_ports(host):
|
|
195
|
+
# Get port number
|
|
196
|
+
port_number = port['@portid']
|
|
197
|
+
if not port_number or not port_number.isdigit():
|
|
198
|
+
continue
|
|
199
|
+
port_number = int(port_number)
|
|
200
|
+
|
|
201
|
+
# Get port state
|
|
202
|
+
state = port.get('state', {}).get('@state', '')
|
|
203
|
+
|
|
204
|
+
# Get extra data
|
|
205
|
+
extra_data = self._get_extra_data(port)
|
|
206
|
+
service_name = extra_data.get('service_name', '')
|
|
207
|
+
version_exact = extra_data.get('version_exact', False)
|
|
208
|
+
conf = extra_data.get('confidence')
|
|
209
|
+
|
|
210
|
+
# Grab CPEs
|
|
211
|
+
cpes = extra_data.get('cpe', [])
|
|
212
|
+
|
|
213
|
+
# Get script output
|
|
214
|
+
scripts = self._get_scripts(port)
|
|
215
|
+
|
|
216
|
+
# Get port protocol
|
|
217
|
+
protocol = port['@protocol'].lower()
|
|
218
|
+
|
|
219
|
+
# Yield port data
|
|
220
|
+
port = {
|
|
221
|
+
PORT: port_number,
|
|
222
|
+
HOST: hostname,
|
|
223
|
+
STATE: state,
|
|
224
|
+
SERVICE_NAME: service_name,
|
|
225
|
+
IP: ip,
|
|
226
|
+
PROTOCOL: protocol,
|
|
227
|
+
EXTRA_DATA: extra_data,
|
|
228
|
+
CONFIDENCE: conf
|
|
229
|
+
}
|
|
230
|
+
yield Port(**port)
|
|
231
|
+
|
|
232
|
+
# Parse each script output to get vulns
|
|
233
|
+
for script in scripts:
|
|
234
|
+
script_id = script['id']
|
|
235
|
+
output = script['output']
|
|
236
|
+
extra_data = {'script': script_id}
|
|
237
|
+
if service_name:
|
|
238
|
+
extra_data['service_name'] = service_name
|
|
239
|
+
funcmap = {
|
|
240
|
+
'vulscan': self._parse_vulscan_output,
|
|
241
|
+
'vulners': self._parse_vulners_output,
|
|
242
|
+
}
|
|
243
|
+
func = funcmap.get(script_id)
|
|
244
|
+
metadata = {
|
|
245
|
+
MATCHED_AT: f'{hostname}:{port_number}',
|
|
246
|
+
IP: ip,
|
|
247
|
+
EXTRA_DATA: extra_data,
|
|
248
|
+
}
|
|
249
|
+
if not func:
|
|
250
|
+
debug(f'Script output parser for "{script_id}" is not supported YET.', sub='cve.nmap')
|
|
251
|
+
continue
|
|
252
|
+
for data in func(output, cpes=cpes):
|
|
253
|
+
data.update(metadata)
|
|
254
|
+
confidence = 'low'
|
|
255
|
+
if 'cpe-match' in data[TAGS]:
|
|
256
|
+
confidence = 'high' if version_exact else 'medium'
|
|
257
|
+
data[CONFIDENCE] = confidence
|
|
258
|
+
if (CONFIG.runners.skip_cve_low_confidence and data[CONFIDENCE] == 'low'):
|
|
259
|
+
debug(f'{data[ID]}: ignored (low confidence).', sub='cve.nmap')
|
|
260
|
+
continue
|
|
261
|
+
if data in datas:
|
|
262
|
+
continue
|
|
263
|
+
yield data
|
|
264
|
+
datas.append(data)
|
|
265
|
+
|
|
266
|
+
#---------------------#
|
|
267
|
+
# XML FILE EXTRACTORS #
|
|
268
|
+
#---------------------#
|
|
269
|
+
def _get_hosts(self):
|
|
270
|
+
hosts = self.get('nmaprun', {}).get('host', {})
|
|
271
|
+
if isinstance(hosts, dict):
|
|
272
|
+
hosts = [hosts]
|
|
273
|
+
return hosts
|
|
274
|
+
|
|
275
|
+
def _get_ports(self, host_cfg):
|
|
276
|
+
ports = host_cfg.get('ports', {}).get('port', [])
|
|
277
|
+
if isinstance(ports, dict):
|
|
278
|
+
ports = [ports]
|
|
279
|
+
return ports
|
|
280
|
+
|
|
281
|
+
def _get_hostname(self, host_cfg):
|
|
282
|
+
hostnames = host_cfg.get('hostnames', {})
|
|
283
|
+
if hostnames:
|
|
284
|
+
hostnames = hostnames.get('hostname', [])
|
|
285
|
+
if isinstance(hostnames, dict):
|
|
286
|
+
hostnames = [hostnames]
|
|
287
|
+
if hostnames:
|
|
288
|
+
hostname = hostnames[0]['@name']
|
|
289
|
+
else:
|
|
290
|
+
hostname = self._get_address(host_cfg).get('@addr', None)
|
|
291
|
+
return hostname
|
|
292
|
+
|
|
293
|
+
def _get_address(self, host_cfg):
|
|
294
|
+
if isinstance(host_cfg.get('address', {}), list):
|
|
295
|
+
addresses = host_cfg.get('address', {})
|
|
296
|
+
for address in addresses:
|
|
297
|
+
if address.get('@addrtype') == "ipv4":
|
|
298
|
+
return address
|
|
299
|
+
return host_cfg.get('address', {})
|
|
300
|
+
|
|
301
|
+
def _get_ip(self, host_cfg):
|
|
302
|
+
return self._get_address(host_cfg).get('@addr', None)
|
|
303
|
+
|
|
304
|
+
def _get_extra_data(self, port_cfg):
|
|
305
|
+
extra_data = {
|
|
306
|
+
k.lstrip('@'): v
|
|
307
|
+
for k, v in port_cfg.get('service', {}).items()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Strip product / version strings
|
|
311
|
+
if 'product' in extra_data:
|
|
312
|
+
extra_data['product'] = extra_data['product'].lower()
|
|
313
|
+
|
|
314
|
+
# Get version and post-process it
|
|
315
|
+
version = None
|
|
316
|
+
if 'version' in extra_data:
|
|
317
|
+
vsplit = extra_data['version'].split(' ')
|
|
318
|
+
version_exact = True
|
|
319
|
+
os = None
|
|
320
|
+
if len(vsplit) == 3:
|
|
321
|
+
version, os, extra_version = tuple(vsplit)
|
|
322
|
+
if os == 'or' and extra_version == 'later':
|
|
323
|
+
version_exact = False
|
|
324
|
+
os = None
|
|
325
|
+
version = f'{version}-{extra_version}'
|
|
326
|
+
elif len(vsplit) == 2:
|
|
327
|
+
version, os = tuple(vsplit)
|
|
328
|
+
elif len(vsplit) == 1:
|
|
329
|
+
version = vsplit[0]
|
|
330
|
+
else:
|
|
331
|
+
version = extra_data['version']
|
|
332
|
+
if os:
|
|
333
|
+
extra_data['os'] = os
|
|
334
|
+
if version:
|
|
335
|
+
extra_data['version'] = version
|
|
336
|
+
extra_data['version_exact'] = version_exact
|
|
337
|
+
|
|
338
|
+
# Grap service name
|
|
339
|
+
product = extra_data.get('product', None) or extra_data.get('name', None)
|
|
340
|
+
if product:
|
|
341
|
+
service_name = product
|
|
342
|
+
if version:
|
|
343
|
+
service_name += f'/{version}'
|
|
344
|
+
extra_data['service_name'] = service_name
|
|
345
|
+
|
|
346
|
+
# Grab CPEs
|
|
347
|
+
cpes = extra_data.get('cpe', [])
|
|
348
|
+
if not isinstance(cpes, list):
|
|
349
|
+
cpes = [cpes]
|
|
350
|
+
extra_data['cpe'] = cpes
|
|
351
|
+
debug(f'Found CPEs: {",".join(cpes)}', sub='cve.nmap')
|
|
352
|
+
|
|
353
|
+
# Grab confidence
|
|
354
|
+
conf = int(extra_data.get('conf', 0))
|
|
355
|
+
if conf > 7:
|
|
356
|
+
confidence = 'high'
|
|
357
|
+
elif conf > 4:
|
|
358
|
+
confidence = 'medium'
|
|
359
|
+
else:
|
|
360
|
+
confidence = 'low'
|
|
361
|
+
extra_data['confidence'] = confidence
|
|
362
|
+
|
|
363
|
+
# Build custom CPE
|
|
364
|
+
if product and version:
|
|
365
|
+
vsplit = version.split('-')
|
|
366
|
+
version_cpe = vsplit[0] if not version_exact else version
|
|
367
|
+
cpe = VulnMulti.create_cpe_string(product, version_cpe)
|
|
368
|
+
if cpe not in cpes:
|
|
369
|
+
cpes.append(cpe)
|
|
370
|
+
debug(f'Added new CPE from identified product and version: {cpe}', sub='cve.nmap')
|
|
371
|
+
|
|
372
|
+
return extra_data
|
|
373
|
+
|
|
374
|
+
def _get_scripts(self, port_cfg):
|
|
375
|
+
scripts = port_cfg.get('script', [])
|
|
376
|
+
if isinstance(scripts, dict):
|
|
377
|
+
scripts = [scripts]
|
|
378
|
+
scripts = [
|
|
379
|
+
{k.lstrip('@'): v for k, v in script.items()}
|
|
380
|
+
for script in scripts
|
|
381
|
+
]
|
|
382
|
+
return scripts
|
|
383
|
+
|
|
384
|
+
#--------------#
|
|
385
|
+
# VULN PARSERS #
|
|
386
|
+
#--------------#
|
|
387
|
+
def _parse_vulscan_output(self, out, cpes=[]):
|
|
388
|
+
"""Parse nmap vulscan script output.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
out (str): Vulscan script output.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
list: List of Vulnerability dicts.
|
|
395
|
+
"""
|
|
396
|
+
provider_name = ''
|
|
397
|
+
for line in out.splitlines():
|
|
398
|
+
if not line:
|
|
399
|
+
continue
|
|
400
|
+
line = line.strip()
|
|
401
|
+
if not line.startswith('[') and line != 'No findings': # provider line
|
|
402
|
+
provider_name, _ = tuple(line.split(' - '))
|
|
403
|
+
continue
|
|
404
|
+
reg = r'\[([ A-Za-z0-9_@./#&+-]*)\] (.*)'
|
|
405
|
+
matches = re.match(reg, line)
|
|
406
|
+
if not matches:
|
|
407
|
+
continue
|
|
408
|
+
vuln_id, vuln_title = matches.groups()
|
|
409
|
+
vuln = {
|
|
410
|
+
ID: vuln_id,
|
|
411
|
+
NAME: vuln_id,
|
|
412
|
+
DESCRIPTION: vuln_title,
|
|
413
|
+
PROVIDER: provider_name,
|
|
414
|
+
TAGS: [provider_name]
|
|
415
|
+
}
|
|
416
|
+
if provider_name == 'MITRE CVE':
|
|
417
|
+
data = VulnMulti.lookup_cve(vuln['id'], *cpes)
|
|
418
|
+
if data:
|
|
419
|
+
vuln.update(data)
|
|
420
|
+
yield vuln
|
|
421
|
+
else:
|
|
422
|
+
debug(f'Vulscan provider {provider_name} is not supported YET.', sub='cve.provider', verbose=True)
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
def _parse_vulners_output(self, out, **kwargs):
|
|
426
|
+
cpes = kwargs.get('cpes', [])
|
|
427
|
+
provider_name = 'vulners'
|
|
428
|
+
for line in out.splitlines():
|
|
429
|
+
if not line:
|
|
430
|
+
continue
|
|
431
|
+
line = line.strip()
|
|
432
|
+
if line.startswith('cpe:'):
|
|
433
|
+
cpes.append(line.rstrip(':'))
|
|
434
|
+
continue
|
|
435
|
+
elems = tuple(line.split('\t'))
|
|
436
|
+
|
|
437
|
+
if len(elems) == 4: # exploit
|
|
438
|
+
exploit_id, cvss_score, reference_url, _ = elems
|
|
439
|
+
name = exploit_id
|
|
440
|
+
# edb_id = name.split(':')[-1] if 'EDB-ID' in name else None
|
|
441
|
+
exploit = {
|
|
442
|
+
ID: exploit_id,
|
|
443
|
+
NAME: name,
|
|
444
|
+
PROVIDER: provider_name,
|
|
445
|
+
REFERENCE: reference_url,
|
|
446
|
+
TAGS: [exploit_id, provider_name],
|
|
447
|
+
CVSS_SCORE: cvss_score,
|
|
448
|
+
CONFIDENCE: 'low',
|
|
449
|
+
'_type': 'exploit',
|
|
450
|
+
}
|
|
451
|
+
# TODO: lookup exploit in ExploitDB to find related CVEs
|
|
452
|
+
# if edb_id:
|
|
453
|
+
# print(edb_id)
|
|
454
|
+
# exploit_data = VulnMulti.lookup_exploitdb(edb_id)
|
|
455
|
+
vuln = VulnMulti.lookup_cve_from_vulners_exploit(exploit_id, *cpes)
|
|
456
|
+
if vuln:
|
|
457
|
+
yield vuln
|
|
458
|
+
exploit[TAGS].extend(vuln[TAGS])
|
|
459
|
+
exploit[CONFIDENCE] = vuln[CONFIDENCE]
|
|
460
|
+
yield exploit
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
elif len(elems) == 3: # vuln
|
|
464
|
+
vuln = {}
|
|
465
|
+
vuln_id, vuln_cvss, reference_url = tuple(line.split('\t'))
|
|
466
|
+
vuln_cvss = float(vuln_cvss)
|
|
467
|
+
vuln_id = vuln_id.split(':')[-1]
|
|
468
|
+
vuln_type = vuln_id.split('-')[0]
|
|
469
|
+
vuln = {
|
|
470
|
+
ID: vuln_id,
|
|
471
|
+
NAME: vuln_id,
|
|
472
|
+
PROVIDER: provider_name,
|
|
473
|
+
CVSS_SCORE: vuln_cvss,
|
|
474
|
+
SEVERITY: VulnMulti.cvss_to_severity(vuln_cvss),
|
|
475
|
+
REFERENCES: [reference_url],
|
|
476
|
+
TAGS: [vuln_id, provider_name],
|
|
477
|
+
CONFIDENCE: 'low'
|
|
478
|
+
}
|
|
479
|
+
if vuln_type == 'CVE' or vuln_type == 'PRION:CVE':
|
|
480
|
+
data = VulnMulti.lookup_cve(vuln_id, *cpes)
|
|
481
|
+
if data:
|
|
482
|
+
vuln.update(data)
|
|
483
|
+
yield vuln
|
|
484
|
+
else:
|
|
485
|
+
debug(f'Vulners parser for "{vuln_type}" is not implemented YET.', sub='cve.nmap')
|
|
486
|
+
else:
|
|
487
|
+
debug(f'Unrecognized vulners output: {elems}', sub='cve.nmap')
|
secator/tasks/nuclei.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
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, HOST, URL)
|
|
8
|
+
from secator.output_types import Progress, Tag, Vulnerability
|
|
9
|
+
from secator.serializers import JSONSerializer
|
|
10
|
+
from secator.tasks._categories import VulnMulti
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def output_discriminator(self, item):
|
|
14
|
+
"""Discriminate between Tag and Vulnerability based on severity."""
|
|
15
|
+
if 'percent' in item:
|
|
16
|
+
return Progress
|
|
17
|
+
severity = item.get('info', {}).get('severity', '').lower()
|
|
18
|
+
if severity == 'info':
|
|
19
|
+
return Tag
|
|
20
|
+
return Vulnerability
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@task()
|
|
24
|
+
class nuclei(VulnMulti):
|
|
25
|
+
"""Fast and customisable vulnerability scanner based on simple YAML based DSL."""
|
|
26
|
+
cmd = 'nuclei'
|
|
27
|
+
input_types = [HOST, IP, URL]
|
|
28
|
+
output_types = [Vulnerability, Tag, Progress]
|
|
29
|
+
tags = ['vuln', 'scan']
|
|
30
|
+
file_flag = '-l'
|
|
31
|
+
input_flag = '-u'
|
|
32
|
+
json_flag = '-jsonl'
|
|
33
|
+
input_chunk_size = 20
|
|
34
|
+
opts = {
|
|
35
|
+
'bulk_size': {'type': int, 'short': 'bs', 'help': 'Maximum number of hosts to be analyzed in parallel per template'}, # noqa: E501
|
|
36
|
+
'debug': {'type': str, 'help': 'Debug mode'},
|
|
37
|
+
'exclude_severity': {'type': str, 'short': 'es', 'help': 'Exclude severity'},
|
|
38
|
+
'exclude_tags': {'type': str, 'short': 'etags', 'help': 'Exclude tags'},
|
|
39
|
+
'input_mode': {'type': str, 'short': 'im', 'help': 'Mode of input file (list, burp, jsonl, yaml, openapi, swagger)'},
|
|
40
|
+
'hang_monitor': {'is_flag': True, 'short': 'hm', 'default': True, 'help': 'Enable nuclei hang monitoring'},
|
|
41
|
+
'headless_bulk_size': {'type': int, 'short': 'hbs', 'help': 'Maximum number of headless hosts to be analzyed in parallel per template'}, # noqa: E501
|
|
42
|
+
'new_templates': {'type': str, 'short': 'nt', 'help': 'Run only new templates added in latest nuclei-templates release'}, # noqa: E501
|
|
43
|
+
'automatic_scan': {'is_flag': True, 'short': 'as', 'help': 'Automatic web scan using wappalyzer technology detection to tags mapping'}, # noqa: E501
|
|
44
|
+
'omit_raw': {'is_flag': True, 'short': 'or', 'default': True, 'help': 'Omit requests/response pairs in the JSON, JSONL, and Markdown outputs (for findings only)'}, # noqa: E501
|
|
45
|
+
'response_size_read': {'type': int, 'help': 'Max body size to read (bytes)'},
|
|
46
|
+
'stats': {'is_flag': True, 'short': 'stats', 'default': True, 'help': 'Display statistics about the running scan'},
|
|
47
|
+
'stats_json': {'is_flag': True, 'short': 'sj', 'default': True, 'help': 'Display statistics in JSONL(ines) format'},
|
|
48
|
+
'stats_interval': {'type': str, 'short': 'si', 'help': 'Number of seconds to wait between showing a statistics update'}, # noqa: E501
|
|
49
|
+
'tags': {'type': str, 'help': 'Tags'},
|
|
50
|
+
'templates': {'type': str, 'short': 't', 'help': 'Templates'},
|
|
51
|
+
'template_id': {'type': str, 'short': 'tid', 'help': 'Template id'},
|
|
52
|
+
}
|
|
53
|
+
opt_key_map = {
|
|
54
|
+
HEADER: 'header',
|
|
55
|
+
DELAY: OPT_NOT_SUPPORTED,
|
|
56
|
+
FOLLOW_REDIRECT: 'follow-redirects',
|
|
57
|
+
PROXY: 'proxy',
|
|
58
|
+
RATE_LIMIT: 'rate-limit',
|
|
59
|
+
RETRIES: 'retries',
|
|
60
|
+
THREADS: 'c',
|
|
61
|
+
TIMEOUT: 'timeout',
|
|
62
|
+
USER_AGENT: OPT_NOT_SUPPORTED,
|
|
63
|
+
|
|
64
|
+
# nuclei opts
|
|
65
|
+
'exclude_tags': 'exclude-tags',
|
|
66
|
+
'exclude_severity': 'exclude-severity',
|
|
67
|
+
'templates': 't',
|
|
68
|
+
'response_size_read': 'rsr'
|
|
69
|
+
}
|
|
70
|
+
opt_value_map = {
|
|
71
|
+
'tags': lambda x: ','.join(x) if isinstance(x, list) else x,
|
|
72
|
+
'templates': lambda x: ','.join(x) if isinstance(x, list) else x,
|
|
73
|
+
'exclude_tags': lambda x: ','.join(x) if isinstance(x, list) else x,
|
|
74
|
+
}
|
|
75
|
+
item_loaders = [JSONSerializer()]
|
|
76
|
+
output_discriminator = output_discriminator
|
|
77
|
+
output_map = {
|
|
78
|
+
Vulnerability: {
|
|
79
|
+
ID: lambda x: nuclei.id_extractor(x),
|
|
80
|
+
NAME: lambda x: nuclei.name_extractor(x),
|
|
81
|
+
DESCRIPTION: lambda x: x['info'].get('description'),
|
|
82
|
+
SEVERITY: lambda x: x['info'][SEVERITY],
|
|
83
|
+
CONFIDENCE: lambda x: 'high',
|
|
84
|
+
CVSS_SCORE: lambda x: x['info'].get('classification', {}).get('cvss-score') or 0,
|
|
85
|
+
MATCHED_AT: 'matched-at',
|
|
86
|
+
IP: 'ip',
|
|
87
|
+
TAGS: lambda x: x['info']['tags'],
|
|
88
|
+
REFERENCES: lambda x: x['info'].get('reference', []),
|
|
89
|
+
EXTRA_DATA: lambda x: nuclei.extra_data_extractor(x),
|
|
90
|
+
PROVIDER: 'nuclei',
|
|
91
|
+
},
|
|
92
|
+
Tag: {
|
|
93
|
+
NAME: lambda x: nuclei.name_extractor(x),
|
|
94
|
+
'match': 'matched-at',
|
|
95
|
+
'value': lambda x: nuclei.value_extractor(x),
|
|
96
|
+
'category': lambda x: 'info',
|
|
97
|
+
EXTRA_DATA: lambda x: nuclei.extra_data_extractor(x, with_tags=True),
|
|
98
|
+
'_source': 'nuclei',
|
|
99
|
+
},
|
|
100
|
+
Progress: {
|
|
101
|
+
PERCENT: lambda x: int(x['percent']),
|
|
102
|
+
EXTRA_DATA: lambda x: {k: v for k, v in x.items() if k not in ['percent']}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
install_version = 'v3.4.2'
|
|
106
|
+
install_pre = {'*': ['git']}
|
|
107
|
+
install_cmd = 'go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@[install_version]'
|
|
108
|
+
github_handle = 'projectdiscovery/nuclei'
|
|
109
|
+
install_post = {
|
|
110
|
+
'*': 'nuclei -ut'
|
|
111
|
+
}
|
|
112
|
+
proxychains = False
|
|
113
|
+
proxy_socks5 = True # kind of, leaks data when running network / dns templates
|
|
114
|
+
proxy_http = True # same
|
|
115
|
+
profile = 'cpu'
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def id_extractor(item):
|
|
119
|
+
cve_ids = item['info'].get('classification', {}).get('cve-id') or []
|
|
120
|
+
if len(cve_ids) > 0:
|
|
121
|
+
return cve_ids[0]
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def extra_data_extractor(item, with_tags=False):
|
|
126
|
+
data = {}
|
|
127
|
+
data['data'] = item.get('extracted-results', [])
|
|
128
|
+
data['type'] = item.get('type', '')
|
|
129
|
+
data['template_id'] = item['template-id']
|
|
130
|
+
data['template_url'] = item.get('template-url', '')
|
|
131
|
+
for k, v in item.get('meta', {}).items():
|
|
132
|
+
data['data'].append(f'{k}: {v}')
|
|
133
|
+
data['metadata'] = item.get('metadata', {})
|
|
134
|
+
if with_tags:
|
|
135
|
+
data['tags'] = item.get('info', {}).get('tags', [])
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def value_extractor(item):
|
|
140
|
+
values = item.get('extracted-results', '')
|
|
141
|
+
if isinstance(values, list):
|
|
142
|
+
return '\n'.join(values)
|
|
143
|
+
return values
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def name_extractor(item):
|
|
147
|
+
name = item['template-id']
|
|
148
|
+
matcher_name = item.get('matcher-name', '')
|
|
149
|
+
if matcher_name:
|
|
150
|
+
name += f':{matcher_name}'
|
|
151
|
+
return name
|