moriarty-project 0.1.26__py3-none-any.whl → 0.1.27__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.
@@ -1,1050 +0,0 @@
1
- """Port scanning avançado com detecção de serviços e vulnerabilidades."""
2
- from __future__ import annotations
3
-
4
- import asyncio
5
- import json
6
- import re
7
- import ssl
8
- import contextlib
9
- from dataclasses import dataclass, field
10
- from datetime import datetime
11
- from typing import Dict, List, Optional, Any, Set
12
- from pathlib import Path
13
- import dns.asyncresolver
14
- import structlog
15
- from rich.console import Console
16
- from rich.progress import track
17
- from rich.table import Table, box
18
-
19
- logger = structlog.get_logger(__name__)
20
- console = Console()
21
-
22
- # Perfis de varredura
23
- PROFILES = {
24
- "quick": [21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 389, 443, 445,
25
- 465, 587, 993, 995, 1433, 1521, 2049, 3306, 3389, 5432, 5900, 6379,
26
- 8080, 8443, 9000, 10000, 27017],
27
- "web": [80, 443, 8080, 8443, 8000, 8888, 10443, 4443],
28
- "mail": [25, 110, 143, 465, 587, 993, 995],
29
- "db": [1433, 1521, 27017, 27018, 27019, 28017, 3306, 5000, 5432, 5984, 6379, 8081],
30
- "full": list(range(1, 1025)),
31
- "all": list(range(1, 65536)),
32
- }
33
-
34
- # URL base para atualizações de serviços e vulnerabilidades
35
- SERVICES_DB_URL = "https://raw.githubusercontent.com/nmap/nmap/master/nmap-services"
36
- VULN_DB_URL = "https://cve.mitre.org/data/downloads/allitems.csv"
37
-
38
- # Diretório para armazenar dados locais
39
- DATA_DIR = Path.home() / ".moriarty" / "data"
40
- SERVICES_DB = DATA_DIR / "services.yml"
41
- VULN_DB = DATA_DIR / "vulnerabilities.yml"
42
-
43
- # Garante que o diretório de dados existe
44
- DATA_DIR.mkdir(parents=True, exist_ok=True)
45
-
46
- # Estrutura para armazenar assinaturas de serviços
47
- @dataclass
48
- class ServiceSignature:
49
- name: str
50
- port: int
51
- protocol: str = "tcp"
52
- banner_patterns: List[str] = field(default_factory=list)
53
- ssl_ports: Set[int] = field(default_factory=set)
54
- version_pattern: Optional[str] = None
55
- cpe: Optional[str] = None
56
- vulns: List[Dict[str, str]] = field(default_factory=list)
57
- last_updated: Optional[datetime] = None
58
-
59
- # Dicionário para armazenar assinaturas de serviços
60
- SERVICE_SIGNATURES: Dict[str, ServiceSignature] = {}
61
-
62
- # Mapeamento de portas para serviços comuns
63
- SERVICE_MAP = {
64
- 21: "FTP",
65
- 22: "SSH",
66
- 23: "Telnet",
67
- 25: "SMTP",
68
- 53: "DNS",
69
- 80: "HTTP",
70
- 110: "POP3",
71
- 111: "RPCbind",
72
- 135: "MSRPC",
73
- 139: "NetBIOS",
74
- 143: "IMAP",
75
- 389: "LDAP",
76
- 443: "HTTPS",
77
- 445: "SMB",
78
- 465: "SMTPS",
79
- 500: "IKE",
80
- 515: "LPD",
81
- 554: "RTSP",
82
- 587: "SMTP (Submission)",
83
- 631: "IPP",
84
- 636: "LDAPS",
85
- 993: "IMAPS",
86
- 995: "POP3S",
87
- 1080: "SOCKS",
88
- 1194: "OpenVPN",
89
- 1433: "MSSQL",
90
- 1521: "Oracle",
91
- 2049: "NFS",
92
- 2375: "Docker",
93
- 2376: "Docker TLS",
94
- 3000: "Node.js",
95
- 3306: "MySQL",
96
- 3389: "RDP",
97
- 5000: "UPnP",
98
- 5432: "PostgreSQL",
99
- 5601: "Kibana",
100
- 5672: "AMQP",
101
- 5900: "VNC",
102
- 5984: "CouchDB",
103
- 6379: "Redis",
104
- 8000: "HTTP-Alt",
105
- 8008: "HTTP-Alt",
106
- 8080: "HTTP-Proxy",
107
- 8081: "HTTP-Alt",
108
- 8088: "HTTP-Alt",
109
- 8089: "Splunk",
110
- 8090: "HTTP-Alt",
111
- 8091: "Couchbase",
112
- 8096: "Plex",
113
- 8125: "StatsD",
114
- 8140: "Puppet",
115
- 8200: "Vault",
116
- 8300: "Consul",
117
- 8333: "Bitcoin",
118
- 8443: "HTTPS-Alt",
119
- 8500: "Consul",
120
- 8545: "Ethereum",
121
- 8765: "Grafana",
122
- 8888: "Jupyter",
123
- 9000: "SonarQube",
124
- 9001: "Tor",
125
- 9042: "Cassandra",
126
- 9090: "Prometheus",
127
- 9092: "Kafka",
128
- 9100: "Node-Exporter",
129
- 9200: "Elasticsearch",
130
- 9300: "Elasticsearch",
131
- 9418: "Git",
132
- 9999: "JIRA",
133
- 10000: "Webmin",
134
- 10250: "Kubelet",
135
- 11211: "Memcached",
136
- 15672: "RabbitMQ",
137
- 16379: "Redis",
138
- 27017: "MongoDB",
139
- 27018: "MongoDB",
140
- 27019: "MongoDB",
141
- 28017: "MongoDB",
142
- 32608: "Kubernetes"
143
- }
144
-
145
-
146
- # Vulnerabilidades comuns por serviço
147
- VULNERABILITIES = {
148
- "SSH": ["CVE-2016-0777", "CVE-2016-0778", "CVE-2018-15473"],
149
- "SMB": ["EternalBlue", "SMBGhost", "EternalRomance", "SambaCry"],
150
- "RDP": ["BlueKeep", "CVE-2019-0708", "CVE-2019-1181", "CVE-2019-1182"],
151
- "Redis": ["Unauthenticated Access", "CVE-2015-4335", "CVE-2016-8339"],
152
- "MongoDB": ["Unauthenticated Access", "CVE-2016-6494"],
153
- "Elasticsearch": ["CVE-2015-1427", "CVE-2015-3337", "CVE-2015-5531"],
154
- "Memcached": ["DRDoS Amplification", "CVE-2016-8704", "CVE-2016-8705"],
155
- "Docker": ["CVE-2019-5736", "CVE-2019-13139", "CVE-2019-14271"],
156
- "Kubernetes": ["CVE-2018-1002105", "CVE-2019-11253", "CVE-2019-11255"],
157
- "VNC": ["CVE-2006-2369", "CVE-2015-5239", "CVE-2018-20019"],
158
- "Jenkins": ["CVE-2017-1000353", "CVE-2018-1000861", "CVE-2019-1003000"],
159
- "MySQL": ["CVE-2016-6662", "CVE-2016-6663", "CVE-2016-6664"],
160
- "PostgreSQL": ["CVE-2019-9193", "CVE-2018-1058", "CVE-2016-5423"],
161
- "Oracle": ["CVE-2012-1675", "CVE-2012-3137", "CVE-2018-3110"],
162
- "MSSQL": ["CVE-2019-1068", "CVE-2018-8273", "CVE-2018-8271"],
163
- }
164
-
165
- @dataclass
166
- class ServiceInfo:
167
- """Informações detalhadas sobre um serviço."""
168
- name: str
169
- version: Optional[str] = None
170
- ssl: bool = False
171
- ssl_info: Optional[Dict[str, Any]] = None
172
- banner: Optional[str] = None
173
- vulns: List[str] = field(default_factory=list) # Alterado para List[str]
174
- cpe: Optional[str] = None
175
- extra: Dict[str, Any] = field(default_factory=dict)
176
- confidence: float = 0.0 # Nível de confiança na identificação (0.0 a 1.0)
177
- last_checked: Optional[datetime] = None
178
-
179
- def to_dict(self) -> Dict[str, Any]:
180
- """Converte o objeto para dicionário."""
181
- return {
182
- "name": self.name,
183
- "version": self.version,
184
- "ssl": self.ssl,
185
- "ssl_info": self.ssl_info,
186
- "banner": self.banner,
187
- "vulns": self.vulns,
188
- "cpe": self.cpe,
189
- "confidence": self.confidence,
190
- "last_checked": self.last_checked.isoformat() if self.last_checked else None
191
- }
192
-
193
- @dataclass
194
- class PortScanResult:
195
- port: int
196
- protocol: str = "tcp"
197
- status: str = "closed"
198
- target: Optional[str] = None
199
- service: Optional[ServiceInfo] = None
200
- banner: Optional[str] = None
201
- timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
202
-
203
- def to_dict(self) -> Dict[str, Any]:
204
- """Converte o resultado para dicionário."""
205
- result = {
206
- "port": self.port,
207
- "protocol": self.protocol,
208
- "status": self.status,
209
- "banner": self.banner,
210
- "timestamp": self.timestamp,
211
- }
212
-
213
- if self.service:
214
- service_info = {
215
- "name": self.service.name,
216
- "version": self.service.version,
217
- "ssl": self.service.ssl,
218
- "vulnerabilities": self.service.vulns,
219
- "cpe": self.service.cpe,
220
- "extra": self.service.extra,
221
- }
222
- if self.service.ssl_info:
223
- service_info["ssl_info"] = self.service.ssl_info
224
-
225
- result["service"] = service_info
226
-
227
- return result
228
-
229
- def to_json(self) -> str:
230
- """Retorna uma representação JSON do resultado."""
231
- return json.dumps(self.to_dict(), indent=2)
232
-
233
- class PortScanner:
234
- """Execução assíncrona de port scanning com detecção avançada de serviços."""
235
-
236
- def __init__(
237
- self,
238
- target: str,
239
- profile: str = "quick",
240
- concurrency: int = 200,
241
- timeout: float = 2.0,
242
- stealth_level: int = 0,
243
- resolve_services: bool = True,
244
- check_vulns: bool = True,
245
- ):
246
- self.target = target
247
- self.profile = profile if profile in PROFILES else "quick"
248
- self.stealth_level = max(0, min(stealth_level, 5))
249
- self.resolve_services = resolve_services
250
- self.check_vulns = check_vulns
251
-
252
- # Ajusta concorrência baseado no nível de stealth
253
- stealth_factors = [1.0, 0.8, 0.6, 0.4, 0.2, 0.1]
254
- adjusted_concurrency = int(concurrency * stealth_factors[self.stealth_level])
255
- self.concurrency = max(10, min(adjusted_concurrency, 500))
256
-
257
- # Ajusta timeout baseado no nível de stealth
258
- self.timeout = timeout * (1 + (self.stealth_level * 0.5))
259
-
260
- # Resolvedor DNS assíncrono
261
- self.resolver = dns.asyncresolver.Resolver()
262
- self.resolver.timeout = 2.0
263
- self.resolver.lifetime = 2.0
264
-
265
- async def scan(self) -> List[PortScanResult]:
266
- """Executa a varredura de portas de forma assíncrona."""
267
- console.print(f"[bold]🔍 Iniciando varredura em {self.target}[/bold]")
268
- ports = sorted(PROFILES[self.profile])
269
- total_ports = len(ports)
270
- console.print(f"📊 Perfil: [bold]{self.profile}[/bold], Portas a verificar: [bold]{total_ports}[/bold]")
271
-
272
- if self.stealth_level > 0:
273
- console.print(f"🔒 Modo furtivo: nível {self.stealth_level}")
274
-
275
- results: List[PortScanResult] = []
276
- sem = asyncio.Semaphore(self.concurrency)
277
-
278
- # Barra de progresso
279
- with console.status("[bold]Verificando portas...") as status:
280
- async def scan_port(port: int) -> PortScanResult:
281
- async with sem:
282
- try:
283
- return await self._probe_port(port)
284
- except Exception as e:
285
- logger.debug(f"Erro ao escanear porta {port}: {str(e)}")
286
- return PortScanResult(port=port, status="error", target=self.target)
287
-
288
- # Executa as tarefas em lote para evitar sobrecarga de memória
289
- batch_size = min(100, max(10, self.concurrency * 2))
290
- for i in range(0, len(ports), batch_size):
291
- batch = ports[i:i + batch_size]
292
- tasks = [scan_port(port) for port in batch]
293
- batch_results = await asyncio.gather(*tasks)
294
- results.extend(batch_results)
295
-
296
- # Atualiza status
297
- open_ports = len([r for r in results if r.status == "open"])
298
- status.update(f"[bold]Verificando portas... {min(i + len(batch), total_ports)}/{total_ports} (abertas: {open_ports})")
299
-
300
- # Ordena os resultados por número de porta
301
- results.sort(key=lambda r: r.port)
302
-
303
- # Filtra e conta portas por status
304
- open_ports = [r for r in results if r.status == "open"]
305
- closed_ports = [r for r in results if r.status == "closed"]
306
- error_ports = [r for r in results if r.status == "error"]
307
-
308
- # Exibe resultados
309
- console.print("\n[bold]📊 Resultados da varredura:[/bold]")
310
-
311
- # Tabela de portas abertas
312
- if open_ports:
313
- table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
314
- table.add_column("Porta", style="red", width=10)
315
- table.add_column("Status", style="green")
316
- table.add_column("Serviço", style="blue")
317
- table.add_column("Detalhes", style="yellow")
318
-
319
- for result in open_ports:
320
- service = result.service.name if result.service else "desconhecido"
321
- version = f" {result.service.version}" if result.service and result.service.version else ""
322
- ssl = "🔒" if result.service and result.service.ssl else ""
323
- vulns = f" [red]({len(result.service.vulns)} vulns)" if result.service and result.service.vulns else ""
324
-
325
- table.add_row(
326
- f"{result.port}/tcp",
327
- "[green]ABERTA[/green]",
328
- f"[red]{service}{version}[/red] {ssl}",
329
- vulns
330
- )
331
-
332
- # Adiciona banner se disponível
333
- if result.banner:
334
- banner = result.banner.split('\n')[0][:80] # Pega apenas a primeira linha e limita o tamanho
335
- table.add_row("", "", f"[dim]↳ {banner}...[/dim]", "")
336
-
337
- console.print("\n[bold]🚪 Portas abertas:[/bold]")
338
- console.print(table)
339
-
340
- # Resumo
341
- console.print("\n[bold]📋 Resumo:[/bold]")
342
- console.print(f" • [green]Portas abertas:[/green] {len(open_ports)}")
343
- console.print(f" • [red]Portas fechadas:[/red] {len(closed_ports)}")
344
- if error_ports:
345
- console.print(f" • [yellow]Erros:[/yellow] {len(error_ports)} porta(s) com erro")
346
-
347
- # Lista de portas abertas resumida
348
- if open_ports:
349
- open_ports_str = ", ".join(str(r.port) for r in open_ports)
350
- if len(open_ports_str) > 80:
351
- open_ports_str = open_ports_str[:77] + "..."
352
- console.print(f"\n🔍 Portas abertas: {open_ports_str}")
353
-
354
- return results
355
-
356
- def _print_result(self, result: PortScanResult):
357
- """Método mantido para compatibilidade, mas não é mais usado internamente."""
358
- pass
359
- async def _probe_port(self, port: int) -> PortScanResult:
360
- """Verifica se uma porta está aberta e coleta informações do serviço."""
361
- result = PortScanResult(port=port, status="closed", target=self.target)
362
-
363
- # Atraso aleatório para evitar detecção
364
- if self.stealth_level > 0:
365
- import random
366
- await asyncio.sleep(random.uniform(0.01, 0.5) * self.stealth_level)
367
-
368
- # Portas que devem ser verificadas com TLS primeiro
369
- ssl_ports = {
370
- # HTTPS
371
- 443, # HTTPS padrão
372
- 8443, # HTTPS alternativo
373
- 10443, # HTTPS alternativo
374
- 4443, # HTTPS alternativo
375
- 1443, # HTTPS alternativo
376
- 9443, # Nginx Admin / HTTPS alternativo
377
- # Email seguro
378
- 465, # SMTPS
379
- 993, # IMAPS
380
- 995, # POP3S
381
- # Outros serviços seguros
382
- 636, # LDAPS
383
- 853, # DNS sobre TLS
384
- 989, # FTPS Data
385
- 990, # FTPS Control
386
- 992, # Telnet sobre TLS
387
- 994, # IRCS
388
- # Adicionais comuns
389
- 5061, # SIP-TLS
390
- 5062, # SIP-TLS
391
- 5989, # CIM XML sobre HTTPS
392
- 832, # NETCONF sobre TLS
393
- 6514, # Syslog sobre TLS
394
- 5684, # CoAP sobre DTLS
395
- 8883, # MQTT sobre SSL
396
- 8884, # MQTT sobre WebSocket
397
- 8888, # HTTP alternativo com SSL
398
- 10000, # Webmin
399
- 10001, # Webmin alternativo
400
- 20000 # Usermin (Webmin para usuários)
401
- }
402
-
403
- # Portas que devem tentar obter banner via TLS
404
- http_ports_with_tls = {443, 8443, 10443, 4443, 9443, 10000, 10001, 20000}
405
-
406
-
407
- try:
408
- # Para portas SSL conhecidas, tenta TLS primeiro
409
- if port in ssl_ports:
410
- ssl_info = await self._check_ssl(port)
411
- if ssl_info:
412
- result.status = "open"
413
- service_name = self._identify_ssl_service(port, ssl_info)
414
- result.service = ServiceInfo(name=service_name, ssl=True, ssl_info=ssl_info)
415
-
416
- # Tenta obter banner apenas para portas HTTP/HTTPS conhecidas
417
- if port in http_ports_with_tls:
418
- try:
419
- banner = await self._get_ssl_banner(port)
420
- if banner:
421
- result.banner = banner
422
- # Atualiza informações do serviço com base no banner
423
- service_info = await self._identify_service(port, banner)
424
- if service_info:
425
- result.service.name = service_info.name
426
- result.service.version = service_info.version
427
- result.service.cpe = service_info.cpe
428
- except Exception as e:
429
- logger.debug(f"Erro ao obter banner SSL da porta {port}: {str(e)}")
430
-
431
- return result
432
-
433
-
434
- # Se não for porta SSL ou falhar, tenta conexão TCP normal
435
- reader, writer = await asyncio.wait_for(
436
- asyncio.open_connection(self.target, port),
437
- timeout=self.timeout
438
- )
439
-
440
- # Se chegou aqui, a conexão TCP foi estabelecida, mas ainda não sabemos se a porta está realmente aberta
441
- # Vamos tentar ler o banner para confirmar
442
- try:
443
- # Lê o banner inicial (até 1024 bytes)
444
- banner_bytes = await asyncio.wait_for(
445
- reader.read(1024),
446
- timeout=self.timeout
447
- )
448
-
449
- # Se chegou aqui sem exceção, a porta está realmente aberta
450
- result.status = "open"
451
-
452
- if banner_bytes:
453
- # Tenta decodificar como texto
454
- try:
455
- banner = banner_bytes.decode('utf-8', errors='replace').strip()
456
- result.banner = banner
457
-
458
- # Identifica o serviço baseado no banner
459
- service_info = await self._identify_service(port, banner)
460
- if service_info:
461
- result.service = service_info
462
-
463
- except UnicodeDecodeError:
464
- # Se não for texto, exibe como hex
465
- result.banner = f"[binary data] {banner_bytes.hex()[:100]}..."
466
-
467
- # Se a porta está aberta, verifica SSL/TLS em portas comuns (apenas se ainda não verificou)
468
- if result.status == "open" and port in ssl_ports and (result.service is None or not result.service.ssl):
469
- try:
470
- ssl_info = await self._check_ssl(port)
471
- if ssl_info:
472
- if result.service is None:
473
- result.service = ServiceInfo(name=SERVICE_MAP.get(port, "unknown"))
474
- result.service.ssl = True
475
- result.service.ssl_info = ssl_info
476
- # Atualiza o nome do serviço com base nas informações SSL
477
- ssl_service = self._identify_ssl_service(port, ssl_info)
478
- if ssl_service:
479
- result.service.name = ssl_service
480
- except Exception as e:
481
- logger.debug(f"Erro ao verificar SSL na porta {port}: {str(e)}")
482
- # Se falhar ao verificar SSL, mantém a porta como aberta mas sem informações SSL
483
-
484
- except (asyncio.TimeoutError, ConnectionResetError, OSError) as e:
485
- logger.debug(f"Erro ao ler banner da porta {port}: {str(e)}")
486
-
487
- finally:
488
- # Fecha a conexão
489
- writer.close()
490
- try:
491
- await writer.wait_closed()
492
- except Exception as e:
493
- logger.debug(f"Erro ao fechar conexão: {str(e)}")
494
-
495
- except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
496
- # Porta fechada ou inacessível
497
- result.status = "closed"
498
-
499
- except Exception as e:
500
- logger.error(f"Erro inesperado ao verificar porta {port}: {str(e)}")
501
- result.status = "error"
502
-
503
- # Se a porta está aberta, tenta identificar o serviço se ainda não identificado
504
- if result.status == "open":
505
- try:
506
- # Se tem serviço SSL mas não foi identificado corretamente
507
- if result.service and (not result.service.name or result.service.name == "ssl"):
508
- if 'ssl_info' in locals() and ssl_info:
509
- service_name = self._identify_ssl_service(port, ssl_info)
510
- if service_name:
511
- result.service.name = service_name
512
-
513
- # Se ainda não identificou o serviço, tenta pelo número da porta
514
- if not result.service and port in SERVICE_MAP:
515
- result.service = ServiceInfo(name=SERVICE_MAP[port])
516
-
517
- # Verifica vulnerabilidades conhecidas para portas abertas
518
- if self.check_vulns and result.service:
519
- result.service.vulns = self._check_known_vulns(port, result.service.name)
520
- except Exception as e:
521
- logger.error(f"Erro ao processar informações da porta {port}: {str(e)}")
522
- # Em caso de erro, mantém a porta como aberta mas sem informações adicionais
523
-
524
-
525
- return result
526
-
527
- async def _identify_service(self, port: int, banner: str) -> Optional[ServiceInfo]:
528
- """Tenta identificar o serviço rodando na porta com base no banner."""
529
- if not banner:
530
- return None
531
-
532
- banner_lower = banner.lower()
533
- service = ServiceInfo(name="unknown")
534
-
535
- # Verifica por padrões comuns de banners
536
- if "apache" in banner_lower or "httpd" in banner_lower:
537
- service.name = "Apache HTTP Server"
538
- if match := re.search(r'Apache[/\s]([0-9.]+)', banner, re.IGNORECASE):
539
- service.version = match.group(1)
540
- service.cpe = f"cpe:/a:apache:http_server:{service.version}"
541
-
542
- elif "nginx" in banner_lower:
543
- service.name = "Nginx"
544
- if match := re.search(r'nginx[/\s]([0-9.]+)', banner, re.IGNORECASE):
545
- service.version = match.group(1)
546
- service.cpe = f"cpe:/a:nginx:nginx:{service.version}"
547
-
548
- elif "microsoft-iis" in banner_lower or "microsoft httpapi" in banner_lower:
549
- service.name = "Microsoft IIS"
550
- if match := re.search(r'Microsoft-IIS/([0-9.]+)', banner, re.IGNORECASE):
551
- service.version = match.group(1)
552
- service.cpe = f"cpe:/a:microsoft:iis:{service.version}"
553
-
554
- elif "openbsd openssh" in banner_lower or "openssh" in banner_lower:
555
- service.name = "OpenSSH"
556
- if match := re.search(r'openssh[_-]?([0-9.]+[a-z]*)', banner, re.IGNORECASE):
557
- service.version = match.group(1)
558
- service.cpe = f"cpe:/a:openbsd:openssh:{service.version}"
559
-
560
- elif "postfix" in banner_lower:
561
- service.name = "Postfix"
562
- if match := re.search(r'postfix[\s(]([0-9.]+)', banner, re.IGNORECASE):
563
- service.version = match.group(1)
564
- service.cpe = f"cpe:/a:postfix:postfix:{service.version}"
565
-
566
- elif "exim" in banner_lower:
567
- service.name = "Exim"
568
- if match := re.search(r'exim[\s(]([0-9.]+)', banner, re.IGNORECASE):
569
- service.version = match.group(1)
570
- service.cpe = f"cpe:/a:exim:exim:{service.version}"
571
-
572
- elif "dovecot" in banner_lower:
573
- service.name = "Dovecot"
574
- if match := re.search(r'dovecot[\s(]([0-9.]+)', banner, re.IGNORECASE):
575
- service.version = match.group(1)
576
- service.cpe = f"cpe:/a:dovecot:dovecot:{service.version}"
577
-
578
- elif "proftpd" in banner_lower:
579
- service.name = "ProFTPD"
580
- if match := re.search(r'proftpd[\s(]([0-9.]+)', banner, re.IGNORECASE):
581
- service.version = match.group(1)
582
- service.cpe = f"cpe:/a:proftpd:proftpd:{service.version}"
583
-
584
- elif "vsftpd" in banner_lower:
585
- service.name = "vsFTPd"
586
- if match := re.search(r'vsftpd[\s(]([0-9.]+)', banner, re.IGNORECASE):
587
- service.version = match.group(1)
588
- service.cpe = f"cpe:/a:vsftpd:vsftpd:{service.version}"
589
-
590
- elif "mysql" in banner_lower:
591
- service.name = "MySQL"
592
- if match := re.search(r'([0-9.]+)[- ]*mysql', banner, re.IGNORECASE):
593
- service.version = match.group(1)
594
- service.cpe = f"cpe:/a:mysql:mysql:{service.version}"
595
-
596
- elif "postgresql" in banner_lower or 'postgres' in banner_lower:
597
- service.name = "PostgreSQL"
598
- if match := re.search(r'postgresql[\s(]([0-9.]+)', banner, re.IGNORECASE):
599
- service.version = match.group(1)
600
- service.cpe = f"cpe:/a:postgresql:postgresql:{service.version}"
601
-
602
- elif "redis" in banner_lower:
603
- service.name = "Redis"
604
- if match := re.search(r'redis[\s:]([0-9.]+)', banner, re.IGNORECASE):
605
- service.version = match.group(1)
606
- service.cpe = f"cpe:/a:redis:redis:{service.version}"
607
-
608
- elif "mongodb" in banner_lower:
609
- service.name = "MongoDB"
610
- if match := re.search(r'mongod?b[\s(]([0-9.]+)', banner, re.IGNORECASE):
611
- service.version = match.group(1)
612
- service.cpe = f"cpe:/a:mongodb:mongodb:{service.version}"
613
-
614
- elif "microsoft sql server" in banner_lower or "sql server" in banner_lower:
615
- service.name = "Microsoft SQL Server"
616
- if match := re.search(r'sql server[\s(]([0-9.]+)', banner, re.IGNORECASE):
617
- service.version = match.group(1)
618
- service.cpe = f"cpe:/a:microsoft:sql_server:{service.version}"
619
-
620
- elif "oracle" in banner_lower and "database" in banner_lower:
621
- service.name = "Oracle Database"
622
- if match := re.search(r'oracle[\s(]([0-9.]+)', banner, re.IGNORECASE):
623
- service.version = match.group(1)
624
- service.cpe = f"cpe:/a:oracle:database:{service.version}"
625
-
626
- # Se não identificou pelo banner, tenta pela porta
627
- if service.name == "unknown" and port in SERVICE_MAP:
628
- service.name = SERVICE_MAP[port]
629
-
630
- # Verifica vulnerabilidades conhecidas
631
- if self.check_vulns:
632
- service.vulns = self._check_known_vulns(port, service.name)
633
-
634
- return service if service.name != "unknown" else None
635
-
636
- def _identify_ssl_service(self, port: int, ssl_info: Dict[str, Any]) -> str:
637
- """Tenta identificar o serviço baseado na porta e informações SSL."""
638
- # Mapeia portas comuns para serviços SSL
639
- ssl_services = {
640
- 443: "HTTPS",
641
- 465: "SMTPS",
642
- 563: "NNTPS",
643
- 636: "LDAPS",
644
- 853: "DNS-over-TLS",
645
- 989: "FTPS (data)",
646
- 990: "FTPS (control)",
647
- 992: "Telnet over TLS/SSL",
648
- 993: "IMAPS",
649
- 994: "IRC over SSL",
650
- 995: "POP3S",
651
- 1443: "HTTPS (alt)",
652
- 2376: "Docker TLS",
653
- 2377: "Docker Swarm",
654
- 3001: "HTTPS (Node.js)",
655
- 3306: "MySQL over SSL",
656
- 3389: "RDP over TLS",
657
- 4000: "HTTPS (alt)",
658
- 4001: "HTTPS (alt)",
659
- 4002: "HTTPS (alt)",
660
- 4003: "HTTPS (alt)",
661
- 4004: "HTTPS (alt)",
662
- 4005: "HTTPS (alt)",
663
- 4006: "HTTPS (alt)",
664
- 4007: "HTTPS (alt)",
665
- 4008: "HTTPS (alt)",
666
- 4009: "HTTPS (alt)",
667
- 4433: "HTTPS (alt)",
668
- 4443: "HTTPS (alt)",
669
- 5000: "HTTPS (alt)",
670
- 5001: "HTTPS (alt)",
671
- 5002: "HTTPS (alt)",
672
- 5003: "HTTPS (alt)",
673
- 5004: "HTTPS (alt)",
674
- 5005: "HTTPS (alt)",
675
- 5006: "HTTPS (alt)",
676
- 5007: "HTTPS (alt)",
677
- 5008: "HTTPS (alt)",
678
- 5009: "HTTPS (alt)",
679
- 5432: "PostgreSQL over SSL",
680
- 5671: "AMQPS",
681
- 5800: "VNC over TLS",
682
- 5901: "VNC over TLS (alt)",
683
- 6001: "HTTPS (alt)",
684
- 6002: "HTTPS (alt)",
685
- 6003: "HTTPS (alt)",
686
- 6004: "HTTPS (alt)",
687
- 6005: "HTTPS (alt)",
688
- 6006: "HTTPS (alt)",
689
- 6007: "HTTPS (alt)",
690
- 6008: "HTTPS (alt)",
691
- 6009: "HTTPS (alt)",
692
- 7000: "HTTPS (alt)",
693
- 7001: "HTTPS (alt)",
694
- 7002: "HTTPS (alt)",
695
- 7003: "HTTPS (alt)",
696
- 7004: "HTTPS (alt)",
697
- 7005: "HTTPS (alt)",
698
- 7006: "HTTPS (alt)",
699
- 7007: "HTTPS (alt)",
700
- 7008: "HTTPS (alt)",
701
- 7009: "HTTPS (alt)",
702
- 8000: "HTTPS (alt)",
703
- 8001: "HTTPS (alt)",
704
- 8002: "HTTPS (alt)",
705
- 8003: "HTTPS (alt)",
706
- 8004: "HTTPS (alt)",
707
- 8005: "HTTPS (alt)",
708
- 8006: "HTTPS (alt)",
709
- 8007: "HTTPS (alt)",
710
- 8008: "HTTPS (alt)",
711
- 8009: "HTTPS (alt)",
712
- 8080: "HTTPS (alt)",
713
- 8081: "HTTPS (alt)",
714
- 8082: "HTTPS (alt)",
715
- 8083: "HTTPS (alt)",
716
- 8084: "HTTPS (alt)",
717
- 8085: "HTTPS (alt)",
718
- 8086: "HTTPS (alt)",
719
- 8087: "HTTPS (alt)",
720
- 8088: "HTTPS (alt)",
721
- 8089: "HTTPS (alt)",
722
- 8090: "HTTPS (alt)",
723
- 8091: "HTTPS (alt)",
724
- 8443: "HTTPS (alt)",
725
- 8444: "HTTPS (alt)",
726
- 8445: "HTTPS (alt)",
727
- 8446: "HTTPS (alt)",
728
- 8447: "HTTPS (alt)",
729
- 8448: "HTTPS (alt)",
730
- 8449: "HTTPS (alt)",
731
- 9000: "HTTPS (alt)",
732
- 9001: "HTTPS (alt)",
733
- 9002: "HTTPS (alt)",
734
- 9003: "HTTPS (alt)",
735
- 9004: "HTTPS (alt)",
736
- 9005: "HTTPS (alt)",
737
- 9006: "HTTPS (alt)",
738
- 9007: "HTTPS (alt)",
739
- 9008: "HTTPS (alt)",
740
- 9009: "HTTPS (alt)",
741
- 9010: "HTTPS (alt)",
742
- 9443: "HTTPS (alt)",
743
- 10000: "HTTPS (alt)",
744
- 10443: "HTTPS (alt)",
745
- 18080: "HTTPS (alt)",
746
- 18081: "HTTPS (alt)",
747
- 18082: "HTTPS (alt)",
748
- 18083: "HTTPS (alt)",
749
- 18084: "HTTPS (alt)",
750
- 18085: "HTTPS (alt)",
751
- 18086: "HTTPS (alt)",
752
- 18087: "HTTPS (alt)",
753
- 18088: "HTTPS (alt)",
754
- 18089: "HTTPS (alt)",
755
- 20000: "HTTPS (alt)",
756
- 27017: "MongoDB over SSL",
757
- 27018: "MongoDB over SSL (alt)",
758
- 27019: "MongoDB over SSL (alt)",
759
- 28017: "MongoDB over SSL (alt)",
760
- 30000: "HTTPS (alt)",
761
- 30001: "HTTPS (alt)",
762
- 30002: "HTTPS (alt)",
763
- 30003: "HTTPS (alt)",
764
- 30004: "HTTPS (alt)",
765
- 30005: "HTTPS (alt)",
766
- 30006: "HTTPS (alt)",
767
- 30007: "HTTPS (alt)",
768
- 30008: "HTTPS (alt)",
769
- 30009: "HTTPS (alt)",
770
- 30010: "HTTPS (alt)",
771
- 40000: "HTTPS (alt)",
772
- 40001: "HTTPS (alt)",
773
- 40002: "HTTPS (alt)",
774
- 40003: "HTTPS (alt)",
775
- 40004: "HTTPS (alt)",
776
- 40005: "HTTPS (alt)",
777
- 40006: "HTTPS (alt)",
778
- 40007: "HTTPS (alt)",
779
- 40008: "HTTPS (alt)",
780
- 40009: "HTTPS (alt)",
781
- 40010: "HTTPS (alt)",
782
- 50000: "HTTPS (alt)",
783
- 50001: "HTTPS (alt)",
784
- 50002: "HTTPS (alt)",
785
- 50003: "HTTPS (alt)",
786
- 50004: "HTTPS (alt)",
787
- 50005: "HTTPS (alt)",
788
- 50006: "HTTPS (alt)",
789
- 50007: "HTTPS (alt)",
790
- 50008: "HTTPS (alt)",
791
- 50009: "HTTPS (alt)",
792
- 50010: "HTTPS (alt)",
793
- 60000: "HTTPS (alt)",
794
- 60001: "HTTPS (alt)",
795
- 60002: "HTTPS (alt)",
796
- 60003: "HTTPS (alt)",
797
- 60004: "HTTPS (alt)",
798
- 60005: "HTTPS (alt)",
799
- 60006: "HTTPS (alt)",
800
- 60007: "HTTPS (alt)",
801
- 60008: "HTTPS (alt)",
802
- 60009: "HTTPS (alt)",
803
- 60010: "HTTPS (alt)",
804
- }
805
-
806
- return ssl_services.get(port, "SSL Service")
807
-
808
- async def _check_ssl(self, port: int) -> Optional[Dict[str, Any]]:
809
- """Verifica informações SSL/TLS da porta."""
810
- ssl_info = {}
811
-
812
- try:
813
- # Cria um contexto SSL
814
- ssl_context = ssl.create_default_context()
815
- ssl_context.check_hostname = False
816
- ssl_context.verify_mode = ssl.CERT_NONE
817
-
818
- # Tenta conectar com SSL/TLS
819
- reader, writer = await asyncio.wait_for(
820
- asyncio.open_connection(
821
- self.target,
822
- port,
823
- ssl=ssl_context,
824
- server_hostname=self.target
825
- ),
826
- timeout=self.timeout
827
- )
828
-
829
- # Obtém o certificado
830
- ssl_object = writer.get_extra_info('ssl_object')
831
- if ssl_object and hasattr(ssl_object, 'getpeercert'):
832
- cert = ssl_object.getpeercert()
833
-
834
- # Extrai informações do certificado
835
- if cert:
836
- ssl_info = {
837
- 'version': ssl_object.version(),
838
- 'cipher': ssl_object.cipher(),
839
- 'compression': ssl_object.compression(),
840
- 'issuer': dict(x[0] for x in cert.get('issuer', [])),
841
- 'subject': dict(x[0] for x in cert.get('subject', [])),
842
- 'not_before': cert.get('notBefore'),
843
- 'not_after': cert.get('notAfter'),
844
- 'serial_number': cert.get('serialNumber'),
845
- 'subject_alt_name': [
846
- name[1] for name in cert.get('subjectAltName', [])
847
- if name[0] == 'DNS'
848
- ],
849
- 'ocsp': cert.get('OCSP', []),
850
- 'ca_issuers': cert.get('caIssuers', []),
851
- 'crl_distribution_points': cert.get('crlDistributionPoints', []),
852
- }
853
-
854
- # Verifica se o certificado está expirado
855
- from datetime import datetime
856
- now = datetime.utcnow()
857
- not_after = datetime.strptime(ssl_info['not_after'], '%b %d %H:%M:%S %Y %Z')
858
- ssl_info['expired'] = now > not_after
859
-
860
- # Verifica se o certificado é auto-assinado
861
- ssl_info['self_signed'] = (
862
- ssl_info['issuer'] == ssl_info['subject']
863
- and ssl_info['issuer'].get('organizationName', '').lower() != 'let\'s encrypt'
864
- )
865
-
866
- # Verifica se o certificado é válido para o domínio
867
- import idna
868
- from socket import gethostbyname
869
-
870
- try:
871
- hostname = idna.encode(self.target).decode('ascii')
872
- ip = gethostbyname(hostname)
873
-
874
- # Verifica se o IP está nos subjectAltNames
875
- alt_names = []
876
- for name in ssl_info.get('subject_alt_name', []):
877
- if name.startswith('*'):
878
- # Lida com wildcards básicos
879
- domain = name[2:] # Remove o *.
880
- if hostname.endswith(domain):
881
- alt_names.append(hostname)
882
- else:
883
- alt_names.append(name)
884
-
885
- ssl_info['valid_hostname'] = (
886
- hostname in alt_names or
887
- f'*.{hostname.split(".", 1)[1]}' in alt_names
888
- )
889
-
890
- # Verifica se o IP está nos subjectAltNames
891
- ssl_info['valid_ip'] = ip in alt_names
892
-
893
- except (UnicodeError, IndexError, OSError):
894
- ssl_info['valid_hostname'] = False
895
- ssl_info['valid_ip'] = False
896
-
897
- return ssl_info
898
-
899
- except (ssl.SSLError, asyncio.TimeoutError, ConnectionRefusedError, OSError) as e:
900
- logger.debug(f"Erro ao verificar SSL na porta {port}: {str(e)}")
901
- return None
902
-
903
- except Exception as e:
904
- logger.error(f"Erro inesperado ao verificar SSL na porta {port}: {str(e)}", exc_info=True)
905
- return None
906
-
907
- finally:
908
- if 'writer' in locals():
909
- writer.close()
910
- try:
911
- await writer.wait_closed()
912
- except:
913
- pass
914
-
915
- async def _get_ssl_banner(self, port: int) -> Optional[str]:
916
- try:
917
- ssl_context = ssl.create_default_context()
918
- ssl_context.check_hostname = False
919
- ssl_context.verify_mode = ssl.CERT_NONE
920
- reader, writer = await asyncio.wait_for(
921
- asyncio.open_connection(self.target, port, ssl=ssl_context, server_hostname=self.target),
922
- timeout=self.timeout
923
- )
924
- req = f"HEAD / HTTP/1.0\r\nHost: {self.target}\r\nUser-Agent: moriarty/1.0\r\nConnection: close\r\n\r\n"
925
- writer.write(req.encode("ascii"))
926
- await writer.drain()
927
- data = await asyncio.wait_for(reader.read(2048), timeout=self.timeout)
928
- writer.close()
929
- with contextlib.suppress(Exception):
930
- await writer.wait_closed()
931
- return data.decode("utf-8", errors="replace").strip()
932
- except Exception:
933
- return None
934
-
935
-
936
- def _check_known_vulns(self, port: int, service_name: str) -> List[str]:
937
- """Verifica vulnerabilidades conhecidas para o serviço na porta."""
938
- vulns = []
939
-
940
- # Verifica vulnerabilidades específicas do serviço
941
- for service, cves in VULNERABILITIES.items():
942
- if service.lower() in service_name.lower():
943
- vulns.extend(cves)
944
-
945
- # Verifica vulnerabilidades específicas da porta (apenas como informação, não como confirmação)
946
- if port == 22: # SSH
947
- vulns.extend(["CVE-2016-0777 (verificar versão)", "CVE-2016-0778 (verificar versão)", "CVE-2018-15473 (verificar versão)"])
948
- elif port == 445: # SMB
949
- vulns.extend(["Possível vulnerabilidade SMB - requer verificação de versão"])
950
- elif port == 3389: # RDP
951
- vulns.extend(["Possível vulnerabilidade RDP - requer verificação de versão"])
952
- elif port == 27017: # MongoDB
953
- vulns.extend(["Acesso não autenticado (se sem senha)"])
954
- elif port == 9200: # Elasticsearch
955
- vulns.extend(["Possível exposição indevida - verificar configurações"])
956
- elif port == 11211: # Memcached
957
- vulns.extend(["Possível amplificação DRDoS - verificar configuração"])
958
- elif port == 2375: # Docker
959
- vulns.extend(["API Docker exposta - verificar autenticação"])
960
- elif port == 10250: # Kubelet
961
- vulns.extend(["Kubelet exposto - verificar autenticação"])
962
-
963
- return list(set(vulns)) # Remove duplicatas
964
-
965
-
966
- def format_scan_results(results: List[PortScanResult], output_format: str = "text", total_ports: Optional[int] = None) -> str:
967
- """Formata os resultados da varredura no formato solicitado.
968
-
969
- Args:
970
- results: Lista de resultados da varredura
971
- output_format: Formato de saída ('text' ou 'json')
972
- total_ports: Número total de portas verificadas (opcional)
973
- """
974
- if output_format.lower() == "json":
975
- return json.dumps([r.to_dict() for r in results], indent=2)
976
-
977
- # Formato de texto para saída no console
978
- output = []
979
- output.append("")
980
- output.append(f"[bold]Resultado da varredura de portas[/bold]")
981
- output.append(f"Alvo: {results[0].target if results else 'N/A'}")
982
- output.append(f"Portas verificadas: {total_ports if total_ports is not None else len([r for r in results if r.status != 'error'])}")
983
- output.append("-" * 80)
984
-
985
- # Cabeçalho da tabela
986
- output.append(
987
- f"{'PORTA':<8} {'PROTOCOLO':<10} {'STATUS':<10} {'SERVIÇO':<25} {'VULNERABILIDADES'}"
988
- )
989
- output.append("-" * 80)
990
-
991
- # Linhas da tabela
992
- for result in results:
993
- if result.status == "open":
994
- port = f"[green]{result.port}[/green]"
995
- status = "[green]ABERTA[/green]"
996
- elif result.status == "closed":
997
- port = f"[red]{result.port}[/red]"
998
- status = "[red]FECHADA[/red]"
999
- else:
1000
- port = f"[yellow]{result.port}[/yellow]"
1001
- status = "[yellow]ERRO[/yellow]"
1002
-
1003
-
1004
- service = result.service.name if result.service else "desconhecido"
1005
- version = f" {result.service.version}" if result.service and result.service.version else ""
1006
- service_info = f"{service}{version}"
1007
-
1008
- if result.service and result.service.ssl:
1009
- service_info += " 🔒"
1010
-
1011
- vulns = ", ".join(result.service.vulns) if result.service and result.service.vulns else "-"
1012
-
1013
- output.append(
1014
- f"{port:<8} {'tcp':<10} {status:<10} {service_info:<25} {vulns}"
1015
- )
1016
-
1017
- # Resumo
1018
- open_ports = [r for r in results if r.status == "open"]
1019
- output.append("-" * 80)
1020
- output.append(f"Total de portas abertas: {len(open_ports)}")
1021
-
1022
- # Conta serviços por tipo
1023
- services = {}
1024
- for result in open_ports:
1025
- if result.service:
1026
- service_name = result.service.name
1027
- services[service_name] = services.get(service_name, 0) + 1
1028
-
1029
- if services:
1030
- output.append("\nServiços identificados:")
1031
- for service, count in sorted(services.items()):
1032
- output.append(f" - {service}: {count} porta{'s' if count > 1 else ''}")
1033
-
1034
- # Verifica vulnerabilidades críticas
1035
- critical_vulns = []
1036
- for result in open_ports:
1037
- if result.service and result.service.vulns:
1038
- for vuln in result.service.vulns:
1039
- if any(cve in vuln.upper() for cve in ["CVE", "MS"]):
1040
- critical_vulns.append((result.port, vuln))
1041
-
1042
- if critical_vulns:
1043
- output.append("\n[bold red]VULNERABILIDADES CRÍTICAS ENCONTRADAS:[/bold red]")
1044
- for port, vuln in critical_vulns:
1045
- output.append(f" - Porta {port}: {vuln}")
1046
-
1047
- return "\n".join(output)
1048
-
1049
-
1050
- __all__ = ["PortScanner", "PortScanResult", "format_scan_results"]