secator 0.6.0__py3-none-any.whl → 0.8.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.
Potentially problematic release.
This version of secator might be problematic. Click here for more details.
- secator/celery.py +160 -185
- secator/celery_utils.py +268 -0
- secator/cli.py +427 -176
- secator/config.py +114 -68
- secator/configs/workflows/host_recon.yaml +5 -3
- secator/configs/workflows/port_scan.yaml +7 -3
- secator/configs/workflows/subdomain_recon.yaml +2 -2
- secator/configs/workflows/url_bypass.yaml +10 -0
- secator/configs/workflows/url_dirsearch.yaml +1 -1
- secator/configs/workflows/url_vuln.yaml +1 -1
- secator/decorators.py +170 -92
- secator/definitions.py +11 -4
- secator/exporters/__init__.py +7 -5
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +27 -19
- secator/exporters/gdrive.py +16 -11
- secator/exporters/json.py +3 -1
- secator/exporters/table.py +30 -2
- secator/exporters/txt.py +20 -16
- secator/hooks/gcs.py +53 -0
- secator/hooks/mongodb.py +53 -27
- secator/installer.py +277 -60
- secator/output_types/__init__.py +29 -11
- secator/output_types/_base.py +11 -1
- secator/output_types/error.py +36 -0
- secator/output_types/exploit.py +12 -8
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +8 -1
- secator/output_types/port.py +9 -2
- secator/output_types/progress.py +5 -0
- secator/output_types/record.py +5 -3
- secator/output_types/stat.py +33 -0
- secator/output_types/subdomain.py +1 -1
- secator/output_types/tag.py +8 -6
- secator/output_types/target.py +2 -2
- secator/output_types/url.py +14 -11
- secator/output_types/user_account.py +6 -6
- secator/output_types/vulnerability.py +8 -6
- secator/output_types/warning.py +24 -0
- secator/report.py +56 -23
- secator/rich.py +44 -39
- secator/runners/_base.py +629 -638
- secator/runners/_helpers.py +5 -91
- secator/runners/celery.py +18 -0
- secator/runners/command.py +404 -214
- secator/runners/scan.py +8 -24
- secator/runners/task.py +21 -55
- secator/runners/workflow.py +41 -40
- secator/scans/__init__.py +28 -0
- secator/serializers/dataclass.py +6 -0
- secator/serializers/json.py +10 -5
- secator/serializers/regex.py +12 -4
- secator/tasks/_categories.py +147 -42
- secator/tasks/bbot.py +295 -0
- secator/tasks/bup.py +99 -0
- secator/tasks/cariddi.py +38 -49
- secator/tasks/dalfox.py +3 -0
- secator/tasks/dirsearch.py +14 -25
- secator/tasks/dnsx.py +49 -30
- secator/tasks/dnsxbrute.py +4 -1
- secator/tasks/feroxbuster.py +10 -20
- secator/tasks/ffuf.py +3 -2
- secator/tasks/fping.py +4 -4
- secator/tasks/gau.py +5 -0
- secator/tasks/gf.py +2 -2
- secator/tasks/gospider.py +4 -0
- secator/tasks/grype.py +11 -13
- secator/tasks/h8mail.py +32 -42
- secator/tasks/httpx.py +58 -21
- secator/tasks/katana.py +19 -23
- secator/tasks/maigret.py +27 -25
- secator/tasks/mapcidr.py +2 -3
- secator/tasks/msfconsole.py +22 -19
- secator/tasks/naabu.py +18 -2
- secator/tasks/nmap.py +82 -55
- secator/tasks/nuclei.py +13 -3
- secator/tasks/searchsploit.py +26 -11
- secator/tasks/subfinder.py +5 -1
- secator/tasks/wpscan.py +91 -94
- secator/template.py +61 -45
- secator/thread.py +24 -0
- secator/utils.py +417 -78
- secator/utils_test.py +48 -23
- secator/workflows/__init__.py +28 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/METADATA +59 -48
- secator-0.8.0.dist-info/RECORD +115 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/WHEEL +1 -1
- secator-0.6.0.dist-info/RECORD +0 -101
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/entry_points.txt +0 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/licenses/LICENSE +0 -0
secator/tasks/nmap.py
CHANGED
|
@@ -8,15 +8,14 @@ from secator.config import CONFIG
|
|
|
8
8
|
from secator.decorators import task
|
|
9
9
|
from secator.definitions import (CONFIDENCE, CVSS_SCORE, DELAY,
|
|
10
10
|
DESCRIPTION, EXTRA_DATA, FOLLOW_REDIRECT,
|
|
11
|
-
HEADER, HOST, ID, IP, MATCHED_AT, NAME,
|
|
11
|
+
HEADER, HOST, ID, IP, PROTOCOL, MATCHED_AT, NAME,
|
|
12
12
|
OPT_NOT_SUPPORTED, OUTPUT_PATH, PORT, PORTS, PROVIDER,
|
|
13
13
|
PROXY, RATE_LIMIT, REFERENCE, REFERENCES,
|
|
14
14
|
RETRIES, SCRIPT, SERVICE_NAME, SEVERITY, STATE, TAGS,
|
|
15
15
|
THREADS, TIMEOUT, TOP_PORTS, USER_AGENT)
|
|
16
|
-
from secator.output_types import Exploit, Port, Vulnerability
|
|
17
|
-
from secator.rich import console
|
|
16
|
+
from secator.output_types import Exploit, Port, Vulnerability, Info, Error
|
|
18
17
|
from secator.tasks._categories import VulnMulti
|
|
19
|
-
from secator.utils import debug
|
|
18
|
+
from secator.utils import debug, traceback_as_string
|
|
20
19
|
|
|
21
20
|
logger = logging.getLogger(__name__)
|
|
22
21
|
|
|
@@ -24,7 +23,7 @@ logger = logging.getLogger(__name__)
|
|
|
24
23
|
@task()
|
|
25
24
|
class nmap(VulnMulti):
|
|
26
25
|
"""Network Mapper is a free and open source utility for network discovery and security auditing."""
|
|
27
|
-
cmd = 'nmap
|
|
26
|
+
cmd = 'nmap'
|
|
28
27
|
input_flag = None
|
|
29
28
|
input_chunk_size = 1
|
|
30
29
|
file_flag = '-iL'
|
|
@@ -34,8 +33,11 @@ class nmap(VulnMulti):
|
|
|
34
33
|
PORTS: {'type': str, 'short': 'p', 'help': 'Ports to scan'},
|
|
35
34
|
TOP_PORTS: {'type': int, 'short': 'tp', 'help': 'Top ports to scan [full, 100, 1000]'},
|
|
36
35
|
SCRIPT: {'type': str, 'default': 'vulners', 'help': 'NSE scripts'},
|
|
37
|
-
|
|
36
|
+
'skip_host_discovery': {'is_flag': True, 'short': 'Pn', 'default': False, 'help': 'Skip host discovery (no ping)'},
|
|
37
|
+
'version_detection': {'is_flag': True, 'short': 'sV', 'default': False, 'help': 'Version detection'},
|
|
38
38
|
'tcp_syn_stealth': {'is_flag': True, 'short': 'sS', 'default': False, 'help': 'TCP SYN Stealth'},
|
|
39
|
+
'tcp_connect': {'is_flag': True, 'short': 'sT', 'default': False, 'help': 'TCP Connect scan'},
|
|
40
|
+
'udp_scan': {'is_flag': True, 'short': 'sU', 'default': False, 'help': 'UDP scan'},
|
|
39
41
|
'output_path': {'type': str, 'short': 'oX', 'default': None, 'help': 'Output XML file path'},
|
|
40
42
|
}
|
|
41
43
|
opt_key_map = {
|
|
@@ -51,14 +53,22 @@ class nmap(VulnMulti):
|
|
|
51
53
|
|
|
52
54
|
# Nmap opts
|
|
53
55
|
PORTS: '-p',
|
|
56
|
+
'skip_host_discovery': '-Pn',
|
|
57
|
+
'version_detection': '-sV',
|
|
58
|
+
'tcp_connect': '-sT',
|
|
59
|
+
'tcp_syn_stealth': '-sS',
|
|
60
|
+
'udp_scan': '-sU',
|
|
54
61
|
'output_path': '-oX',
|
|
55
|
-
'tcp_syn_stealth': '-sS'
|
|
56
62
|
}
|
|
57
63
|
opt_value_map = {
|
|
58
64
|
PORTS: lambda x: ','.join([str(p) for p in x]) if isinstance(x, list) else x
|
|
59
65
|
}
|
|
66
|
+
install_pre = {
|
|
67
|
+
'apt|pacman|brew': ['nmap'],
|
|
68
|
+
'apk': ['nmap', 'nmap-scripts'],
|
|
69
|
+
}
|
|
60
70
|
install_cmd = (
|
|
61
|
-
'sudo
|
|
71
|
+
'sudo git clone https://github.com/scipag/vulscan /opt/scipag_vulscan || true && '
|
|
62
72
|
'sudo ln -s /opt/scipag_vulscan /usr/share/nmap/scripts/vulscan || true'
|
|
63
73
|
)
|
|
64
74
|
proxychains = True
|
|
@@ -75,21 +85,23 @@ class nmap(VulnMulti):
|
|
|
75
85
|
self.output_path = output_path
|
|
76
86
|
self.cmd += f' -oX {self.output_path}'
|
|
77
87
|
tcp_syn_stealth = self.get_opt_value('tcp_syn_stealth')
|
|
78
|
-
|
|
88
|
+
tcp_connect = self.get_opt_value('tcp_connect')
|
|
89
|
+
udp_scan = self.get_opt_value('udp_scan')
|
|
90
|
+
if tcp_syn_stealth or udp_scan:
|
|
79
91
|
self.cmd = f'sudo {self.cmd}'
|
|
80
|
-
|
|
92
|
+
if tcp_connect and tcp_syn_stealth:
|
|
93
|
+
self._print(
|
|
94
|
+
'Options -sT (SYN stealth scan) and -sS (CONNECT scan) are conflicting. Keeping only -sT.',
|
|
95
|
+
'bold gold3')
|
|
96
|
+
self.cmd = self.cmd.replace('-sT ', '')
|
|
81
97
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if self.
|
|
98
|
+
@staticmethod
|
|
99
|
+
def on_cmd_done(self):
|
|
100
|
+
if not os.path.exists(self.output_path):
|
|
101
|
+
yield Error(message=f'Could not find XML results in {self.output_path}')
|
|
85
102
|
return
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if self.print_line:
|
|
89
|
-
self._print(note)
|
|
90
|
-
if os.path.exists(self.output_path):
|
|
91
|
-
nmap_data = self.xml_to_json()
|
|
92
|
-
yield from nmap_data
|
|
103
|
+
yield Info(message=f'XML results saved to {self.output_path}')
|
|
104
|
+
yield from self.xml_to_json()
|
|
93
105
|
|
|
94
106
|
def xml_to_json(self):
|
|
95
107
|
results = []
|
|
@@ -97,17 +109,18 @@ class nmap(VulnMulti):
|
|
|
97
109
|
content = f.read()
|
|
98
110
|
try:
|
|
99
111
|
results = xmltodict.parse(content) # parse XML to dict
|
|
100
|
-
except Exception as
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
yield Error(
|
|
114
|
+
message=f'Cannot parse XML output {self.output_path} to valid JSON.',
|
|
115
|
+
traceback=traceback_as_string(exc)
|
|
116
|
+
)
|
|
117
|
+
yield from nmapData(results)
|
|
106
118
|
|
|
107
119
|
|
|
108
120
|
class nmapData(dict):
|
|
109
121
|
|
|
110
122
|
def __iter__(self):
|
|
123
|
+
datas = []
|
|
111
124
|
for host in self._get_hosts():
|
|
112
125
|
hostname = self._get_hostname(host)
|
|
113
126
|
ip = self._get_ip(host)
|
|
@@ -123,14 +136,9 @@ class nmapData(dict):
|
|
|
123
136
|
|
|
124
137
|
# Get extra data
|
|
125
138
|
extra_data = self._get_extra_data(port)
|
|
126
|
-
service_name = extra_data
|
|
139
|
+
service_name = extra_data.get('service_name', '')
|
|
127
140
|
version_exact = extra_data.get('version_exact', False)
|
|
128
141
|
conf = extra_data.get('confidence')
|
|
129
|
-
if not version_exact:
|
|
130
|
-
console.print(
|
|
131
|
-
f'[bold orange1]nmap could not identify an exact version for {service_name} '
|
|
132
|
-
f'(detection confidence is {conf}): do not blindy trust the results ![/]'
|
|
133
|
-
)
|
|
134
142
|
|
|
135
143
|
# Grab CPEs
|
|
136
144
|
cpes = extra_data.get('cpe', [])
|
|
@@ -138,6 +146,9 @@ class nmapData(dict):
|
|
|
138
146
|
# Get script output
|
|
139
147
|
scripts = self._get_scripts(port)
|
|
140
148
|
|
|
149
|
+
# Get port protocol
|
|
150
|
+
protocol = port['@protocol'].lower()
|
|
151
|
+
|
|
141
152
|
# Yield port data
|
|
142
153
|
port = {
|
|
143
154
|
PORT: port_number,
|
|
@@ -145,7 +156,9 @@ class nmapData(dict):
|
|
|
145
156
|
STATE: state,
|
|
146
157
|
SERVICE_NAME: service_name,
|
|
147
158
|
IP: ip,
|
|
148
|
-
|
|
159
|
+
PROTOCOL: protocol,
|
|
160
|
+
EXTRA_DATA: extra_data,
|
|
161
|
+
CONFIDENCE: conf
|
|
149
162
|
}
|
|
150
163
|
yield port
|
|
151
164
|
|
|
@@ -169,16 +182,19 @@ class nmapData(dict):
|
|
|
169
182
|
if not func:
|
|
170
183
|
debug(f'Script output parser for "{script_id}" is not supported YET.', sub='cve')
|
|
171
184
|
continue
|
|
172
|
-
for
|
|
173
|
-
|
|
185
|
+
for data in func(output, cpes=cpes):
|
|
186
|
+
data.update(metadata)
|
|
174
187
|
confidence = 'low'
|
|
175
|
-
if 'cpe-match' in
|
|
188
|
+
if 'cpe-match' in data[TAGS]:
|
|
176
189
|
confidence = 'high' if version_exact else 'medium'
|
|
177
|
-
|
|
178
|
-
if (CONFIG.runners.skip_cve_low_confidence and
|
|
179
|
-
debug(f'{
|
|
190
|
+
data[CONFIDENCE] = confidence
|
|
191
|
+
if (CONFIG.runners.skip_cve_low_confidence and data[CONFIDENCE] == 'low'):
|
|
192
|
+
debug(f'{data[ID]}: ignored (low confidence).', sub='cve')
|
|
193
|
+
continue
|
|
194
|
+
if data in datas:
|
|
180
195
|
continue
|
|
181
|
-
yield
|
|
196
|
+
yield data
|
|
197
|
+
datas.append(data)
|
|
182
198
|
|
|
183
199
|
#---------------------#
|
|
184
200
|
# XML FILE EXTRACTORS #
|
|
@@ -197,7 +213,6 @@ class nmapData(dict):
|
|
|
197
213
|
|
|
198
214
|
def _get_hostname(self, host_cfg):
|
|
199
215
|
hostnames = host_cfg.get('hostnames', {})
|
|
200
|
-
hostname = self['_host']
|
|
201
216
|
if hostnames:
|
|
202
217
|
hostnames = hostnames.get('hostname', [])
|
|
203
218
|
if isinstance(hostnames, dict):
|
|
@@ -205,11 +220,19 @@ class nmapData(dict):
|
|
|
205
220
|
if hostnames:
|
|
206
221
|
hostname = hostnames[0]['@name']
|
|
207
222
|
else:
|
|
208
|
-
hostname =
|
|
223
|
+
hostname = self._get_address(host_cfg).get('@addr', None)
|
|
209
224
|
return hostname
|
|
210
225
|
|
|
226
|
+
def _get_address(self, host_cfg):
|
|
227
|
+
if isinstance(host_cfg.get('address', {}), list):
|
|
228
|
+
addresses = host_cfg.get('address', {})
|
|
229
|
+
for address in addresses:
|
|
230
|
+
if address.get('@addrtype') == "ipv4":
|
|
231
|
+
return address
|
|
232
|
+
return host_cfg.get('address', {})
|
|
233
|
+
|
|
211
234
|
def _get_ip(self, host_cfg):
|
|
212
|
-
return
|
|
235
|
+
return self._get_address(host_cfg).get('@addr', None)
|
|
213
236
|
|
|
214
237
|
def _get_extra_data(self, port_cfg):
|
|
215
238
|
extra_data = {
|
|
@@ -246,7 +269,7 @@ class nmapData(dict):
|
|
|
246
269
|
extra_data['version_exact'] = version_exact
|
|
247
270
|
|
|
248
271
|
# Grap service name
|
|
249
|
-
product = extra_data.get('
|
|
272
|
+
product = extra_data.get('product', None) or extra_data.get('name', None)
|
|
250
273
|
if product:
|
|
251
274
|
service_name = product
|
|
252
275
|
if version:
|
|
@@ -324,12 +347,12 @@ class nmapData(dict):
|
|
|
324
347
|
TAGS: [vuln_id, provider_name]
|
|
325
348
|
}
|
|
326
349
|
if provider_name == 'MITRE CVE':
|
|
327
|
-
data = VulnMulti.lookup_cve(vuln['id'], cpes
|
|
350
|
+
data = VulnMulti.lookup_cve(vuln['id'], *cpes)
|
|
328
351
|
if data:
|
|
329
352
|
vuln.update(data)
|
|
330
353
|
yield vuln
|
|
331
354
|
else:
|
|
332
|
-
debug(f'Vulscan provider {provider_name} is not supported YET.', sub='cve')
|
|
355
|
+
debug(f'Vulscan provider {provider_name} is not supported YET.', sub='cve.provider', verbose=True)
|
|
333
356
|
continue
|
|
334
357
|
|
|
335
358
|
def _parse_vulners_output(self, out, **kwargs):
|
|
@@ -343,30 +366,35 @@ class nmapData(dict):
|
|
|
343
366
|
cpes.append(line.rstrip(':'))
|
|
344
367
|
continue
|
|
345
368
|
elems = tuple(line.split('\t'))
|
|
346
|
-
vuln = {}
|
|
347
369
|
|
|
348
370
|
if len(elems) == 4: # exploit
|
|
349
371
|
# TODO: Implement exploit processing
|
|
350
372
|
exploit_id, cvss_score, reference_url, _ = elems
|
|
351
373
|
name = exploit_id
|
|
352
374
|
# edb_id = name.split(':')[-1] if 'EDB-ID' in name else None
|
|
353
|
-
|
|
375
|
+
exploit = {
|
|
354
376
|
ID: exploit_id,
|
|
355
377
|
NAME: name,
|
|
356
378
|
PROVIDER: provider_name,
|
|
357
379
|
REFERENCE: reference_url,
|
|
380
|
+
TAGS: [exploit_id, provider_name],
|
|
381
|
+
CVSS_SCORE: cvss_score,
|
|
382
|
+
CONFIDENCE: 'low',
|
|
358
383
|
'_type': 'exploit',
|
|
359
|
-
TAGS: [exploit_id, provider_name]
|
|
360
|
-
# CVSS_SCORE: cvss_score,
|
|
361
|
-
# CONFIDENCE: 'low'
|
|
362
384
|
}
|
|
363
385
|
# TODO: lookup exploit in ExploitDB to find related CVEs
|
|
364
386
|
# if edb_id:
|
|
365
387
|
# print(edb_id)
|
|
366
|
-
#
|
|
367
|
-
|
|
388
|
+
# exploit_data = VulnMulti.lookup_exploitdb(edb_id)
|
|
389
|
+
vuln = VulnMulti.lookup_cve_from_vulners_exploit(exploit_id, *cpes)
|
|
390
|
+
if vuln:
|
|
391
|
+
yield vuln
|
|
392
|
+
exploit[TAGS].extend(vuln[TAGS])
|
|
393
|
+
exploit[CONFIDENCE] = vuln[CONFIDENCE]
|
|
394
|
+
yield exploit
|
|
368
395
|
|
|
369
396
|
elif len(elems) == 3: # vuln
|
|
397
|
+
vuln = {}
|
|
370
398
|
vuln_id, vuln_cvss, reference_url = tuple(line.split('\t'))
|
|
371
399
|
vuln_cvss = float(vuln_cvss)
|
|
372
400
|
vuln_id = vuln_id.split(':')[-1]
|
|
@@ -382,8 +410,7 @@ class nmapData(dict):
|
|
|
382
410
|
CONFIDENCE: 'low'
|
|
383
411
|
}
|
|
384
412
|
if vuln_type == 'CVE' or vuln_type == 'PRION:CVE':
|
|
385
|
-
|
|
386
|
-
data = VulnMulti.lookup_cve(vuln_id, cpes=cpes)
|
|
413
|
+
data = VulnMulti.lookup_cve(vuln_id, *cpes)
|
|
387
414
|
if data:
|
|
388
415
|
vuln.update(data)
|
|
389
416
|
yield vuln
|
secator/tasks/nuclei.py
CHANGED
|
@@ -4,15 +4,16 @@ from secator.definitions import (CONFIDENCE, CVSS_SCORE, DELAY, DESCRIPTION,
|
|
|
4
4
|
MATCHED_AT, NAME, OPT_NOT_SUPPORTED, PERCENT,
|
|
5
5
|
PROVIDER, PROXY, RATE_LIMIT, REFERENCES,
|
|
6
6
|
RETRIES, SEVERITY, TAGS, THREADS, TIMEOUT,
|
|
7
|
-
USER_AGENT
|
|
7
|
+
USER_AGENT)
|
|
8
8
|
from secator.output_types import Progress, Vulnerability
|
|
9
|
+
from secator.serializers import JSONSerializer
|
|
9
10
|
from secator.tasks._categories import VulnMulti
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@task()
|
|
13
14
|
class nuclei(VulnMulti):
|
|
14
15
|
"""Fast and customisable vulnerability scanner based on simple YAML based DSL."""
|
|
15
|
-
cmd =
|
|
16
|
+
cmd = 'nuclei'
|
|
16
17
|
file_flag = '-l'
|
|
17
18
|
input_flag = '-u'
|
|
18
19
|
json_flag = '-jsonl'
|
|
@@ -23,6 +24,11 @@ class nuclei(VulnMulti):
|
|
|
23
24
|
'exclude_severity': {'type': str, 'short': 'es', 'help': 'Exclude severity'},
|
|
24
25
|
'template_id': {'type': str, 'short': 'tid', 'help': 'Template id'},
|
|
25
26
|
'debug': {'type': str, 'help': 'Debug mode'},
|
|
27
|
+
'stats': {'is_flag': True, 'short': 'stats', 'default': True, 'help': 'Display statistics about the running scan'},
|
|
28
|
+
'stats_json': {'is_flag': True, 'short': 'sj', 'default': True, 'help': 'Display statistics in JSONL(ines) format'},
|
|
29
|
+
'stats_interval': {'type': str, 'short': 'si', 'default': 20, 'help': 'Number of seconds to wait between showing a statistics update'}, # noqa: E501
|
|
30
|
+
'hang_monitor': {'is_flag': True, 'short': 'hm', 'default': True, 'help': 'Enable nuclei hang monitoring'},
|
|
31
|
+
'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
|
|
26
32
|
}
|
|
27
33
|
opt_key_map = {
|
|
28
34
|
HEADER: 'header',
|
|
@@ -45,6 +51,7 @@ class nuclei(VulnMulti):
|
|
|
45
51
|
'templates': lambda x: ','.join(x) if isinstance(x, list) else x,
|
|
46
52
|
'exclude_tags': lambda x: ','.join(x) if isinstance(x, list) else x,
|
|
47
53
|
}
|
|
54
|
+
item_loaders = [JSONSerializer()]
|
|
48
55
|
output_types = [Vulnerability, Progress]
|
|
49
56
|
output_map = {
|
|
50
57
|
Vulnerability: {
|
|
@@ -67,8 +74,11 @@ class nuclei(VulnMulti):
|
|
|
67
74
|
}
|
|
68
75
|
}
|
|
69
76
|
ignore_return_code = True
|
|
70
|
-
install_cmd = 'go install -v github.com/projectdiscovery/nuclei/v2/cmd/nuclei@latest
|
|
77
|
+
install_cmd = 'go install -v github.com/projectdiscovery/nuclei/v2/cmd/nuclei@latest'
|
|
71
78
|
install_github_handle = 'projectdiscovery/nuclei'
|
|
79
|
+
install_post = {
|
|
80
|
+
'*': 'nuclei -ut'
|
|
81
|
+
}
|
|
72
82
|
proxychains = False
|
|
73
83
|
proxy_socks5 = True # kind of, leaks data when running network / dns templates
|
|
74
84
|
proxy_http = True # same
|
secator/tasks/searchsploit.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
|
+
from secator.config import CONFIG
|
|
3
4
|
from secator.decorators import task
|
|
4
5
|
from secator.definitions import (CVES, EXTRA_DATA, ID, MATCHED_AT, NAME,
|
|
5
6
|
PROVIDER, REFERENCE, TAGS, OPT_NOT_SUPPORTED)
|
|
6
7
|
from secator.output_types import Exploit
|
|
7
8
|
from secator.runners import Command
|
|
9
|
+
from secator.serializers import JSONSerializer
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
SEARCHSPLOIT_TITLE_REGEX = re.compile(r'^((?:[a-zA-Z\-_!\.()]+\d?\s?)+)\.?\s*(.*)$')
|
|
@@ -12,7 +14,7 @@ SEARCHSPLOIT_TITLE_REGEX = re.compile(r'^((?:[a-zA-Z\-_!\.()]+\d?\s?)+)\.?\s*(.*
|
|
|
12
14
|
|
|
13
15
|
@task()
|
|
14
16
|
class searchsploit(Command):
|
|
15
|
-
"""Exploit
|
|
17
|
+
"""Exploit searcher based on ExploitDB."""
|
|
16
18
|
cmd = 'searchsploit'
|
|
17
19
|
input_flag = None
|
|
18
20
|
json_flag = '--json'
|
|
@@ -21,6 +23,7 @@ class searchsploit(Command):
|
|
|
21
23
|
'strict': {'short': 's', 'is_flag': True, 'default': False, 'help': 'Strict match'}
|
|
22
24
|
}
|
|
23
25
|
opt_key_map = {}
|
|
26
|
+
item_loaders = [JSONSerializer()]
|
|
24
27
|
output_types = [Exploit]
|
|
25
28
|
output_map = {
|
|
26
29
|
Exploit: {
|
|
@@ -35,7 +38,13 @@ class searchsploit(Command):
|
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
|
-
|
|
41
|
+
install_pre = {
|
|
42
|
+
'apk': ['ncurses']
|
|
43
|
+
}
|
|
44
|
+
install_cmd = (
|
|
45
|
+
f'git clone https://gitlab.com/exploit-database/exploitdb.git {CONFIG.dirs.share}/exploitdb || true && '
|
|
46
|
+
f'ln -sf $HOME/.local/share/exploitdb/searchsploit {CONFIG.dirs.bin}/searchsploit'
|
|
47
|
+
)
|
|
39
48
|
proxychains = False
|
|
40
49
|
proxy_socks5 = False
|
|
41
50
|
proxy_http = False
|
|
@@ -56,14 +65,15 @@ class searchsploit(Command):
|
|
|
56
65
|
|
|
57
66
|
@staticmethod
|
|
58
67
|
def before_init(self):
|
|
59
|
-
|
|
68
|
+
if len(self.inputs) == 0:
|
|
69
|
+
return
|
|
70
|
+
_in = self.inputs[0]
|
|
60
71
|
self.matched_at = None
|
|
61
72
|
if '~' in _in:
|
|
62
73
|
split = _in.split('~')
|
|
63
74
|
self.matched_at = split[0]
|
|
64
|
-
self.
|
|
65
|
-
|
|
66
|
-
self.input = self.input.replace('httpd', '').replace('/', ' ')
|
|
75
|
+
self.inputs[0] = split[1]
|
|
76
|
+
self.inputs[0] = self.inputs[0].replace('httpd', '').replace('/', ' ')
|
|
67
77
|
|
|
68
78
|
@staticmethod
|
|
69
79
|
def on_item_pre_convert(self, item):
|
|
@@ -80,12 +90,17 @@ class searchsploit(Command):
|
|
|
80
90
|
group = match.groups()
|
|
81
91
|
product = '-'.join(group[0].strip().split(' '))
|
|
82
92
|
if len(group[1]) > 1:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
try:
|
|
94
|
+
versions, title = tuple(group[1].split(' - '))
|
|
95
|
+
item.name = title
|
|
96
|
+
product_info = [f'{product.lower()} {v.strip()}' for v in versions.split('/')]
|
|
97
|
+
item.tags = product_info + item.tags
|
|
98
|
+
except ValueError:
|
|
99
|
+
item.name = item.name.split(' - ')[-1]
|
|
100
|
+
item.tags = [product.lower()]
|
|
101
|
+
pass
|
|
87
102
|
# else:
|
|
88
103
|
# self._print(f'[bold red]{item.name} ({item.reference}) did not quite match SEARCHSPLOIT_TITLE_REGEX. Please report this issue.[/]') # noqa: E501
|
|
89
|
-
input_tag = '-'.join(self.
|
|
104
|
+
input_tag = '-'.join(self.inputs[0].replace('\'', '').split(' '))
|
|
90
105
|
item.tags = [input_tag] + item.tags
|
|
91
106
|
return item
|
secator/tasks/subfinder.py
CHANGED
|
@@ -2,6 +2,7 @@ from secator.decorators import task
|
|
|
2
2
|
from secator.definitions import (DELAY, DOMAIN, OPT_NOT_SUPPORTED, PROXY,
|
|
3
3
|
RATE_LIMIT, RETRIES, THREADS, TIMEOUT)
|
|
4
4
|
from secator.output_types import Subdomain
|
|
5
|
+
from secator.serializers import JSONSerializer
|
|
5
6
|
from secator.tasks._categories import ReconDns
|
|
6
7
|
|
|
7
8
|
|
|
@@ -23,6 +24,7 @@ class subfinder(ReconDns):
|
|
|
23
24
|
opt_value_map = {
|
|
24
25
|
PROXY: lambda x: x.replace('http://', '').replace('https://', '') if x else None
|
|
25
26
|
}
|
|
27
|
+
item_loaders = [JSONSerializer()]
|
|
26
28
|
output_map = {
|
|
27
29
|
Subdomain: {
|
|
28
30
|
DOMAIN: 'input',
|
|
@@ -38,4 +40,6 @@ class subfinder(ReconDns):
|
|
|
38
40
|
|
|
39
41
|
@staticmethod
|
|
40
42
|
def validate_item(self, item):
|
|
41
|
-
|
|
43
|
+
if isinstance(item, dict):
|
|
44
|
+
return item['input'] != 'localhost'
|
|
45
|
+
return True
|
secator/tasks/wpscan.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
|
+
from secator.config import CONFIG
|
|
4
5
|
from secator.decorators import task
|
|
5
6
|
from secator.definitions import (CONFIDENCE, CVSS_SCORE, DELAY, DESCRIPTION,
|
|
6
7
|
EXTRA_DATA, FOLLOW_REDIRECT, HEADER, ID,
|
|
@@ -8,7 +9,7 @@ from secator.definitions import (CONFIDENCE, CVSS_SCORE, DELAY, DESCRIPTION,
|
|
|
8
9
|
PROXY, RATE_LIMIT, REFERENCES, RETRIES,
|
|
9
10
|
SEVERITY, TAGS, THREADS, TIMEOUT,
|
|
10
11
|
URL, USER_AGENT)
|
|
11
|
-
from secator.output_types import Tag, Vulnerability
|
|
12
|
+
from secator.output_types import Tag, Vulnerability, Info, Error
|
|
12
13
|
from secator.tasks._categories import VulnHttp
|
|
13
14
|
|
|
14
15
|
|
|
@@ -66,112 +67,108 @@ class wpscan(VulnHttp):
|
|
|
66
67
|
},
|
|
67
68
|
}
|
|
68
69
|
output_types = [Vulnerability, Tag]
|
|
69
|
-
|
|
70
|
+
install_pre = {
|
|
71
|
+
'apt': ['kali:libcurl4t64', 'libffi-dev'],
|
|
72
|
+
'pacman': ['ruby-erb'],
|
|
73
|
+
}
|
|
74
|
+
install_cmd = f'gem install wpscan --user-install -n {CONFIG.dirs.bin}'
|
|
75
|
+
install_post = {
|
|
76
|
+
'kali': (
|
|
77
|
+
f'gem uninstall nokogiri --user-install -n {CONFIG.dirs.bin} --force --executables && '
|
|
78
|
+
f'gem install nokogiri --user-install -n {CONFIG.dirs.bin} --platform=ruby'
|
|
79
|
+
)
|
|
80
|
+
}
|
|
70
81
|
proxychains = False
|
|
71
82
|
proxy_http = True
|
|
72
83
|
proxy_socks5 = False
|
|
73
84
|
ignore_return_code = True
|
|
74
85
|
profile = 'io'
|
|
75
86
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self.
|
|
83
|
-
if not self.output_json:
|
|
84
|
-
return
|
|
87
|
+
@staticmethod
|
|
88
|
+
def on_init(self):
|
|
89
|
+
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
90
|
+
if not output_path:
|
|
91
|
+
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
92
|
+
self.output_path = output_path
|
|
93
|
+
self.cmd += f' -o {self.output_path}'
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
@staticmethod
|
|
96
|
+
def on_cmd_done(self):
|
|
97
|
+
if not os.path.exists(self.output_path):
|
|
98
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
99
|
+
return
|
|
89
100
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
102
|
+
with open(self.output_path, 'r') as f:
|
|
103
|
+
data = json.load(f)
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return
|
|
105
|
+
# Get URL
|
|
106
|
+
target = data.get('target_url', self.inputs[0])
|
|
97
107
|
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
# Wordpress version
|
|
109
|
+
version = data.get('version', {})
|
|
110
|
+
if version:
|
|
111
|
+
wp_version = version['number']
|
|
112
|
+
wp_version_status = version['status']
|
|
113
|
+
if wp_version_status == 'outdated':
|
|
114
|
+
vuln = version
|
|
115
|
+
vuln.update({
|
|
116
|
+
'url': target,
|
|
117
|
+
'to_s': 'Wordpress outdated version',
|
|
118
|
+
'type': wp_version,
|
|
119
|
+
'references': {},
|
|
120
|
+
})
|
|
121
|
+
yield vuln
|
|
100
122
|
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
# Main theme
|
|
124
|
+
main_theme = data.get('main_theme', {})
|
|
125
|
+
if main_theme:
|
|
126
|
+
version = main_theme.get('version', {})
|
|
127
|
+
slug = main_theme['slug']
|
|
128
|
+
location = main_theme['location']
|
|
103
129
|
if version:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
version = main_theme.get('version', {})
|
|
120
|
-
slug = main_theme['slug']
|
|
121
|
-
location = main_theme['location']
|
|
122
|
-
if version:
|
|
123
|
-
number = version['number']
|
|
124
|
-
latest_version = main_theme.get('latest_version')
|
|
125
|
-
yield Tag(
|
|
126
|
-
name=f'Wordpress theme - {slug} {number}',
|
|
127
|
-
match=target,
|
|
128
|
-
extra_data={
|
|
129
|
-
'url': location,
|
|
130
|
-
'latest_version': latest_version
|
|
131
|
-
}
|
|
130
|
+
number = version['number']
|
|
131
|
+
latest_version = main_theme.get('latest_version')
|
|
132
|
+
yield Tag(
|
|
133
|
+
name=f'Wordpress theme - {slug} {number}',
|
|
134
|
+
match=target,
|
|
135
|
+
extra_data={
|
|
136
|
+
'url': location,
|
|
137
|
+
'latest_version': latest_version
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
if (latest_version and number < latest_version):
|
|
141
|
+
yield Vulnerability(
|
|
142
|
+
matched_at=target,
|
|
143
|
+
name=f'Wordpress theme - {slug} {number} outdated',
|
|
144
|
+
severity='info'
|
|
132
145
|
)
|
|
133
|
-
if (latest_version and number < latest_version):
|
|
134
|
-
yield Vulnerability(
|
|
135
|
-
matched_at=target,
|
|
136
|
-
name=f'Wordpress theme - {slug} {number} outdated',
|
|
137
|
-
severity='info'
|
|
138
|
-
)
|
|
139
146
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
# Interesting findings
|
|
148
|
+
interesting_findings = data.get('interesting_findings', [])
|
|
149
|
+
for item in interesting_findings:
|
|
150
|
+
yield item
|
|
144
151
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
152
|
+
# Plugins
|
|
153
|
+
plugins = data.get('plugins', {})
|
|
154
|
+
for _, data in plugins.items():
|
|
155
|
+
version = data.get('version', {})
|
|
156
|
+
slug = data['slug']
|
|
157
|
+
location = data['location']
|
|
158
|
+
if version:
|
|
159
|
+
number = version['number']
|
|
160
|
+
latest_version = data.get('latest_version')
|
|
161
|
+
yield Tag(
|
|
162
|
+
name=f'Wordpress plugin - {slug} {number}',
|
|
163
|
+
match=target,
|
|
164
|
+
extra_data={
|
|
165
|
+
'url': location,
|
|
166
|
+
'latest_version': latest_version
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
if (latest_version and number < latest_version):
|
|
170
|
+
yield Vulnerability(
|
|
171
|
+
matched_at=target,
|
|
172
|
+
name=f'Wordpress plugin - {slug} {number} outdated',
|
|
173
|
+
severity='info'
|
|
161
174
|
)
|
|
162
|
-
if (latest_version and number < latest_version):
|
|
163
|
-
yield Vulnerability(
|
|
164
|
-
matched_at=target,
|
|
165
|
-
name=f'Wordpress plugin - {slug} {number} outdated',
|
|
166
|
-
severity='info'
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
self.print_item_count = prev
|
|
170
|
-
|
|
171
|
-
@staticmethod
|
|
172
|
-
def on_init(self):
|
|
173
|
-
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
174
|
-
if not output_path:
|
|
175
|
-
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
176
|
-
self.output_path = output_path
|
|
177
|
-
self.cmd += f' -o {self.output_path}'
|