300 lines
12 KiB
Python
300 lines
12 KiB
Python
#!/usr/bin/env python
|
||
|
||
import asyncio
|
||
import dns.resolver
|
||
import argparse
|
||
import re
|
||
import socket
|
||
import sys
|
||
import time
|
||
from dataclasses import dataclass
|
||
from typing import List, Optional, Tuple
|
||
from rich.console import Console
|
||
from rich.table import Table
|
||
from rich import print as rprint
|
||
|
||
VERSION = "2.0.0"
|
||
|
||
@dataclass
|
||
class SecurityAnalysis:
|
||
score: int
|
||
features: List[str]
|
||
vulnerabilities: List[str]
|
||
spf_strict: bool
|
||
dmarc_enforced: bool
|
||
spoofable: bool
|
||
|
||
class Colors:
|
||
GREEN = "\033[0;32m"
|
||
RED = "\033[0;31m"
|
||
YELLOW = "\033[1;33m"
|
||
BLUE = "\033[0;34m"
|
||
CYAN = "\033[0;36m"
|
||
NC = "\033[0m"
|
||
|
||
class EmailValidator:
|
||
def __init__(self, verbose: bool = False, stealth: bool = False, delay: int = 0):
|
||
self.verbose = verbose
|
||
self.stealth = stealth
|
||
self.delay = delay
|
||
self.console = Console()
|
||
|
||
@staticmethod
|
||
def show_banner():
|
||
banner = """
|
||
███████╗███╗ ███╗ █████╗ ██╗██╗ ██████╗ ███████╗██╗███╗ ██╗████████╗
|
||
██╔════╝████╗ ████║██╔══██╗██║██║ ██╔═══██╗██╔════╝██║████╗ ██║╚══██╔══╝
|
||
█████╗ ██╔████╔██║███████║██║██║ ██║ ██║███████╗██║██╔██╗ ██║ ██║
|
||
██╔══╝ ██║╚██╔╝██║██╔══██║██║██║ ██║ ██║╚════██║██║██║╚██╗██║ ██║
|
||
███████╗██║ ╚═╝ ██║██║ ██║██║███████╗╚██████╔╝███████║██║██║ ╚████║ ██║
|
||
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝ ╚═════╝ ╚══════╝╚═╝╚═╝ ╚═══╝ ╚═╝
|
||
"""
|
||
print(f"{Colors.CYAN}{banner}{Colors.NC}")
|
||
print(f"{Colors.CYAN} Email OSINT Validator v{VERSION}{Colors.NC}")
|
||
print(f"{Colors.BLUE} Desarrollado por: Kevin Muñoz (MrHacker|TheSL18){Colors.NC}")
|
||
print(f"{Colors.YELLOW} Uso ético - Solo para investigación autorizada{Colors.NC}\n")
|
||
|
||
async def analyze_mx_security(self, domain: str) -> SecurityAnalysis:
|
||
security_features = []
|
||
vulnerabilities = []
|
||
security_score = 0
|
||
spf_strict = False
|
||
dmarc_enforced = False
|
||
spoofable = False
|
||
|
||
try:
|
||
# Check SPF
|
||
try:
|
||
answers = dns.resolver.resolve(domain, 'TXT')
|
||
spf_record = next((str(record) for record in answers
|
||
if str(record).startswith('"v=spf1')), None)
|
||
|
||
if spf_record:
|
||
security_features.append("SPF")
|
||
security_score += 20
|
||
|
||
if '-all' in spf_record:
|
||
security_features.append("SPF_STRICT")
|
||
spf_strict = True
|
||
security_score += 15
|
||
elif '~all' in spf_record:
|
||
security_features.append("SPF_SOFT_FAIL")
|
||
vulnerabilities.append("SPF no es estricto (usa ~all)")
|
||
security_score += 10
|
||
elif '?all' in spf_record:
|
||
vulnerabilities.append("SPF en modo neutral")
|
||
security_score += 5
|
||
elif '+all' in spf_record:
|
||
vulnerabilities.append("SPF permite cualquier remitente")
|
||
spoofable = True
|
||
else:
|
||
vulnerabilities.append("No se encontró registro SPF")
|
||
spoofable = True
|
||
except Exception as e:
|
||
vulnerabilities.append(f"Error al verificar SPF: {str(e)}")
|
||
|
||
# Check DMARC
|
||
try:
|
||
answers = dns.resolver.resolve(f'_dmarc.{domain}', 'TXT')
|
||
dmarc_record = next((str(record) for record in answers
|
||
if str(record).startswith('"v=DMARC1')), None)
|
||
|
||
if dmarc_record:
|
||
security_features.append("DMARC")
|
||
security_score += 20
|
||
|
||
if 'p=reject' in dmarc_record:
|
||
security_features.append("DMARC_ENFORCED")
|
||
dmarc_enforced = True
|
||
security_score += 15
|
||
elif 'p=quarantine' in dmarc_record:
|
||
security_features.append("DMARC_QUARANTINE")
|
||
security_score += 10
|
||
elif 'p=none' in dmarc_record:
|
||
vulnerabilities.append("DMARC en modo monitoreo")
|
||
security_score += 5
|
||
else:
|
||
vulnerabilities.append("No se encontró registro DMARC")
|
||
spoofable = True
|
||
except Exception as e:
|
||
vulnerabilities.append(f"Error al verificar DMARC: {str(e)}")
|
||
|
||
# Check DKIM
|
||
dkim_selectors = ['default', 'google', 'mail', 'email', 'key1', 'selector1', 'selector2']
|
||
dkim_found = False
|
||
|
||
for selector in dkim_selectors:
|
||
try:
|
||
dns.resolver.resolve(f'{selector}._domainkey.{domain}', 'TXT')
|
||
security_features.append("DKIM")
|
||
security_score += 15
|
||
dkim_found = True
|
||
break
|
||
except:
|
||
continue
|
||
|
||
if not dkim_found:
|
||
vulnerabilities.append("No se encontró registro DKIM")
|
||
|
||
except Exception as e:
|
||
vulnerabilities.append(f"Error en análisis de seguridad: {str(e)}")
|
||
|
||
return SecurityAnalysis(
|
||
score=security_score,
|
||
features=security_features,
|
||
vulnerabilities=vulnerabilities,
|
||
spf_strict=spf_strict,
|
||
dmarc_enforced=dmarc_enforced,
|
||
spoofable=spoofable
|
||
)
|
||
|
||
def fingerprint_server(self, response: str) -> Optional[str]:
|
||
server_info = re.search(r'^220.*$', response, re.MULTILINE | re.IGNORECASE)
|
||
if not server_info:
|
||
return None
|
||
|
||
server_info = server_info.group(0)
|
||
|
||
if 'mx.google.com' in server_info.lower() or 'gmail-smtp' in server_info.lower():
|
||
return "Google Mail Infrastructure"
|
||
elif 'microsoft' in server_info.lower():
|
||
return "Microsoft Exchange/Office 365"
|
||
elif 'postfix' in server_info.lower():
|
||
return "Postfix"
|
||
elif 'exim' in server_info.lower():
|
||
return "Exim"
|
||
elif 'sendmail' in server_info.lower():
|
||
return "Sendmail"
|
||
elif 'zimbra' in server_info.lower():
|
||
return "Zimbra"
|
||
|
||
return "Unknown Server"
|
||
|
||
async def validate_email(self, email: str) -> bool:
|
||
if not re.match(r'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$', email):
|
||
rprint("[red]❌ Formato de correo inválido[/]")
|
||
return False
|
||
|
||
rprint("[green]✓ La sintaxis del correo es correcta[/]")
|
||
|
||
domain = email.split('@')[1]
|
||
|
||
try:
|
||
rprint("[cyan]🔍 Buscando registros MX...[/]")
|
||
mx_records = sorted(
|
||
[(rdata.preference, rdata.exchange.to_text().rstrip('.'))
|
||
for rdata in dns.resolver.resolve(domain, 'MX')],
|
||
key=lambda x: x[0]
|
||
)
|
||
|
||
if not mx_records:
|
||
rprint(f"[red]❌ No se encontraron registros MX para {domain}[/]")
|
||
return False
|
||
|
||
for preference, server in mx_records:
|
||
rprint(f"[green]✓ MX record encontrado: {server} (Prioridad {preference})[/]")
|
||
|
||
if self.verbose:
|
||
security_analysis = await self.analyze_mx_security(domain)
|
||
self.display_security_analysis(domain, security_analysis)
|
||
|
||
primary_mx = mx_records[0][1]
|
||
|
||
if self.stealth:
|
||
time.sleep(self.delay)
|
||
|
||
rprint(f"[cyan]⏳ Iniciando diálogo con {primary_mx}[/]")
|
||
|
||
try:
|
||
with socket.create_connection((primary_mx, 25), timeout=10) as sock:
|
||
sock.settimeout(10)
|
||
|
||
response = sock.recv(1024).decode()
|
||
if self.verbose:
|
||
server_type = self.fingerprint_server(response)
|
||
if server_type:
|
||
rprint(f"[blue]ℹ Servidor detectado: {server_type}[/]")
|
||
|
||
commands = [
|
||
f"HELO email-validator-osint.local\r\n",
|
||
f"MAIL FROM:<validator@email-validator-osint.local>\r\n",
|
||
f"RCPT TO:<{email}>\r\n",
|
||
"QUIT\r\n"
|
||
]
|
||
|
||
for cmd in commands:
|
||
sock.send(cmd.encode())
|
||
response = sock.recv(1024).decode()
|
||
|
||
if self.verbose:
|
||
rprint(f"[cyan]→ {cmd.strip()}[/]")
|
||
rprint(f"[cyan]← {response.strip()}[/]")
|
||
|
||
code = response[:3]
|
||
if code == "550":
|
||
rprint(f"[red]❌ El correo {email} no es válido[/]")
|
||
return False
|
||
elif code not in ["220", "250", "221"]:
|
||
rprint(f"[yellow]⚠ Respuesta inesperada del servidor: {response.strip()}[/]")
|
||
return False
|
||
|
||
rprint(f"[green]✓ El correo {email} es válido[/]")
|
||
return True
|
||
|
||
except Exception as e:
|
||
rprint(f"[red]❌ Error al conectar con el servidor: {str(e)}[/]")
|
||
return False
|
||
|
||
except Exception as e:
|
||
rprint(f"[red]❌ Error: {str(e)}[/]")
|
||
return False
|
||
|
||
def display_security_analysis(self, domain: str, analysis: SecurityAnalysis):
|
||
table = Table(title=f"🔒 Análisis de Seguridad para {domain}")
|
||
|
||
table.add_column("Categoría", style="cyan")
|
||
table.add_column("Detalle", style="white")
|
||
|
||
table.add_row(
|
||
"📊 Puntuación de Seguridad",
|
||
f"{analysis.score}/100"
|
||
)
|
||
|
||
if analysis.features:
|
||
table.add_row(
|
||
"✅ Características de Seguridad",
|
||
"\n".join(analysis.features)
|
||
)
|
||
|
||
if analysis.vulnerabilities:
|
||
table.add_row(
|
||
"⚠️ Vulnerabilidades",
|
||
"\n".join(analysis.vulnerabilities)
|
||
)
|
||
|
||
table.add_row(
|
||
"🎯 Estado de Protecciones",
|
||
f"SPF Estricto: {'✅' if analysis.spf_strict else '❌'}\n"
|
||
f"DMARC Enforced: {'✅' if analysis.dmarc_enforced else '❌'}\n"
|
||
f"Spoofing Posible: {'⚠️ SÍ' if analysis.spoofable else '✅ NO'}"
|
||
)
|
||
|
||
self.console.print(table)
|
||
|
||
async def main():
|
||
parser = argparse.ArgumentParser(description='Email OSINT Validator')
|
||
parser.add_argument('email', help='Email to validate')
|
||
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose mode')
|
||
parser.add_argument('-s', '--stealth', action='store_true', help='Stealth mode')
|
||
parser.add_argument('-d', '--delay', type=int, default=0, help='Delay between queries')
|
||
args = parser.parse_args()
|
||
|
||
validator = EmailValidator(verbose=args.verbose, stealth=args.stealth, delay=args.delay)
|
||
validator.show_banner()
|
||
|
||
result = await validator.validate_email(args.email)
|
||
sys.exit(0 if result else 1)
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|