secator 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of secator might be problematic. Click here for more details.

Files changed (114) hide show
  1. secator/__init__.py +0 -0
  2. secator/celery.py +482 -0
  3. secator/cli.py +617 -0
  4. secator/config.py +137 -0
  5. secator/configs/__init__.py +0 -0
  6. secator/configs/profiles/__init__.py +0 -0
  7. secator/configs/profiles/aggressive.yaml +7 -0
  8. secator/configs/profiles/default.yaml +9 -0
  9. secator/configs/profiles/stealth.yaml +7 -0
  10. secator/configs/scans/__init__.py +0 -0
  11. secator/configs/scans/domain.yaml +18 -0
  12. secator/configs/scans/host.yaml +14 -0
  13. secator/configs/scans/network.yaml +17 -0
  14. secator/configs/scans/subdomain.yaml +8 -0
  15. secator/configs/scans/url.yaml +12 -0
  16. secator/configs/workflows/__init__.py +0 -0
  17. secator/configs/workflows/cidr_recon.yaml +28 -0
  18. secator/configs/workflows/code_scan.yaml +11 -0
  19. secator/configs/workflows/host_recon.yaml +41 -0
  20. secator/configs/workflows/port_scan.yaml +34 -0
  21. secator/configs/workflows/subdomain_recon.yaml +33 -0
  22. secator/configs/workflows/url_crawl.yaml +29 -0
  23. secator/configs/workflows/url_dirsearch.yaml +29 -0
  24. secator/configs/workflows/url_fuzz.yaml +35 -0
  25. secator/configs/workflows/url_nuclei.yaml +11 -0
  26. secator/configs/workflows/url_vuln.yaml +55 -0
  27. secator/configs/workflows/user_hunt.yaml +10 -0
  28. secator/configs/workflows/wordpress.yaml +14 -0
  29. secator/decorators.py +309 -0
  30. secator/definitions.py +165 -0
  31. secator/exporters/__init__.py +12 -0
  32. secator/exporters/_base.py +3 -0
  33. secator/exporters/csv.py +30 -0
  34. secator/exporters/gdrive.py +118 -0
  35. secator/exporters/json.py +15 -0
  36. secator/exporters/table.py +7 -0
  37. secator/exporters/txt.py +25 -0
  38. secator/hooks/__init__.py +0 -0
  39. secator/hooks/mongodb.py +212 -0
  40. secator/output_types/__init__.py +24 -0
  41. secator/output_types/_base.py +95 -0
  42. secator/output_types/exploit.py +50 -0
  43. secator/output_types/ip.py +33 -0
  44. secator/output_types/port.py +45 -0
  45. secator/output_types/progress.py +35 -0
  46. secator/output_types/record.py +34 -0
  47. secator/output_types/subdomain.py +42 -0
  48. secator/output_types/tag.py +46 -0
  49. secator/output_types/target.py +30 -0
  50. secator/output_types/url.py +76 -0
  51. secator/output_types/user_account.py +41 -0
  52. secator/output_types/vulnerability.py +97 -0
  53. secator/report.py +107 -0
  54. secator/rich.py +124 -0
  55. secator/runners/__init__.py +12 -0
  56. secator/runners/_base.py +833 -0
  57. secator/runners/_helpers.py +153 -0
  58. secator/runners/command.py +638 -0
  59. secator/runners/scan.py +65 -0
  60. secator/runners/task.py +106 -0
  61. secator/runners/workflow.py +135 -0
  62. secator/serializers/__init__.py +8 -0
  63. secator/serializers/dataclass.py +33 -0
  64. secator/serializers/json.py +15 -0
  65. secator/serializers/regex.py +17 -0
  66. secator/tasks/__init__.py +10 -0
  67. secator/tasks/_categories.py +304 -0
  68. secator/tasks/cariddi.py +102 -0
  69. secator/tasks/dalfox.py +65 -0
  70. secator/tasks/dirsearch.py +90 -0
  71. secator/tasks/dnsx.py +56 -0
  72. secator/tasks/dnsxbrute.py +34 -0
  73. secator/tasks/feroxbuster.py +91 -0
  74. secator/tasks/ffuf.py +86 -0
  75. secator/tasks/fping.py +44 -0
  76. secator/tasks/gau.py +47 -0
  77. secator/tasks/gf.py +33 -0
  78. secator/tasks/gospider.py +71 -0
  79. secator/tasks/grype.py +79 -0
  80. secator/tasks/h8mail.py +81 -0
  81. secator/tasks/httpx.py +99 -0
  82. secator/tasks/katana.py +133 -0
  83. secator/tasks/maigret.py +78 -0
  84. secator/tasks/mapcidr.py +32 -0
  85. secator/tasks/msfconsole.py +174 -0
  86. secator/tasks/naabu.py +52 -0
  87. secator/tasks/nmap.py +344 -0
  88. secator/tasks/nuclei.py +97 -0
  89. secator/tasks/searchsploit.py +52 -0
  90. secator/tasks/subfinder.py +40 -0
  91. secator/tasks/wpscan.py +179 -0
  92. secator/utils.py +445 -0
  93. secator/utils_test.py +183 -0
  94. secator-0.0.1.dist-info/LICENSE +60 -0
  95. secator-0.0.1.dist-info/METADATA +199 -0
  96. secator-0.0.1.dist-info/RECORD +114 -0
  97. secator-0.0.1.dist-info/WHEEL +5 -0
  98. secator-0.0.1.dist-info/entry_points.txt +2 -0
  99. secator-0.0.1.dist-info/top_level.txt +2 -0
  100. tests/__init__.py +0 -0
  101. tests/integration/__init__.py +0 -0
  102. tests/integration/inputs.py +42 -0
  103. tests/integration/outputs.py +392 -0
  104. tests/integration/test_scans.py +82 -0
  105. tests/integration/test_tasks.py +103 -0
  106. tests/integration/test_workflows.py +163 -0
  107. tests/performance/__init__.py +0 -0
  108. tests/performance/loadtester.py +56 -0
  109. tests/unit/__init__.py +0 -0
  110. tests/unit/test_celery.py +39 -0
  111. tests/unit/test_scans.py +0 -0
  112. tests/unit/test_serializers.py +51 -0
  113. tests/unit/test_tasks.py +348 -0
  114. tests/unit/test_workflows.py +96 -0
