-
Davide Vaghetti authoredDavide Vaghetti authored
createMails.py 13.57 KiB
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# Reaction Mailcreate
# Copyright (C) 2020 Tobias Dussa <tobias-reaction-mailcreate@dussa.de>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from email.message import EmailMessage
from email.utils import formatdate
import argparse
import csv
import datetime
import getpass
import hashlib
import jinja2
import os
import requests
import secrets
import smtplib
import sys
parser = argparse.ArgumentParser()
if sys.version_info < (3, 8):
class ExtendAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
items = getattr(namespace, self.dest) or []
items.extend(values)
setattr(namespace, self.dest, items)
parser.register('action', 'extend', ExtendAction)
# Parse args
parser.add_argument('-b', '--basedir', dest='basedir', default='Mails', help='base directory for all output (default: "Mails")')
parser.add_argument('-B', '--bcc', dest='bcc', default=[], nargs=1, action='extend', help='additional mail recipient to bcc (default: None)')
parser.add_argument('-c', '--campaign', dest='campaign', default='Test', help='campaign name (default: "Test")')
parser.add_argument('-d', '--dry-run', dest='dryrun', default=False, action='store_true', help='dry run -- do not actually send mails or create targets (default: False)')
parser.add_argument('-f', '--from', dest='sender', default='Nobody <nobody@example.com>', help='sender mail address (default: "Nobody <nobody@example.com>"; implies dry-run if not set)')
parser.add_argument('-F', '--force', dest='force', default=False, action='store_true', help='force insecure login without TLS/SSL (default: False)')
parser.add_argument('-H', '--hashstring', dest='hashstring', default='{salt}{campaign}{infix}-{site}', help='string to be hashed for the URL (default: "{salt}{campaign}{infix}-{site}" where "{salt}" is a random string)')
parser.add_argument('-i', '--input', dest='input', default='{basedir}/{campaign}/Input{infix}.lst', help='input file (default: "{basedir}/{campaign}/Input{infix}.lst")')
parser.add_argument('-o', '--output', dest='output', default='{basedir}/{campaign}/{site}/Mail{infix}.txt', help='output file name template (default: "{basedir}/{campaign}/{site}/Mail{infix}.txt")')
parser.add_argument( '--salt', dest='salt', default=None, help='salt to use for hashing (default: random 8-byte hex string)')
parser.add_argument('-R', '--reply-to', dest='replyto', default=None, help='reply-to mail address (default: None)')
parser.add_argument('-s', '--subject', dest='subject', default='Security Challenge for {site} -- {campaign}{infix}', help='mail subject (default: "Security Challenge Message -- {campaign}{infix}")')
parser.add_argument('-S', '--smtpserver', dest='smtpserver', default='localhost', help='SMTP server to use (default: "localhost"); port can be specified with "<host>:<port>" notation and takes precedence over implied ports and port specification')
parser.add_argument( '--smtpport', dest='smtpport', default=0, type=int, help='SMTP port to use (default: 25); takes precedence over implied ports')
parser.add_argument( '--smtpuser', dest='smtpuser', default=None, help='SMTP user to login with (default: none); implies TLS (port 465) unless STARTTLS is set as well')
parser.add_argument( '--smtppass', dest='smtppass', default=None, help='SMTP password to login with (default: none); implies TLS (port 465) unless STARTTLS is set as well; will be queried interactively if set to "-"')
parser.add_argument( '--starttls', dest='starttls', default=False, action='store_true', help='login using STARTTLS (default: False); implies port 587')
parser.add_argument('-t', '--template', dest='template', default='{basedir}/{campaign}/Mail.template', help='mail template file (default: "{basedir}/{campaign}/Mail.template")')
parser.add_argument('-T', '--to', dest='to', default='{firstname} {lastname} <{email}>', help='recipient mail address (default: "{firstname} {lastname} <{email}>")')
parser.add_argument('-u', '--url', dest='url', default='{webserver}/{campaign}{infix}-{hash}', help='URL template to use (default: "{webserver}/{campaign}{infix}-{hash}"')
parser.add_argument('-v', '--verbose', dest='verbose', default=False, action='store_true', help='increase verbosity')
parser.add_argument('-w', '--webserver', dest='webserver', default='https://challenge.example.com', help='web server to use (default: "https://challenge.example.com"; implies dry-run if not set')
parser.add_argument('infix', default='', nargs='?', help='infix for ID purposes, default empty')
args = parser.parse_args()
class SafeDict(dict):
def __missing__(self, key):
return '{' + key + '}'
if (args.sender == 'Nobody <nobody@example.com>') or \
(args.webserver == 'https://challenge.example.com'):
args.dryrun = True
if (len(args.smtpserver.split(':')) == 2) or \
(']:' in args.smtpserver):
args.smtpport = args.smtpserver.rsplit(':', 1)[1]
args.smtpserver = args.smtpserver.rsplit(':', 1)[0]
if (args.smtpport == 0):
if args.starttls:
args.smtpport = 587
elif args.smtpuser or args.smtppass:
args.smtpport = 465
else:
args.smtpport = 25
if args.smtpuser and not args.smtppass:
args.smtppass = ''
if args.smtppass and not args.smtpuser:
args.smtpuser = ''
if args.smtppass == '-':
args.smtppass = getpass.getpass('SMTP authentication password: ')
if args.dryrun:
print('DRY RUN. Not actually sending e-mails or creating targets, just creating mail files and URLs.')
if args.force:
print('FORCE INSECURE LOGIN. Will send authentication data even if no secure connection can be established.')
if args.starttls:
print('STARTTLS enabled.')
elif args.verbose:
print('STARTTLS disabled.')
# Generate salt to use for secure hashing if unset
if not args.salt:
args.salt = secrets.token_hex(8)
if args.verbose:
print(f'Using "{args.basedir}" as base directory.')
if args.bcc:
print(f'Using "{", ".join(args.bcc)}" as silent mail recipients.')
print(f'Using "{args.campaign}" as campaign.')
print(f'Using "{args.sender}" as sender mail address.')
if args.replyto:
print(f'Using "{args.replyto}" as reply-to mail address.')
print(f'Using "{args.hashstring.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver))}" as hash string.')
print(f'Using "{args.input.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt))}" as input file.')
print(f'Using "{args.output.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt))}" as output file name template.')
print(f'Using "{args.salt.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt))}" as salt.')
print(f'Using "{args.subject.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt))}" as mail subject.')
print(f'Using "{args.template.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt))}" as template file.')
print(f'Using "{args.to.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt))}" as recipient mail address.')
print(f'Using "{args.url.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt))}" as URL template.')
print(f'Using "{args.webserver}" as web server.')
print(f'Using "{args.infix}" as infix.')
print(f'Using "{args.smtpserver}" as SMTP server.')
print(f'Using {str(args.smtpport)} as SMTP port.')
if args.smtpuser:
print(f'Using "{args.smtpuser}" as user for SMTP authentication.')
if args.smtppass:
print('Using "{args.smtppass}" as password for SMTP authentication.')
if (args.smtpuser or args.smtppass) and \
(args.smtpport == 25) and \
not args.starttls and \
not args.force:
print('SMTP authentication specified, but port 25 used (without STARTTLS). Bailing out.')
sys.exit(-1)
elif args.force:
print('WARNING: SMTP authentication specified, but port 25 used (without STARTTLS)!')
data = dict()
template = jinja2.Template(open(args.template.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt))).read())
def toHex(serial):
tmpString = hex(serial)[2:].upper()
if ((len(tmpString) % 2) == 1):
tmpString = '0' + tmpString
return ':'.join([tmpString[i:i+2] for i in range(0, len(tmpString), 2)])
def createMail(data):
output = args.output.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt, site=data['site'], firstname=data['firstname'], lastname=data['lastname'], email=data['email'], hash=data['hash'], URL=data['URL']))
directory = os.path.dirname(output)
print(f'Writing mail to {output}')
if not os.path.exists(directory):
os.makedirs(directory)
with open(output, 'wb') as mailfile:
template.stream(data).dump(mailfile, encoding='utf-8')
message = EmailMessage()
with open(output, 'r') as mailfile:
message.set_content(mailfile.read())
message['Date'] = formatdate(localtime=True)
message['Subject'] = args.subject.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt, site=data['site'], firstname=data['firstname'], lastname=data['lastname'], email=data['email'], hash=data['hash'], URL=data['URL']))
message['From'] = args.sender.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt, site=data['site'], firstname=data['firstname'], lastname=data['lastname'], email=data['email'], hash=data['hash'], URL=data['URL']))
message['Reply-To'] = args.replyto.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt, site=data['site'], firstname=data['firstname'], lastname=data['lastname'], email=data['email'], hash=data['hash'], URL=data['URL']))
message['To'] = args.to.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt, site=data['site'], firstname=data['firstname'], lastname=data['lastname'], email=data['email'], hash=data['hash'], URL=data['URL']))
message['Bcc'] = ', '.join(args.bcc).format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt, site=data['site'], firstname=data['firstname'], lastname=data['lastname'], email=data['email'], hash=data['hash'], URL=data['URL']))
if not args.dryrun:
if message['Bcc']:
print(f'Sending mail to {message["To"]} (and {message["Bcc"]})')
else:
print(f'Sending mail to {message["To"]}')
if args.starttls:
with smtplib.SMTP(args.smtpserver, args.smtpport) as smtp:
smtp.starttls()
smtp.login(args.smtpuser, args.smtppass)
smtp.send_message(message)
elif args.smtpuser or args.smtppass:
with smtplib.SMTP_SSL(args.smtpserver, args.smtpport) as smtp:
smtp.login(args.smtpuser, args.smtppass)
smtp.send_message(message)
else:
with smtplib.SMTP(args.smtpserver, args.smtpport) as smtp:
smtp.send_message(message)
print(f'Creating target at {data["URL"]}/create')
requests.get(f'{data["URL"]}/create')
def generateHashAndURL(data):
data['hash'] = hashlib.sha256(args.hashstring.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt, site=data['site'], firstname=data['firstname'], lastname=data['lastname'], email=data['email'])).encode('utf-8')).hexdigest()
data['URL'] = args.url.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt, site=data['site'], firstname=data['firstname'], lastname=data['lastname'], email=data['email'], hash=data['hash']))
with open(args.input.format_map(SafeDict(basedir=args.basedir, campaign=args.campaign, infix=args.infix, webserver=args.webserver, salt=args.salt)), 'r') as inputfile:
data = csv.DictReader(inputfile, fieldnames=['site', 'firstname', 'lastname', 'email'], delimiter=',')
for row in data:
if (row['site'] != 'site') or \
(row['firstname'] != 'firstname') or \
(row['lastname'] != 'lastname') or \
(row['email'] != 'email'):
generateHashAndURL(row)
createMail(row)