moriarty-project 0.1.24__py3-none-any.whl → 0.1.26__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/domain_cmd.py +629 -119
- moriarty/modules/domain_scanner.py +182 -59
- moriarty/modules/port_scanner.py +312 -122
- moriarty/modules/port_scanner_nmap.py +290 -0
- moriarty/modules/web_crawler.py +774 -152
- {moriarty_project-0.1.24.dist-info → moriarty_project-0.1.26.dist-info}/METADATA +5 -3
- {moriarty_project-0.1.24.dist-info → moriarty_project-0.1.26.dist-info}/RECORD +10 -9
- {moriarty_project-0.1.24.dist-info → moriarty_project-0.1.26.dist-info}/WHEEL +0 -0
- {moriarty_project-0.1.24.dist-info → moriarty_project-0.1.26.dist-info}/entry_points.txt +0 -0
moriarty/modules/port_scanner.py
CHANGED
@@ -2,25 +2,19 @@
|
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
4
|
import asyncio
|
5
|
-
import contextlib
|
6
5
|
import json
|
7
6
|
import re
|
8
|
-
import socket
|
9
7
|
import ssl
|
8
|
+
import contextlib
|
10
9
|
from dataclasses import dataclass, field
|
11
10
|
from datetime import datetime
|
12
|
-
from typing import Dict, List, Optional,
|
13
|
-
|
14
|
-
# Importa a classe ServiceInfo para uso no código
|
15
|
-
|
16
|
-
import aiohttp
|
17
|
-
import dns.resolver
|
11
|
+
from typing import Dict, List, Optional, Any, Set
|
12
|
+
from pathlib import Path
|
18
13
|
import dns.asyncresolver
|
19
|
-
import OpenSSL.crypto
|
20
14
|
import structlog
|
21
15
|
from rich.console import Console
|
22
|
-
from rich.
|
23
|
-
from rich.table import Table
|
16
|
+
from rich.progress import track
|
17
|
+
from rich.table import Table, box
|
24
18
|
|
25
19
|
logger = structlog.get_logger(__name__)
|
26
20
|
console = Console()
|
@@ -37,6 +31,34 @@ PROFILES = {
|
|
37
31
|
"all": list(range(1, 65536)),
|
38
32
|
}
|
39
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
|
+
|
40
62
|
# Mapeamento de portas para serviços comuns
|
41
63
|
SERVICE_MAP = {
|
42
64
|
21: "FTP",
|
@@ -117,9 +139,10 @@ SERVICE_MAP = {
|
|
117
139
|
27018: "MongoDB",
|
118
140
|
27019: "MongoDB",
|
119
141
|
28017: "MongoDB",
|
120
|
-
32608: "Kubernetes"
|
142
|
+
32608: "Kubernetes"
|
121
143
|
}
|
122
144
|
|
145
|
+
|
123
146
|
# Vulnerabilidades comuns por serviço
|
124
147
|
VULNERABILITIES = {
|
125
148
|
"SSH": ["CVE-2016-0777", "CVE-2016-0778", "CVE-2018-15473"],
|
@@ -147,15 +170,31 @@ class ServiceInfo:
|
|
147
170
|
ssl: bool = False
|
148
171
|
ssl_info: Optional[Dict[str, Any]] = None
|
149
172
|
banner: Optional[str] = None
|
150
|
-
vulns: List[str] = field(default_factory=list)
|
173
|
+
vulns: List[str] = field(default_factory=list) # Alterado para List[str]
|
151
174
|
cpe: Optional[str] = None
|
152
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
|
+
}
|
153
192
|
|
154
193
|
@dataclass
|
155
194
|
class PortScanResult:
|
156
195
|
port: int
|
157
196
|
protocol: str = "tcp"
|
158
|
-
status: str = "
|
197
|
+
status: str = "closed"
|
159
198
|
target: Optional[str] = None
|
160
199
|
service: Optional[ServiceInfo] = None
|
161
200
|
banner: Optional[str] = None
|
@@ -218,152 +257,272 @@ class PortScanner:
|
|
218
257
|
# Ajusta timeout baseado no nível de stealth
|
219
258
|
self.timeout = timeout * (1 + (self.stealth_level * 0.5))
|
220
259
|
|
221
|
-
# Cache para serviços já identificados
|
222
|
-
self.service_cache: Dict[int, ServiceInfo] = {}
|
223
|
-
|
224
260
|
# Resolvedor DNS assíncrono
|
225
261
|
self.resolver = dns.asyncresolver.Resolver()
|
226
262
|
self.resolver.timeout = 2.0
|
227
263
|
self.resolver.lifetime = 2.0
|
228
264
|
|
229
265
|
async def scan(self) -> List[PortScanResult]:
|
230
|
-
"""Executa a varredura de portas."""
|
231
|
-
console.print(f"[bold]Iniciando varredura em {self.target}[/bold]")
|
232
|
-
|
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]")
|
233
271
|
|
234
|
-
ports = PROFILES[self.profile]
|
235
272
|
if self.stealth_level > 0:
|
236
|
-
|
237
|
-
random.shuffle(ports)
|
238
|
-
|
239
|
-
sem = asyncio.Semaphore(self.concurrency)
|
240
|
-
results: List[PortScanResult] = []
|
273
|
+
console.print(f"🔒 Modo furtivo: nível {self.stealth_level}")
|
241
274
|
|
242
|
-
|
243
|
-
|
244
|
-
result = await self._probe_port(port)
|
245
|
-
if result:
|
246
|
-
results.append(result)
|
247
|
-
self._print_result(result)
|
275
|
+
results: List[PortScanResult] = []
|
276
|
+
sem = asyncio.Semaphore(self.concurrency)
|
248
277
|
|
249
|
-
#
|
250
|
-
|
251
|
-
|
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})")
|
252
299
|
|
253
300
|
# Ordena os resultados por número de porta
|
254
301
|
results.sort(key=lambda r: r.port)
|
255
302
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
port_info = f"[bold blue]{result.port:>5}/tcp[/bold blue]"
|
261
|
-
status = "[green]open[/green]" if result.status == "open" else "[yellow]filtered[/yellow]"
|
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"]
|
262
307
|
|
263
|
-
|
264
|
-
|
308
|
+
# Exibe resultados
|
309
|
+
console.print("\n[bold]📊 Resultados da varredura:[/bold]")
|
265
310
|
|
266
|
-
|
267
|
-
|
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")
|
268
318
|
|
269
|
-
|
270
|
-
|
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]", "")
|
271
336
|
|
272
|
-
|
273
|
-
|
274
|
-
|
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")
|
275
346
|
|
276
|
-
|
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}")
|
277
353
|
|
278
|
-
|
279
|
-
console.print(f" [dim]Banner: {result.banner[:100]}{'...' if len(result.banner) > 100 else ''}[/dim]")
|
354
|
+
return results
|
280
355
|
|
281
|
-
|
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:
|
282
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
|
+
|
283
363
|
# Atraso aleatório para evitar detecção
|
284
364
|
if self.stealth_level > 0:
|
365
|
+
import random
|
285
366
|
await asyncio.sleep(random.uniform(0.01, 0.5) * self.stealth_level)
|
286
367
|
|
287
|
-
#
|
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
|
+
|
288
407
|
try:
|
289
|
-
#
|
290
|
-
|
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
|
291
435
|
reader, writer = await asyncio.wait_for(
|
292
436
|
asyncio.open_connection(self.target, port),
|
293
|
-
timeout=
|
437
|
+
timeout=self.timeout
|
294
438
|
)
|
295
439
|
|
296
|
-
# Se chegou aqui, a porta está aberta
|
297
|
-
|
298
|
-
|
299
|
-
# Tenta obter o banner do serviço
|
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
|
300
442
|
try:
|
301
|
-
#
|
302
|
-
|
443
|
+
# Lê o banner inicial (até 1024 bytes)
|
444
|
+
banner_bytes = await asyncio.wait_for(
|
445
|
+
reader.read(1024),
|
446
|
+
timeout=self.timeout
|
447
|
+
)
|
303
448
|
|
304
|
-
#
|
305
|
-
|
306
|
-
await writer.drain()
|
449
|
+
# Se chegou aqui sem exceção, a porta está realmente aberta
|
450
|
+
result.status = "open"
|
307
451
|
|
308
|
-
# Lê até 1024 bytes
|
309
|
-
banner_bytes = await asyncio.wait_for(reader.read(1024), timeout=read_timeout)
|
310
452
|
if banner_bytes:
|
311
453
|
# Tenta decodificar como texto
|
312
454
|
try:
|
313
455
|
banner = banner_bytes.decode('utf-8', errors='replace').strip()
|
314
456
|
result.banner = banner
|
315
457
|
|
316
|
-
#
|
458
|
+
# Identifica o serviço baseado no banner
|
317
459
|
service_info = await self._identify_service(port, banner)
|
318
460
|
if service_info:
|
319
461
|
result.service = service_info
|
462
|
+
|
320
463
|
except UnicodeDecodeError:
|
321
|
-
# Se não for texto,
|
322
|
-
result.banner = banner_bytes.hex(
|
323
|
-
except (asyncio.TimeoutError, ConnectionResetError, OSError):
|
324
|
-
# Ignora erros de leitura do banner
|
325
|
-
pass
|
326
|
-
|
327
|
-
# Verifica se é um serviço SSL/TLS
|
328
|
-
if port in [443, 465, 636, 993, 995, 8443] or (result.service and result.service.ssl):
|
329
|
-
ssl_info = await self._check_ssl(port)
|
330
|
-
if ssl_info:
|
331
|
-
if not result.service:
|
332
|
-
result.service = ServiceInfo(name="ssl")
|
333
|
-
result.service.ssl = True
|
334
|
-
result.service.ssl_info = ssl_info
|
335
|
-
|
336
|
-
# Tenta identificar o serviço SSL
|
337
|
-
if not result.service.name or result.service.name == "ssl":
|
338
|
-
service_name = self._identify_ssl_service(port, ssl_info)
|
339
|
-
result.service.name = service_name
|
340
|
-
|
341
|
-
# Se não identificou o serviço, tenta pelo número da porta
|
342
|
-
if not result.service and port in SERVICE_MAP:
|
343
|
-
result.service = ServiceInfo(name=SERVICE_MAP[port])
|
464
|
+
# Se não for texto, exibe como hex
|
465
|
+
result.banner = f"[binary data] {banner_bytes.hex()[:100]}..."
|
344
466
|
|
345
|
-
#
|
346
|
-
if
|
347
|
-
|
348
|
-
|
349
|
-
|
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
|
350
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
|
+
|
351
495
|
except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
|
352
496
|
# Porta fechada ou inacessível
|
353
|
-
|
497
|
+
result.status = "closed"
|
354
498
|
|
355
499
|
except Exception as e:
|
356
|
-
logger.error(f"Erro ao verificar porta {port}: {str(e)}")
|
357
|
-
|
500
|
+
logger.error(f"Erro inesperado ao verificar porta {port}: {str(e)}")
|
501
|
+
result.status = "error"
|
358
502
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
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
|
367
526
|
|
368
527
|
async def _identify_service(self, port: int, banner: str) -> Optional[ServiceInfo]:
|
369
528
|
"""Tenta identificar o serviço rodando na porta com base no banner."""
|
@@ -752,6 +911,27 @@ class PortScanner:
|
|
752
911
|
await writer.wait_closed()
|
753
912
|
except:
|
754
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
|
+
|
755
935
|
|
756
936
|
def _check_known_vulns(self, port: int, service_name: str) -> List[str]:
|
757
937
|
"""Verifica vulnerabilidades conhecidas para o serviço na porta."""
|
@@ -762,29 +942,35 @@ class PortScanner:
|
|
762
942
|
if service.lower() in service_name.lower():
|
763
943
|
vulns.extend(cves)
|
764
944
|
|
765
|
-
# Verifica vulnerabilidades específicas da porta
|
945
|
+
# Verifica vulnerabilidades específicas da porta (apenas como informação, não como confirmação)
|
766
946
|
if port == 22: # SSH
|
767
|
-
vulns.extend(["CVE-2016-0777", "CVE-2016-0778", "CVE-2018-15473"])
|
947
|
+
vulns.extend(["CVE-2016-0777 (verificar versão)", "CVE-2016-0778 (verificar versão)", "CVE-2018-15473 (verificar versão)"])
|
768
948
|
elif port == 445: # SMB
|
769
|
-
vulns.extend(["
|
949
|
+
vulns.extend(["Possível vulnerabilidade SMB - requer verificação de versão"])
|
770
950
|
elif port == 3389: # RDP
|
771
|
-
vulns.extend(["
|
951
|
+
vulns.extend(["Possível vulnerabilidade RDP - requer verificação de versão"])
|
772
952
|
elif port == 27017: # MongoDB
|
773
|
-
vulns.extend(["
|
953
|
+
vulns.extend(["Acesso não autenticado (se sem senha)"])
|
774
954
|
elif port == 9200: # Elasticsearch
|
775
|
-
vulns.extend(["
|
955
|
+
vulns.extend(["Possível exposição indevida - verificar configurações"])
|
776
956
|
elif port == 11211: # Memcached
|
777
|
-
vulns.extend(["DRDoS
|
957
|
+
vulns.extend(["Possível amplificação DRDoS - verificar configuração"])
|
778
958
|
elif port == 2375: # Docker
|
779
|
-
vulns.extend(["
|
959
|
+
vulns.extend(["API Docker exposta - verificar autenticação"])
|
780
960
|
elif port == 10250: # Kubelet
|
781
|
-
vulns.extend(["
|
961
|
+
vulns.extend(["Kubelet exposto - verificar autenticação"])
|
782
962
|
|
783
963
|
return list(set(vulns)) # Remove duplicatas
|
784
964
|
|
785
965
|
|
786
|
-
def format_scan_results(results: List[PortScanResult], output_format: str = "text") -> str:
|
787
|
-
"""Formata os resultados da varredura no formato solicitado.
|
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
|
+
"""
|
788
974
|
if output_format.lower() == "json":
|
789
975
|
return json.dumps([r.to_dict() for r in results], indent=2)
|
790
976
|
|
@@ -793,7 +979,7 @@ def format_scan_results(results: List[PortScanResult], output_format: str = "tex
|
|
793
979
|
output.append("")
|
794
980
|
output.append(f"[bold]Resultado da varredura de portas[/bold]")
|
795
981
|
output.append(f"Alvo: {results[0].target if results else 'N/A'}")
|
796
|
-
output.append(f"Portas verificadas: {len(results)}")
|
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'])}")
|
797
983
|
output.append("-" * 80)
|
798
984
|
|
799
985
|
# Cabeçalho da tabela
|
@@ -807,9 +993,13 @@ def format_scan_results(results: List[PortScanResult], output_format: str = "tex
|
|
807
993
|
if result.status == "open":
|
808
994
|
port = f"[green]{result.port}[/green]"
|
809
995
|
status = "[green]ABERTA[/green]"
|
996
|
+
elif result.status == "closed":
|
997
|
+
port = f"[red]{result.port}[/red]"
|
998
|
+
status = "[red]FECHADA[/red]"
|
810
999
|
else:
|
811
1000
|
port = f"[yellow]{result.port}[/yellow]"
|
812
|
-
status = "[yellow]
|
1001
|
+
status = "[yellow]ERRO[/yellow]"
|
1002
|
+
|
813
1003
|
|
814
1004
|
service = result.service.name if result.service else "desconhecido"
|
815
1005
|
version = f" {result.service.version}" if result.service and result.service.version else ""
|