Skip to content

Commit

Permalink
Merge branch 'main' of github.com:sensepost/mail-in-the-middle
Browse files Browse the repository at this point in the history
  • Loading branch information
felmoltor committed Sep 18, 2024
2 parents 7c983f9 + 8584030 commit eb57cbf
Show file tree
Hide file tree
Showing 16 changed files with 2,572 additions and 348 deletions.
11 changes: 9 additions & 2 deletions Maitm/MailManager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# General imports
import yaml
import json
from enum import Enum
import logging.config
import os
Expand Down Expand Up @@ -213,12 +214,18 @@ def fetch_emails(self, criteria, number: int = 10, offset: int = 0):
self.logger.error("Unsupported protocol for reading emails")
return None

def _json_serializer_with_datetime(self, obj):
if isinstance(obj, datetime):
return obj.isoformat() # Converts datetime to a string in ISO 8601 format
raise TypeError(f"Type {type(obj)} not serializable")

"""
Fetches emails using the exchangelib account object
"""
def _fetch_emails_exchangelib(self, criteria: dict, number: int = 10):
# date_limit: datetime = None, domain_to: str = None, domain_from: str = None, subject: str = None, ignore_seen: bool=True
self.logger.info("[Exchangelib] Fetching %s emails with criteria: %s" % (number, criteria))

self.logger.info("[Exchangelib] Fetching %s emails with criteria:\n%s" % (number, json.dumps(criteria, indent=1, default=self._json_serializer_with_datetime)))

query = None
if criteria["ignore_seen"]:
Expand Down Expand Up @@ -250,7 +257,7 @@ def _fetch_emails_exchangelib(self, criteria: dict, number: int = 10):
Fetches emails using the IMAP protocol
"""
def _fetch_emails_imap(self, criteria: dict, number: int = 10):
self.logger.info("[IMAP] Fetching %s emails with filters: %s" % (number, criteria))
self.logger.info("[IMAP] Fetching %s emails with criteria:\n%s" % (number, json.dumps(criteria, indent=1, default=self._json_serializer_with_datetime)))

# Building the search criteria
imap_criteria = {}
Expand Down
73 changes: 49 additions & 24 deletions Maitm/Maitm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
from imap_tools import MailBox, AND
# from imap_tools.message import MailMessage
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import os, sys, time, re
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from smtplib import SMTP
# from email.message import EmailMessage
from email.message import EmailMessage as PythonEmailMessage
Expand All @@ -23,22 +21,30 @@
from email.utils import parseaddr, getaddresses
import quopri
import copy
from threading import Event as ThreadingEvent

class Maitm():
def __init__(self,config_file=None,only_new=True,forward_emails=False,logfile="logs/maitm.log",level=logging.INFO) -> None:
def __init__(self,
config_file=None,
only_new=True,
forward_emails=False,
logfile="logs/maitm.log",
level=logging.INFO,
stop_event: ThreadingEvent = None) -> None:
self.config = {}
self.bells = []
self.typos = {}
self.smtp_connection = None
self.only_new = only_new
self.forward_emails = forward_emails
self.logfile=logfile
self.threading_stop_event = stop_event

# Initialization
if (os.path.exists(config_file)):
self.read_config(config_file=config_file)

self.mailmanager=MailManager(auth_config=self.config['auth'],logfile=datetime.now().strftime("logs/%Y%m%d_%H%M%S_mailmanager.log"))
self.mailmanager=MailManager(auth_config=self.config['auth'], logfile=self.logfile) # datetime.now().strftime("logs/%Y%m%d_%H%M%S_mailmanager.log"))
# set the date limit to search emails
self.date_limit = self.config["filter"]["date_limit"] if "date_limit" in self.config["filter"].keys() else None
# Set the ignore_seen flag to ignore emails that have been already read by this script
Expand Down Expand Up @@ -147,18 +153,26 @@ def countdown(self,seconds):
sys.stdout.write("{:2d}s".format(remaining))
sys.stdout.flush()
time.sleep(1)
# Check the stop_thread to cancel ahead of time
if self.threading_stop_event is not None and self.threading_stop_event.is_set():
self.logger.info("Maitm has been stopped - Stopping countdown.")
break
sys.stdout.write("\n")

"""
If the UID of this email has not been previously forwarded, it is consired new email
"""
def is_new_email(self,msg_uid: str):
# Check if the file exists, if not, create it
if not os.path.exists("forwardedemails.txt"): open("forwardedemails.txt", "w").close()
uids=[uid.strip() for uid in open("forwardedemails.txt","r").readlines()]
return msg_uid not in uids
"""
Save this UID as observed and forwarded
"""
def flag_observed(self,msg_uid: str):
# Check if the file exists, if not, create it
if not os.path.exists("forwardedemails.txt"): open("forwardedemails.txt", "w").close()
with open("forwardedemails.txt","a") as fe:
fe.write(msg_uid+"\n")

Expand Down Expand Up @@ -351,8 +365,7 @@ def inject_attachment_message_plain(self,content: str,charset=None):
content=self.config["injections"]["attachments"]["attachment_message"]+"\n\n"+content
else:
self.logger.debug("No attachment message to inject in the email.")

return content
return content


"""
Expand Down Expand Up @@ -760,38 +773,42 @@ def taint_html_part(self, part, id):
# I get \xa0 characters when using beautiful soup and there are   characters in the original html so we need to fix that thing manually :-(
# Due to this annoying behaviour, I need to manually replace all   html entities from the original HTML.
# What annoys me most is that this is not happening with other HTML entities, such as < or >
target_html=target_html.replace(' ',' ')
target_html=target_html.replace('\xa0',' ')
target_html=target_html.replace(' ',' ').replace('\xa0',' ')
tainted_html_bytes=target_html

