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.

Files changed (99) hide show
  1. secator/.gitignore +162 -0
  2. secator/__init__.py +0 -0
  3. secator/celery.py +421 -0
  4. secator/cli.py +927 -0
  5. secator/config.py +137 -0
  6. secator/configs/__init__.py +0 -0
  7. secator/configs/profiles/__init__.py +0 -0
  8. secator/configs/profiles/aggressive.yaml +7 -0
  9. secator/configs/profiles/default.yaml +9 -0
  10. secator/configs/profiles/stealth.yaml +7 -0
  11. secator/configs/scans/__init__.py +0 -0
  12. secator/configs/scans/domain.yaml +18 -0
  13. secator/configs/scans/host.yaml +14 -0
  14. secator/configs/scans/network.yaml +17 -0
  15. secator/configs/scans/subdomain.yaml +8 -0
  16. secator/configs/scans/url.yaml +12 -0
  17. secator/configs/workflows/__init__.py +0 -0
  18. secator/configs/workflows/cidr_recon.yaml +28 -0
  19. secator/configs/workflows/code_scan.yaml +11 -0
  20. secator/configs/workflows/host_recon.yaml +41 -0
  21. secator/configs/workflows/port_scan.yaml +34 -0
  22. secator/configs/workflows/subdomain_recon.yaml +33 -0
  23. secator/configs/workflows/url_crawl.yaml +29 -0
  24. secator/configs/workflows/url_dirsearch.yaml +29 -0
  25. secator/configs/workflows/url_fuzz.yaml +35 -0
  26. secator/configs/workflows/url_nuclei.yaml +11 -0
  27. secator/configs/workflows/url_vuln.yaml +55 -0
  28. secator/configs/workflows/user_hunt.yaml +10 -0
  29. secator/configs/workflows/wordpress.yaml +14 -0
  30. secator/decorators.py +346 -0
  31. secator/definitions.py +183 -0
  32. secator/exporters/__init__.py +12 -0
  33. secator/exporters/_base.py +3 -0
  34. secator/exporters/csv.py +29 -0
  35. secator/exporters/gdrive.py +118 -0
  36. secator/exporters/json.py +14 -0
  37. secator/exporters/table.py +7 -0
  38. secator/exporters/txt.py +24 -0
  39. secator/hooks/__init__.py +0 -0
  40. secator/hooks/mongodb.py +212 -0
  41. secator/output_types/__init__.py +24 -0
  42. secator/output_types/_base.py +95 -0
  43. secator/output_types/exploit.py +50 -0
  44. secator/output_types/ip.py +33 -0
  45. secator/output_types/port.py +45 -0
  46. secator/output_types/progress.py +35 -0
  47. secator/output_types/record.py +34 -0
  48. secator/output_types/subdomain.py +42 -0
  49. secator/output_types/tag.py +46 -0
  50. secator/output_types/target.py +30 -0
  51. secator/output_types/url.py +76 -0
  52. secator/output_types/user_account.py +41 -0
  53. secator/output_types/vulnerability.py +97 -0
  54. secator/report.py +95 -0
  55. secator/rich.py +123 -0
  56. secator/runners/__init__.py +12 -0
  57. secator/runners/_base.py +873 -0
  58. secator/runners/_helpers.py +154 -0
  59. secator/runners/command.py +674 -0
  60. secator/runners/scan.py +67 -0
  61. secator/runners/task.py +107 -0
  62. secator/runners/workflow.py +137 -0
  63. secator/serializers/__init__.py +8 -0
  64. secator/serializers/dataclass.py +33 -0
  65. secator/serializers/json.py +15 -0
  66. secator/serializers/regex.py +17 -0
  67. secator/tasks/__init__.py +10 -0
  68. secator/tasks/_categories.py +304 -0
  69. secator/tasks/cariddi.py +102 -0
  70. secator/tasks/dalfox.py +66 -0
  71. secator/tasks/dirsearch.py +88 -0
  72. secator/tasks/dnsx.py +56 -0
  73. secator/tasks/dnsxbrute.py +34 -0
  74. secator/tasks/feroxbuster.py +89 -0
  75. secator/tasks/ffuf.py +85 -0
  76. secator/tasks/fping.py +44 -0
  77. secator/tasks/gau.py +43 -0
  78. secator/tasks/gf.py +34 -0
  79. secator/tasks/gospider.py +71 -0
  80. secator/tasks/grype.py +78 -0
  81. secator/tasks/h8mail.py +80 -0
  82. secator/tasks/httpx.py +104 -0
  83. secator/tasks/katana.py +128 -0
  84. secator/tasks/maigret.py +78 -0
  85. secator/tasks/mapcidr.py +32 -0
  86. secator/tasks/msfconsole.py +176 -0
  87. secator/tasks/naabu.py +52 -0
  88. secator/tasks/nmap.py +341 -0
  89. secator/tasks/nuclei.py +97 -0
  90. secator/tasks/searchsploit.py +53 -0
  91. secator/tasks/subfinder.py +40 -0
  92. secator/tasks/wpscan.py +177 -0
  93. secator/utils.py +404 -0
  94. secator/utils_test.py +183 -0
  95. secator-0.1.0.dist-info/METADATA +379 -0
  96. secator-0.1.0.dist-info/RECORD +99 -0
  97. secator-0.1.0.dist-info/WHEEL +5 -0
  98. secator-0.1.0.dist-info/entry_points.txt +2 -0
  99. 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}')
@@ -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