diff --git a/reaction-mailcreate/createMails.py b/reaction-mailcreate/createMails.py index c9358e1e04bce0d3e736e19c40d3e8fab840f75e..966ab9f1f4cf5db9d16b93e5e3453cb296131dba 100755 --- a/reaction-mailcreate/createMails.py +++ b/reaction-mailcreate/createMails.py @@ -28,10 +28,7 @@ import getpass import hashlib import jinja2 import os -import magic -import requests import secrets -import smtplib import sys parser = argparse.ArgumentParser() @@ -58,13 +55,13 @@ parser.add_argument('-i', '--input', dest='input', default='{basedir}/ parser.add_argument('-o', '--output', dest='output', default='{basedir}/{campaign}/{site}/Mail{infix}.eml', help='output file name template (default: "{basedir}/{campaign}/{site}/Mail{infix}.eml")') parser.add_argument('-R', '--reply-to', dest='replyto', default=None, help='reply-to mail address (default: None)') parser.add_argument( '--salt', dest='salt', default=None, help='salt to use for hashing (default: random 8-byte hex string)') -parser.add_argument( '--sign', dest='sign', default=None, choices=[None, 'gpg', 'smime'], help='signature method (one of "", "gpg", "smime"; default: "")') -parser.add_argument( '--sign-as', dest='sign-as', default=None, help='signature key to use (default: autoselect)') +parser.add_argument( '--sign', dest='sign', default='', choices=['', 'gpg', 'gpgsm', 'openssl'], help='signature method (one of "", "gpg", "gpgsm", "openssl"; default: "")') +parser.add_argument( '--sign-as', dest='signas', default=None, help='signature key to use (default: None meaning autoselect)') 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( '--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}>")') @@ -83,6 +80,34 @@ if (args.sender == 'Nobody <nobody@example.com>') or \ (args.webserver == 'https://challenge.example.com'): args.dryrun = True + +# Import further necessary dependencies +# Import dependencies for actual SMTP interaction if necessary +if not args.dryrun: + import smtplib + + # Import dependencies for HTTTP interaction if necessary + if not args.webserver: + import requests + +# Import dependencies for attachment file-magic if necessary +if args.attach: + import magic + +# Import dependencies for mail signing if necessary +if args.sign: + import magic + + match args.sign: + case 'gpg' | 'gpgsm': + # Import dependencies for GPG-based mail signing if necessary + from email.mime.multipart import MIMEMultipart + import gnupg + case 'openssl': + # Import dependencies for OpenSSL-based mail signing if necessary + from M2Crypto import BIO, Rand, SMIME + + if (len(args.smtpserver.split(':')) == 2) or \ (']:' in args.smtpserver): args.smtpport = args.smtpserver.rsplit(':', 1)[1] @@ -132,6 +157,14 @@ if args.verbose: 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.') + if args.sign: + print(f'Using "{args.sign}" as signature method.') + if args.signas: + print(f'Using "{args.signas}" as signing key.') + else: + print(f'Using auto-selected signing key.') + else: + print(f'Using no signature method.') 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.') @@ -160,6 +193,7 @@ 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()) +# Convert int to hex def toHex(serial): tmpString = hex(serial)[2:].upper() if ((len(tmpString) % 2) == 1): @@ -167,6 +201,51 @@ def toHex(serial): return ':'.join([tmpString[i:i+2] for i in range(0, len(tmpString), 2)]) +# Sign message with GPG/GPGSM +def signMailGPG(binary, message): + # Set up GPG context + gpg = gnupg.GPG(gpgbinary=binary) + + # Sign mail + try: + signature = str(gpg.sign(message.as_string().replace('\n', '\r\n'), detach=True)) + except: + print('ERROR signing mail!') + + # Assemble signature message + signatureMessage = EmailMessage() + signatureMessage['Content-Type'] = 'application/pgp-signature; name="signature.asc"' + signatureMessage['Content-Description'] = 'OpenPGP digital signature' + signatureMessage.set_payload(signature) + + # Assemble new message + newMessage = MIMEMultipart(_subtype="signed", protocol="application/pgp-signature") + newMessage.attach(message) + newMessage.attach(signatureMessage) + + return newMessage + + +# Sign message with OpenSSL +def signMailOpenSSL(message): + return message + + +# Sign message if requested +def signMail(message): + if not args.sign: + return message + + match args.sign: + case 'gpg': + return signMailGPG('gpg', message) + case 'gpgsm': + return signMailGPG('gpgsm', message) + case 'openssl': + return signMailOpenSSL(message) + + +# Assemble and send the actual mail 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) @@ -178,6 +257,9 @@ def createMail(data): with open(attach, 'rb') as attachment: message.add_attachment(attachment.read(), *magic.from_file(attach, mime=True).split('/')) + # Sign mail if requested + message = signMail(message) + 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'])) @@ -225,12 +307,14 @@ def createMail(data): requests.get(f'{data["CreateURL"]}') +# Generate hash, URL, and CreateURL 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'])) data['CreateURL'] = args.createurl.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'])) +# Main loop 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: