Files
ctf/2025/lake/web/web-magicauth/mta/server.py
2025-11-29 14:49:20 +01:00

122 lines
3.8 KiB
Python

import asyncio
import logging
from email import message_from_bytes
from aiosmtpd.controller import Controller
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
LISTEN_HOST = "0.0.0.0"
LISTEN_PORT = 25
FORWARD_HOST = "smtp2http"
FORWARD_PORT = 25
class MTAHandler:
async def handle_DATA(self, server, session, envelope):
try:
peer = session.peer
client_ip = peer[0] if peer else "unknown"
logger.info(f"Received email from {client_ip}: {envelope.mail_from} -> {envelope.rcpt_tos}")
message_data = envelope.content
msg = message_from_bytes(message_data)
# Security: Remove any existing Received headers to prevent spoofing
# Only the last MTA's Received header should be trusted
if 'Received' in msg:
del msg['Received']
msg['Received'] = client_ip
await self.forward_email(
msg,
envelope.mail_from,
envelope.rcpt_tos
)
return '250 OK: Message accepted for delivery'
except Exception as e:
logger.error(f"Error processing email: {e}", exc_info=True)
return '451 Requested action aborted: error in processing'
async def forward_email(self, msg, mail_from, rcpt_tos):
try:
reader, writer = await asyncio.open_connection(FORWARD_HOST, FORWARD_PORT)
# Read banner
banner = await reader.readline()
logger.debug(f"smtp2http banner: {banner.decode().strip()}")
# Send EHLO
writer.write(b"EHLO mta\r\n")
await writer.drain()
response = await reader.readline()
logger.debug(f"EHLO response: {response.decode().strip()}")
# Send MAIL FROM
writer.write(f"MAIL FROM:<{mail_from}>\r\n".encode())
await writer.drain()
response = await reader.readline()
logger.debug(f"MAIL FROM response: {response.decode().strip()}")
# Send RCPT TO for each recipient
for recipient in rcpt_tos:
writer.write(f"RCPT TO:<{recipient}>\r\n".encode())
await writer.drain()
response = await reader.readline()
logger.debug(f"RCPT TO response: {response.decode().strip()}")
# Send DATA command
writer.write(b"DATA\r\n")
await writer.drain()
response = await reader.readline()
logger.debug(f"DATA response: {response.decode().strip()}")
email_content = msg.as_string().encode('utf-8')
writer.write(email_content)
if not email_content.endswith(b"\r\n"):
writer.write(b"\r\n")
writer.write(b".\r\n")
await writer.drain()
response = await reader.readline()
logger.debug(f"Message response: {response.decode().strip()}")
# Send QUIT
writer.write(b"QUIT\r\n")
await writer.drain()
response = await reader.readline()
logger.debug(f"QUIT response: {response.decode().strip()}")
writer.close()
await writer.wait_closed()
logger.info(f"Successfully forwarded email from {mail_from} to {rcpt_tos}")
except Exception as e:
logger.error(f"Failed to forward email: {e}", exc_info=True)
async def main():
handler = MTAHandler()
controller = Controller(
handler,
hostname=LISTEN_HOST,
port=LISTEN_PORT
)
controller.start()
logger.info(f"MTA listening on {LISTEN_HOST}:{LISTEN_PORT}")
try:
await asyncio.Event().wait()
finally:
controller.stop()
if __name__ == "__main__":
asyncio.run(main())