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.

Files changed (90) hide show
  1. secator/celery.py +160 -185
  2. secator/celery_utils.py +268 -0
  3. secator/cli.py +427 -176
  4. secator/config.py +114 -68
  5. secator/configs/workflows/host_recon.yaml +5 -3
  6. secator/configs/workflows/port_scan.yaml +7 -3
  7. secator/configs/workflows/subdomain_recon.yaml +2 -2
  8. secator/configs/workflows/url_bypass.yaml +10 -0
  9. secator/configs/workflows/url_dirsearch.yaml +1 -1
  10. secator/configs/workflows/url_vuln.yaml +1 -1
  11. secator/decorators.py +170 -92
  12. secator/definitions.py +11 -4
  13. secator/exporters/__init__.py +7 -5
  14. secator/exporters/console.py +10 -0
  15. secator/exporters/csv.py +27 -19
  16. secator/exporters/gdrive.py +16 -11
  17. secator/exporters/json.py +3 -1
  18. secator/exporters/table.py +30 -2
  19. secator/exporters/txt.py +20 -16
  20. secator/hooks/gcs.py +53 -0
  21. secator/hooks/mongodb.py +53 -27
  22. secator/installer.py +277 -60
  23. secator/output_types/__init__.py +29 -11
  24. secator/output_types/_base.py +11 -1
  25. secator/output_types/error.py +36 -0
  26. secator/output_types/exploit.py +12 -8
  27. secator/output_types/info.py +24 -0
  28. secator/output_types/ip.py +8 -1
  29. secator/output_types/port.py +9 -2
  30. secator/output_types/progress.py +5 -0
  31. secator/output_types/record.py +5 -3
  32. secator/output_types/stat.py +33 -0
  33. secator/output_types/subdomain.py +1 -1
  34. secator/output_types/tag.py +8 -6
  35. secator/output_types/target.py +2 -2
  36. secator/output_types/url.py +14 -11
  37. secator/output_types/user_account.py +6 -6
  38. secator/output_types/vulnerability.py +8 -6
  39. secator/output_types/warning.py +24 -0
  40. secator/report.py +56 -23
  41. secator/rich.py +44 -39
  42. secator/runners/_base.py +629 -638
  43. secator/runners/_helpers.py +5 -91
  44. secator/runners/celery.py +18 -0
  45. secator/runners/command.py +404 -214
  46. secator/runners/scan.py +8 -24
  47. secator/runners/task.py +21 -55
  48. secator/runners/workflow.py +41 -40
  49. secator/scans/__init__.py +28 -0
  50. secator/serializers/dataclass.py +6 -0
  51. secator/serializers/json.py +10 -5
  52. secator/serializers/regex.py +12 -4
  53. secator/tasks/_categories.py +147 -42
  54. secator/tasks/bbot.py +295 -0
  55. secator/tasks/bup.py +99 -0
  56. secator/tasks/cariddi.py +38 -49
  57. secator/tasks/dalfox.py +3 -0
  58. secator/tasks/dirsearch.py +14 -25
  59. secator/tasks/dnsx.py +49 -30
  60. secator/tasks/dnsxbrute.py +4 -1
  61. secator/tasks/feroxbuster.py +10 -20
  62. secator/tasks/ffuf.py +3 -2
  63. secator/tasks/fping.py +4 -4
  64. secator/tasks/gau.py +5 -0
  65. secator/tasks/gf.py +2 -2
  66. secator/tasks/gospider.py +4 -0
  67. secator/tasks/grype.py +11 -13
  68. secator/tasks/h8mail.py +32 -42
  69. secator/tasks/httpx.py +58 -21
  70. secator/tasks/katana.py +19 -23
  71. secator/tasks/maigret.py +27 -25
  72. secator/tasks/mapcidr.py +2 -3
  73. secator/tasks/msfconsole.py +22 -19
  74. secator/tasks/naabu.py +18 -2
  75. secator/tasks/nmap.py +82 -55
  76. secator/tasks/nuclei.py +13 -3
  77. secator/tasks/searchsploit.py +26 -11
  78. secator/tasks/subfinder.py +5 -1
  79. secator/tasks/wpscan.py +91 -94
  80. secator/template.py +61 -45
  81. secator/thread.py +24 -0
  82. secator/utils.py +417 -78
  83. secator/utils_test.py +48 -23
  84. secator/workflows/__init__.py +28 -0
  85. {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/METADATA +59 -48
  86. secator-0.8.0.dist-info/RECORD +115 -0
  87. {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/WHEEL +1 -1
  88. secator-0.6.0.dist-info/RECORD +0 -101
  89. {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/entry_points.txt +0 -0
  90. {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 -sT -sV -Pn'
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
- # 'tcp_connect': {'type': bool, 'short': 'sT', 'default': False, 'help': 'TCP Connect scan'},
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 apt install -y nmap && sudo git clone https://github.com/scipag/vulscan /opt/scipag_vulscan || true && '
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
- if tcp_syn_stealth:
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
- self.cmd = self.cmd.replace('-sT', '')
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
- def yielder(self):
83
- yield from super().yielder()
84
- if self.return_code != 0:
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
- self.results = []
87
- note = f'nmap XML results saved to {self.output_path}'
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 e:
101
- logger.exception(e)
102
- logger.error(
103
- f'Cannot parse nmap XML output {self.output_path} to valid JSON.')
104
- results['_host'] = self.input
105
- return nmapData(results)
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['service_name']
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
- EXTRA_DATA: extra_data
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 vuln in func(output, cpes=cpes):
173
- vuln.update(metadata)
185
+ for data in func(output, cpes=cpes):
186
+ data.update(metadata)
174
187
  confidence = 'low'
175
- if 'cpe-match' in vuln[TAGS]:
188
+ if 'cpe-match' in data[TAGS]:
176
189
  confidence = 'high' if version_exact else 'medium'
177
- vuln[CONFIDENCE] = confidence
178
- if (CONFIG.runners.skip_cve_low_confidence and vuln[CONFIDENCE] == 'low'):
179
- debug(f'{vuln[ID]}: ignored (low confidence).', sub='cve')
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 vuln
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 = host_cfg.get('address', {}).get('@addr', None)
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 host_cfg.get('address', {}).get('@addr', None)
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('name', None) or extra_data.get('product', None)
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=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
- vuln = {
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
- # vuln_data = VulnMulti.lookup_exploitdb(edb_id)
367
- yield vuln
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
- vuln[TAGS].append('cve')
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, DEFAULT_NUCLEI_FLAGS)
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 = f'nuclei {DEFAULT_NUCLEI_FLAGS}'
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 && nuclei update-templates'
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
@@ -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-DB command line search tool."""
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
- install_cmd = 'sudo git clone https://gitlab.com/exploit-database/exploitdb.git /opt/exploitdb || true && sudo ln -sf /opt/exploitdb/searchsploit /usr/local/bin/searchsploit' # noqa: E501
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
- _in = self.input
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.input = split[1]
65
- if isinstance(self.input, str):
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
- versions, title = tuple(group[1].split(' - '))
84
- item.name = title
85
- product_info = [f'{product.lower()} {v.strip()}' for v in versions.split('/')]
86
- item.tags = product_info + item.tags
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.input.replace('\'', '').split(' '))
104
+ input_tag = '-'.join(self.inputs[0].replace('\'', '').split(' '))
90
105
  item.tags = [input_tag] + item.tags
91
106
  return item
@@ -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
- return item['input'] != 'localhost'
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
- install_cmd = 'sudo apt install -y build-essential ruby-dev rubygems && sudo gem install wpscan'
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
- def yielder(self):
77
- prev = self.print_item_count
78
- self.print_item_count = False
79
- yield from super().yielder()
80
- if self.return_code != 0:
81
- return
82
- self.results = []
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
- note = f'wpscan JSON results saved to {self.output_path}'
87
- if self.print_line:
88
- self._print(note)
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
- if os.path.exists(self.output_path):
91
- with open(self.output_path, 'r') as f:
92
- data = json.load(f)
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
- if self.orig:
95
- yield data
96
- return
105
+ # Get URL
106
+ target = data.get('target_url', self.inputs[0])
97
107
 
98
- # Get URL
99
- target = data.get('target_url', self.targets)
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
- # Wordpress version
102
- version = data.get('version', {})
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
- wp_version = version['number']
105
- wp_version_status = version['status']
106
- if wp_version_status == 'outdated':
107
- vuln = version
108
- vuln.update({
109
- 'url': target,
110
- 'to_s': 'Wordpress outdated version',
111
- 'type': wp_version,
112
- 'references': {},
113
- })
114
- yield vuln
115
-
116
- # Main theme
117
- main_theme = data.get('main_theme', {})
118
- if main_theme:
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
- # Interesting findings
141
- interesting_findings = data.get('interesting_findings', [])
142
- for item in interesting_findings:
143
- yield item
147
+ # Interesting findings
148
+ interesting_findings = data.get('interesting_findings', [])
149
+ for item in interesting_findings:
150
+ yield item
144
151
 
145
- # Plugins
146
- plugins = data.get('plugins', {})
147
- for _, data in plugins.items():
148
- version = data.get('version', {})
149
- slug = data['slug']
150
- location = data['location']
151
- if version:
152
- number = version['number']
153
- latest_version = data.get('latest_version')
154
- yield Tag(
155
- name=f'Wordpress plugin - {slug} {number}',
156
- match=target,
157
- extra_data={
158
- 'url': location,
159
- 'latest_version': latest_version
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}'