# Insert the tracking pixel
tainted_html_bytes = target_html_bytes
if (self.tracking_url is not None):
if (self.tracking_url is not None and len(self.tracking_url)>0):
tainted_html_bytes=self.insert_tracking_pixel_html(id,target_html_bytes,charset=charset)

# Insert the UNC path
if (self.unc_path is not None):
if (self.unc_path is not None and len(self.unc_path)>0):
tainted_html_bytes=self.insert_unc_path_html(id,tainted_html_bytes,charset=charset)

# Modify the links
if (self.links is not None):
if (self.links is not None and len(self.links)>0):
tainted_html_bytes=self.replace_links_html(id,tainted_html_bytes, charset=charset)

# Inject the attachment message if defined
# We do this before the file attachement itself because I don't like to manage the content of the object EmailMessage
if (self.attachment_message):
if (self.attachment_message is not None and len(self.attachment_message)>0):
tainted_html_bytes=self.inject_attachment_message_html(tainted_html_bytes,charset=charset)
# Setting the new HTML payload with the tainted content

# Setting the new HTML payload with the tainted content
# The email can contain unicode characters, so we need to convert them to an adequate ascii encoding supported by smtplib and exchangelib
# They will send weird characters if we don't do this
encoded_payload,transfer_encoding = self.prepare_payload_for_email(tainted_html_bytes, content_type=content_type, charset=charset)
self.logger.debug("Charset before setting payload: %s" % charset)
self.logger.debug("Encoding before setting payload: %s" % part.get('Content-Transfer-Encoding'))
# self.logger.debug("Charset before setting payload: %s" % charset)
# self.logger.debug("Encoding before setting payload: %s" % part.get('Content-Transfer-Encoding'))

# Change the payload encoding header to match the variable 'encoding'
if (part.get('Content-Transfer-Encoding') is not None):
part.replace_header('Content-Transfer-Encoding', transfer_encoding)
else:
part.add_header('Content-Transfer-Encoding', transfer_encoding)
part.set_payload(encoded_payload)
self.logger.debug("Charset after setting payload: %s" % part.get_content_charset())
self.logger.debug("Encoding after setting payload: %s" % part.get('Content-Transfer-Encoding'))
# self.logger.debug("Charset after setting payload: %s" % part.get_content_charset())
# self.logger.debug("Encoding after setting payload: %s" % part.get('Content-Transfer-Encoding'))

"""
Taint the plain text part of the email
Expand Down Expand Up @@ -882,11 +899,11 @@ def forward_message(self,msg: PythonEmailMessage):
try:
# fake_msg.replace_header("Subject",msg["subject"].replace("\n","").replace("\r",""))
# Decide what sender we are going to be
if (self.spoof_sender):
if (self.spoof_sender and len(msg["from"])>0):
fake_msg.replace_header("From",msg["from"]) # .name+" <"+msg.from_values.email+">"
elif(self.fixed_sender is not None):
elif(self.fixed_sender is not None and len(self.fixed_sender)>0):
fake_msg.replace_header("From",self.fixed_sender)
elif(self.authenticated_username is not None):
elif(self.authenticated_username is not None and len(self.authenticated_username)>0):
fake_msg.replace_header("From",self.authenticated_username)
else:
fake_msg.replace_header("From","Max Headroom <[email protected]>")
Expand Down Expand Up @@ -1038,14 +1055,22 @@ def search_with_criteria(criteria: dict):
else:
self.logger.info("No more emails to fetch. Stopping the search.")
break


# If self.date_limit is naive, convert it to timezone-aware (e.g., UTC)
if self.date_limit.tzinfo is None:
self.date_limit = self.date_limit.replace(tzinfo=timezone.utc)

############################
# Never stop or stop monitoring if there's a date_limit defined and is still ahead of now
############################
while (1):
if (self.date_limit is not None and datetime.now() < self.date_limit.replace(tzinfo=None)):
# Pay attention to the threading event 'stop_event' in case that it has been defined
if self.threading_stop_event is not None and self.threading_stop_event.is_set():
self.logger.info("Maitm has been stopped.")
break

# Filter by date
if (self.date_limit is not None and datetime.now(timezone.utc) < self.date_limit): # .replace(tzinfo=None)):
self.logger.info("Date limit is in the future. Stopping the monitoring")
break
else:
Expand Down Expand Up @@ -1075,4 +1100,4 @@ def search_with_criteria(criteria: dict):
self.countdown(self.poll_interval)

# Date limit hit
self.logger.info("Date limit reached. Stopping the monitoring")
self.logger.info("Email monitoring finished.")
4 changes: 4 additions & 0 deletions Maitm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .Maitm import Maitm
from .Bells import *
from .MailConverter import *
from .MailManager import *
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pyyaml = "*"
beautifulsoup4 = "*"
discord-webhook = "*"
exchangelib = "*"
textual = "*"

[dev-packages]
ipython = "*"
Expand Down
Loading

0 comments on commit eb57cbf

Please sign in to comment.