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.
- moriarty/__init__.py +1 -1
- moriarty/cli/app.py +2 -2
- moriarty/cli/domain_cmd.py +127 -634
- moriarty/modules/domain_scanner.py +21 -16
- moriarty/modules/port_scanner_nmap.py +369 -102
- {moriarty_project-0.1.26.dist-info → moriarty_project-0.1.27.dist-info}/METADATA +5 -5
- {moriarty_project-0.1.26.dist-info → moriarty_project-0.1.27.dist-info}/RECORD +9 -11
- moriarty/cli/wifippler.py +0 -124
- moriarty/modules/port_scanner.py +0 -1050
- {moriarty_project-0.1.26.dist-info → moriarty_project-0.1.27.dist-info}/WHEEL +0 -0
- {moriarty_project-0.1.26.dist-info → moriarty_project-0.1.27.dist-info}/entry_points.txt +0 -0
moriarty/modules/port_scanner.py
DELETED
@@ -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"]
|