@@ -0,0 +1,174 @@
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
+ input_type = HOST
22
+ input_chunk_size = 1
23
+ output_types = []
24
+ output_return_type = str
25
+ opt_prefix = '--'
26
+ opts = {
27
+ 'resource': {'type': str, 'help': 'Metasploit resource script.', 'short': 'r'},
28
+ 'execute_command': {'type': str, 'help': 'Metasploit command.', 'short': 'x'},
29
+ 'environment': {'type': str, 'help': 'Environment variables string KEY=VALUE.', 'short': 'e'}
30
+ }
31
+ opt_key_map = {
32
+ 'x': 'execute_command',
33
+ 'r': 'resource',
34
+ HEADER: OPT_NOT_SUPPORTED,
35
+ DELAY: OPT_NOT_SUPPORTED,
36
+ FOLLOW_REDIRECT: OPT_NOT_SUPPORTED,
37
+ PROXY: OPT_NOT_SUPPORTED,
38
+ RATE_LIMIT: OPT_NOT_SUPPORTED,
39
+ RETRIES: OPT_NOT_SUPPORTED,
40
+ THREADS: OPT_NOT_SUPPORTED,
41
+ TIMEOUT: OPT_NOT_SUPPORTED,
42
+ USER_AGENT: OPT_NOT_SUPPORTED,
43
+ THREADS: OPT_NOT_SUPPORTED,
44
+ }
45
+ encoding = 'ansi'
46
+ ignore_return_code = True
47
+
48
+ @staticmethod
49
+ def validate_input(self, input):
50
+ """No list input supported for this command. Pass a single input instead."""
51
+ if isinstance(input, list):
52
+ return False
53
+ return True
54
+
55
+ @staticmethod
56
+ def on_init(self):
57
+ command = self.get_opt_value('execute_command')
58
+ script_path = self.get_opt_value('resource')
59
+ environment = self.run_opts.pop('environment', '')
60
+ env_vars = {}
61
+ if environment:
62
+ env_vars = dict(map(lambda x: x.split('='), environment.strip().split(',')))
63
+ env_vars['RHOST'] = self.input
64
+ env_vars['RHOSTS'] = self.input
65
+
66
+ # Passing msfconsole command directly, simply add RHOST / RHOSTS from host input and run then exit
67
+ if command:
68
+ self.run_opts['msfconsole.execute_command'] = (
69
+ f'setg RHOST {self.input}; '
70
+ f'setg RHOSTS {self.input}; '
71
+ f'{command.format(**env_vars)}; '
72
+ f'exit;'
73
+ )
74
+
75
+ # Passing resource script, replace vars inside by our environment variables if any, write to a temp file, and
76
+ # pass this temp file instead of the original one.
77
+ elif script_path:
78
+
79
+ # Read from original resource script
80
+ with open(script_path, 'r') as f:
81
+ content = f.read().replace('exit', '') + 'exit'
82
+
83
+ # Make a copy and replace vars inside by env vars passed on the CLI
84
+ timestr = get_file_timestamp()
85
+ out_path = f'{DATA_FOLDER}/msfconsole_{timestr}.rc'
86
+ logger.debug(
87
+ f'Writing formatted resource script to new temp file {out_path}'
88
+ )
89
+ with open(out_path, 'w') as f:
90
+ content = content.format(**env_vars)
91
+ f.write(content)
92
+
93
+ script_name = script_path.split('/')[-1]
94
+ self._print(Panel(content, title=f'[bold magenta]{script_name}', expand=False))
95
+
96
+ # Override original command with new resource script
97
+ self.run_opts['msfconsole.resource'] = out_path
98
+
99
+ # Nothing passed, error out
100
+ else:
101
+ raise ValueError('At least one of "inline_script" or "resource_script" must be passed.')
102
+
103
+ # Clear host input
104
+ self.input = ''
105
+
106
+
107
+ # TODO: This is better as it goes through an RPC API to communicate with
108
+ # metasploit rpc server, but it does not give any output.
109
+ # Seems like output is available only in Metasploit Pro, so keeping this in case
110
+ # we add support for it later.
111
+ #
112
+ # from pymetasploit3.msfrpc import MsfRpcClient
113
+ # class msfrpcd():
114
+ #
115
+ # opts = {
116
+ # 'uri': {'type': str, 'default': '/api/', 'help': 'msfrpcd API uri'},
117
+ # 'port': {'type': int, 'default': 55553, 'help': 'msfrpcd port'},
118
+ # 'server': {'type': str, 'default': 'localhost', 'help': 'msfrpcd host'},
119
+ # 'token': {'type': str, 'help': 'msfrpcd token'},
120
+ # 'username': {'type': str, 'default': 'msf', 'help': 'msfrpcd username'},
121
+ # 'password': {'type': str, 'default': 'test', 'help': 'msfrpcd password'},
122
+ # 'module': {'type': str, 'required': True, 'help': 'Metasploit module to run'}
123
+ # }
124
+ #
125
+ # def __init__(self, input, ctx={}, **run_opts):
126
+ # self.module = run_opts.pop('module')
127
+ # pw = run_opts.pop('password')
128
+ # self.run_opts = run_opts
129
+ # self.RHOST = input
130
+ # self.RHOSTS = input
131
+ # self.LHOST = self.get_lhost()
132
+ # # self.start_msgrpc()
133
+ # self.client = MsfRpcClient(pw, ssl=True, **run_opts)
134
+ #
135
+ # # def start_msgrpc(self):
136
+ # # code, out = run_command(f'msfrpcd -P {self.password}')
137
+ # # logger.info(out)
138
+ #
139
+ # def get_lhost(self):
140
+ # try:
141
+ # u = miniupnpc.UPnP()
142
+ # u.discoverdelay = 200
143
+ # u.discover()
144
+ # u.selectigd()
145
+ # return u.externalipaddress()
146
+ # except Exception:
147
+ # return 'localhost'
148
+ #
149
+ # def run(self):
150
+ # """Run a metasploit module.
151
+ #
152
+ # Args:
153
+ # modtype: Module type amongst 'auxiliary', 'exploit', 'post',
154
+ # 'encoder', 'nop', 'payload'.
155
+ # modname: Module name e.g 'auxiliary/scanner/ftp/ftp_version
156
+ # kwargs (dict): Module kwargs e.g RHOSTS, LHOST
157
+ # Returns:
158
+ # dict: Job results.
159
+ # """
160
+ # modtype = self.module.split('/')[0].rstrip('s')
161
+ # job = self.client.modules.execute(
162
+ # modtype,
163
+ # self.module,
164
+ # RHOST=self.RHOST,
165
+ # RHOSTS=self.RHOSTS,
166
+ # LHOST=self.LHOST)
167
+ # if job.get('error', False):
168
+ # logger.error(job['error_message'])
169
+ # job_info = self.client.jobs.info_by_uuid(job['uuid'])
170
+ # while (job_info['status'] in ['running', 'ready']):
171
+ # job_info = self.client.jobs.info_by_uuid(job['uuid'])
172
+ # job_info.update(job)
173
+ # print(type(job_info['result']['127.0.0.1']))
174
+ # 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,344 @@
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, DATA_FOLDER, DELAY,
9
+ DESCRIPTION, EXTRA_DATA, FOLLOW_REDIRECT,
10
+ HEADER, HOST, ID, IP, MATCHED_AT, NAME,
11
+ OPT_NOT_SUPPORTED, 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
+ from secator.utils import get_file_timestamp
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @task()
23
+ class nmap(VulnMulti):
24
+ """Network Mapper is a free and open source utility for network discovery and security auditing."""
25
+ cmd = 'nmap -sT -sV -Pn'
26
+ input_flag = None
27
+ input_chunk_size = 1
28
+ file_flag = '-iL'
29
+ opt_prefix = '--'
30
+ output_types = [Port, Vulnerability, Exploit]
31
+ opts = {
32
+ PORTS: {'type': str, 'help': 'Ports to scan', 'short': 'p'},
33
+ SCRIPT: {'type': str, 'default': 'vulners', 'help': 'NSE scripts'},
34
+ 'output_path': {'type': str, 'short': 'oX', 'default': None, 'help': 'Output XML file path'}
35
+ }
36
+ opt_key_map = {
37
+ HEADER: OPT_NOT_SUPPORTED,
38
+ DELAY: 'scan-delay',
39
+ FOLLOW_REDIRECT: OPT_NOT_SUPPORTED,
40
+ PROXY: None, # TODO: supports --proxies but not in TCP mode [https://github.com/nmap/nmap/issues/1098]
41
+ RATE_LIMIT: 'max-rate',
42
+ RETRIES: 'max-retries',
43
+ THREADS: OPT_NOT_SUPPORTED,
44
+ TIMEOUT: 'max-rtt-timeout',
45
+ USER_AGENT: OPT_NOT_SUPPORTED,
46
+
47
+ # Nmap opts
48
+ PORTS: '-p',
49
+ 'output_path': '-oX'
50
+ }
51
+ opt_value_map = {
52
+ PORTS: lambda x: ','.join([str(p) for p in x]) if isinstance(x, list) else x
53
+ }
54
+ install_cmd = (
55
+ 'sudo apt install -y nmap && sudo git clone https://github.com/scipag/vulscan /opt/scipag_vulscan || true && '
56
+ 'sudo ln -s /opt/scipag_vulscan /usr/share/nmap/scripts/vulscan || true'
57
+ )
58
+ proxychains = True
59
+ proxychains_flavor = 'proxychains4'
60
+ proxy_socks5 = False
61
+ proxy_http = False
62
+ profile = 'io'
63
+
64
+ def yielder(self):
65
+ yield from super().yielder()
66
+ if self.return_code != 0:
67
+ return
68
+ self.results = []
69
+ note = f'nmap XML results saved to {self.output_path}'
70
+ if self.print_line:
71
+ self._print(note)
72
+ if os.path.exists(self.output_path):
73
+ nmap_data = self.xml_to_json()
74
+ yield from nmap_data
75
+
76
+ def xml_to_json(self):
77
+ results = []
78
+ with open(self.output_path, 'r') as f:
79
+ content = f.read()
80
+ try:
81
+ results = xmltodict.parse(content) # parse XML to dict
82
+ except Exception as e:
83
+ logger.exception(e)
84
+ logger.error(
85
+ f'Cannot parse nmap XML output {self.output_path} to valid JSON.')
86
+ results['_host'] = self.input
87
+ return nmapData(results)
88
+
89
+ @staticmethod
90
+ def on_init(self):
91
+ output_path = self.get_opt_value('output_path')
92
+ if not output_path:
93
+ timestr = get_file_timestamp()
94
+ output_path = f'{DATA_FOLDER}/nmap_{timestr}.xml'
95
+ self.output_path = output_path
96
+ self.cmd += f' -oX {self.output_path}'
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}')
342
+
343
+ def _parse_http_csrf_output(self, out, port_data):
344
+ pass
@@ -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': 'id', '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'
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['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