├── spoofer.PNG ├── requirements.txt ├── install.sh ├── JSON_Config └── example.json ├── LICENSE ├── README.md └── spoofer.py /spoofer.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsecure-labs/Sp00fer/HEAD/spoofer.PNG -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama 2 | emailprotectionslib 3 | dnslib 4 | tldextract 5 | argparse 6 | prettytable 7 | future 8 | scapy 9 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | apt install python3 4 | apt install python3-pip 5 | pip3 install git+https://github.com/lunarca/pyemailprotectionslib.git 6 | pip3 install -r requirements.txt 7 | -------------------------------------------------------------------------------- /JSON_Config/example.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "scenario_no": "1", 3 | "comment": "Test number 1 description", 4 | "mailfrom": "CLIENTEMAIL", 5 | "headerfrom": "CLIENTEMAIL", 6 | "to": "CLIENTEMAIL", 7 | "subject": "Test number 1", 8 | "body": "This is a test e-mail message.\n\nPlease forward it to Pentester@[yourdomain] \n\nThank you,\nTest", 9 | "server": "SERVERIP" 10 | }, 11 | { 12 | "scenario_no": "2", 13 | "comment": "Test number 2 description", 14 | "mailfrom": "TESTERDOMAIN", 15 | "headerfrom": "TESTERDOMAIN", 16 | "to": "TESTERDOMAIN", 17 | "subject": "Test number 2", 18 | "body": "This is a test e-mail message.\n\nPlease forward it to Pentester@[yourdomain] \n\nThank you,\nTest", 19 | "server": "SERVERIP" 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 QSecure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sp00fer 2 | 3 | ![alt text](https://github.com/qsecure-labs/Sp00fer/blob/master/spoofer.PNG) 4 | 5 | Sp00fer is a tool for mail server testing (e.g. for open mail relays etc.) and for spoofing checks on specified domains. 6 | 7 | ## Usage (Python3 required): 8 | 9 | ### Linux: 10 | 11 | `git clone https://github.com/qsecure-labs/Sp00fer.git` 12 | 13 | `chmod +x install.sh` 14 | 15 | `./install.sh` 16 | 17 | `python3 spoofer.py -h` 18 | 19 | ### Windows (For windows the pcap argument which saves the traffic is not implemented): 20 | 21 | `git clone https://github.com/qsecure-labs/Sp00fer.git` 22 | 23 | `pip3 install -r requirements.txt` 24 | 25 | `python3 spoofer.py -h` 26 | 27 | ## JSON file structure 28 | 29 | A JSON file is used as a template for each scenario you want to sent. The reserved words which change depending on what you choose in the arguments are: 30 | 31 | - **CLIENTEMAIL** which is replaced by the value of the `--email` argument 32 | - **CLIENTDOMAIN** which is replaced by the value of the `--domain` argument 33 | - **CLIENTNAME** which is derived by the value of the `--email` argument's local part (e.g. info@client.com will become "info") 34 | - **TESTERDOMAIN** which is replaced by the value of the `--tester` argument 35 | - **SERVERIP** which is replaced by the value of the `--server` argument 36 | 37 | Example of the JSON is: 38 | 39 | ```json 40 | [{ 41 | "scenario_no": "1", 42 | "comment": "Test number 1 description", 43 | "mailfrom": "CLIENTEMAIL", 44 | "headerfrom": "CLIENTEMAIL", 45 | "to": "CLIENTEMAIL", 46 | "subject": "Test number 1", 47 | "body": "This is a test e-mail message.\n\nPlease forward it to Pentester@[yourdomain] \n\nThank you,\nTest", 48 | "server": "SERVERIP" 49 | }, 50 | { 51 | "scenario_no": "2", 52 | "comment": "Test number 2 description", 53 | "mailfrom": "TESTERDOMAIN", 54 | "headerfrom": "TESTERDOMAIN", 55 | "to": "TESTERDOMAIN", 56 | "subject": "Test number 2", 57 | "body": "This is a test e-mail message.\n\nPlease forward it to Pentester@[yourdomain] \n\nThank you,\nTest", 58 | "server": "SERVERIP" 59 | }] 60 | ``` 61 | 62 | ## Disclaimer 63 | Sp00fer comes without warranty and is meant to be used by penetration testers during approved penetration testing assessments and/or social enigneering assessments. Sp00fer's developers and QSecure decline all responsibility in case the tool is used for malicious purposes or in any illegal context. 64 | -------------------------------------------------------------------------------- /spoofer.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import emailprotectionslib.spf as spf 3 | import emailprotectionslib.dmarc as dmarc 4 | from colorama import Fore, Back, Style 5 | from prettytable import PrettyTable 6 | import argparse 7 | import json 8 | import time 9 | from smtplib import SMTPException 10 | from smtplib import SMTPResponseException 11 | import smtplib 12 | from scapy.all import * 13 | import os 14 | import random 15 | import string 16 | 17 | 18 | # 19 | # Current Verion: 1.3 20 | # Date Modified: 29/06/2022 21 | # Changes Performed: Added the Message-ID header in the email MIME headers. 22 | # The capture file gets delete if it already exists. 23 | # 24 | # 25 | # Current Verion: 1.2 26 | # Date Modified: 31/05/2022 27 | # Changes Performed: Changed ehlo to helo because smtplib.sendmail() appended the massage size to the mail from: address 28 | # If ehlo was used (ESMTP), it caused DMARC and SPF problems, especially with Sophos antispam. 29 | # Added the MailFrom and HeaderFrom fields in the results report. 30 | # 31 | # 32 | # Current Verion: 1.1 33 | # Date Modified: 19/05/2022 34 | # Changes Performed: Reply-To and Return-Path headers were not included in the emails sent. Fixed it 35 | # CLIENTEMAIL and CLIENTNAME were not replaced if found in replyTo or returnPath variables. Fixed it. 36 | # 37 | 38 | # colours declarations 39 | def prRed(skk): print("\033[91m {}\033[00m" .format(skk)) 40 | def prGreen(skk): print("\033[92m {}\033[00m" .format(skk)) 41 | def prYellow(skk): print("\033[93m {}\033[00m" .format(skk)) 42 | def prLightPurple(skk): print("\033[94m {}\033[00m" .format(skk)) 43 | def prPurple(skk): print("\033[95m {}\033[00m" .format(skk)) 44 | def prCyan(skk): print("\033[96m {}\033[00m" .format(skk)) 45 | def prLightGray(skk): print("\033[97m {}\033[00m" .format(skk)) 46 | def prBlack(skk): print("\033[98m {}\033[00m" .format(skk)) 47 | 48 | 49 | 50 | lightblue = "\033[1;36m" 51 | blue = "\033[1;34m" 52 | normal = "\033[0;00m" 53 | red = "\033[1;31m" 54 | white = "\033[1;37m" 55 | green = "\033[1;32m" 56 | BOLD = '\033[1m' 57 | yellow = '\033[93m' 58 | 59 | if os.name == 'nt': 60 | print(""" ________________ 61 | /.,------------,.\\ 62 | /// .=^^^^^^^\__|\\\\ _______ _______ _____ 63 | \\\ `------. .//______ \ _ \ \ _ \_/ ____\___________ 64 | `\\`--...._ `; //' \\____ \/ /_\ \/ /_\ \ __\/ __ \_ __ \\ 65 | `\\.-,___;. //' | |_> > \_/ \ \_/ \ | \ ___/| | \/ 66 | `\\-..- //' | __/ \_____ /\_____ /__| \___ >__| 67 | `\\ //' |__| \/ \/ \/ 68 | \"\" """) 69 | 70 | print("\n") 71 | 72 | else: 73 | prRed(""" ________________ 74 | /.,------------,.\\ 75 | /// .=^^^^^^^\__|\\\\ _______ _______ _____ 76 | \\\ `------. .//______ \ _ \ \ _ \_/ ____\___________ 77 | `\\`--...._ `; //' \\____ \/ /_\ \/ /_\ \ __\/ __ \_ __ \\ 78 | `\\.-,___;. //' | |_> > \_/ \ \_/ \ | \ ___/| | \/ 79 | `\\-..- //' | __/ \_____ /\_____ /__| \___ >__| 80 | `\\ //' |__| \/ \/ \/ 81 | \"\" """) 82 | 83 | print("\n") 84 | 85 | 86 | # Supported aguments declaration 87 | parser = argparse.ArgumentParser() 88 | if os.name == 'nt': 89 | requiredNamed = parser.add_argument_group('Required arguments') 90 | else: 91 | requiredNamed = parser.add_argument_group( 92 | red + 'Required arguments' + normal) 93 | 94 | requiredNamed.add_argument('-d', '--domain', action='store', 95 | help='Domain to be tested. If only this argument is set, the tool will show the SPF and DMARC record for that domain.', required=True) 96 | requiredNamed.add_argument('-j', '--json', action='store', 97 | help='Path to the JSON file which includes the templates of the emails to be sent') 98 | if os.name == 'nt': 99 | optionalargs = parser.add_argument_group('Optional arguments') 100 | else: 101 | optionalargs = parser.add_argument_group( 102 | green + 'Optional arguments' + normal) 103 | optionalargs.add_argument('-t', '--tester', action='store', 104 | help='Tester\'s email address - It will be used in all the tests which require an external domain (i.e. outside the domain which is tested) ') 105 | optionalargs.add_argument('-e', '--email', action='store', 106 | help='Client email address - one valid email address from the domain to be tested') 107 | optionalargs.add_argument('-s', '--server', action='store', 108 | help='Mail server IP to be used') 109 | optionalargs.add_argument('-p', '--port', action='store', default=25, 110 | help='Mail server port to be used (default 25)') 111 | optionalargs.add_argument('-l', '--delay', action='store', default=5, 112 | help='Delay between emails - defaults to 5 seconds') 113 | optionalargs.add_argument('-y', '--helo', action='store', default=5, 114 | help='Domain to be used in the EHLO/HELO command') 115 | if os.name == 'posix': 116 | optionalargs.add_argument('-c', '--pcap', action='store', 117 | help='Detailed traffic capture of all the SMTP commands in a readable format. Provide the filename') 118 | 119 | args = parser.parse_args() 120 | 121 | # Check if the Results folder already exists. If not, create it 122 | path = os.getcwd() 123 | if os.path.exists(path + "/Results") == False: 124 | os.mkdir(path + "/Results") 125 | 126 | # Create file to store the results 127 | results_name = args.domain + ".txt" 128 | file = open("Results/" + results_name, "a") 129 | 130 | # Delete pcap file if it exists 131 | file_path = path + "/Results/capture.cap" 132 | if os.path.exists(file_path) == True: 133 | os.remove(file_path) 134 | 135 | 136 | # Fill the domain table 137 | x = PrettyTable() 138 | x.field_names = ["Domain"] 139 | x.add_row([args.domain]) 140 | print(x) 141 | if os.path.isfile('Results/' + results_name): 142 | file.write("\n\n") 143 | file.write(str(x)) 144 | 145 | # if the domain argument is given, print the SPF and DMARC records of the domain 146 | if (args.domain is not None): 147 | try: 148 | spf_record = spf.SpfRecord.from_domain(args.domain) 149 | if os.name == 'nt': 150 | print ("\nSPF Record: ") 151 | print (spf_record.record + "\n") 152 | else: 153 | prGreen("\n SPF Record: ") 154 | prCyan(spf_record.record + "\n") 155 | file.write("\n\nSPF Record: ") 156 | file.write(spf_record.record + "\n") 157 | except: 158 | if os.name == 'nt': 159 | print("No SPF record found\n") 160 | else: 161 | prCyan("No SPF record found\n") 162 | file.write("No SPF record found\n") 163 | 164 | try: 165 | dmarc_record = dmarc.DmarcRecord.from_domain(args.domain) 166 | file.write("\nDMARC Record: ") 167 | if os.name == 'nt': 168 | print("DMARC Record: ") 169 | print(dmarc_record.record + "\n") 170 | else: 171 | prGreen("DMARC Record: ") 172 | prCyan(dmarc_record.record + "\n") 173 | file.write(dmarc_record.record + "\n") 174 | except: 175 | if os.name == 'nt': 176 | print("No DMARC record found\n") 177 | else: 178 | prCyan("No DMARC record found\n") 179 | file.write("No DMARC record found\n") 180 | 181 | # Generate the CLIENTNAME parameter to be replaced in the JSON file 182 | if (args.email is not None): 183 | temp = args.email.split("@") 184 | clientname = temp[0] 185 | 186 | # Start the capturing to save all the SMTP communications 187 | if os.name == 'posix': 188 | if (args.pcap is not None): 189 | tcpd = subprocess.Popen( 190 | ['tcpdump', '-n', 'port ' + str(args.port), '-w', 'Results/capture.cap']) 191 | 192 | # Make all the necessary replacements in the JSON file depending on the 193 | # arguments given by the user 194 | if (args.json is not None): 195 | with open(args.json) as f: 196 | data = json.load(f, strict=False) 197 | 198 | for i in list(range(len(data))): 199 | if(args.email is not None): 200 | data[i]['mailfrom'] = data[i]['mailfrom'].replace( 201 | 'CLIENTEMAIL', args.email) 202 | data[i]['headerfrom'] = data[i]['headerfrom'].replace( 203 | 'CLIENTEMAIL', args.email) 204 | data[i]['to'] = data[i]['to'].replace('CLIENTEMAIL', args.email) 205 | try: 206 | data[i]['returnPath'] = data[i]['returnPath'].replace( 207 | 'CLIENTEMAIL', args.email) 208 | data[i]['replyTo'] = data[i]['replyTo'].replace( 209 | 'CLIENTEMAIL', args.email) 210 | except: 211 | pass 212 | 213 | 214 | data[i]['mailfrom'] = data[i]['mailfrom'].replace( 215 | 'CLIENTNAME', clientname) 216 | data[i]['headerfrom'] = data[i]['headerfrom'].replace( 217 | 'CLIENTNAME', clientname) 218 | data[i]['to'] = data[i]['to'].replace('CLIENTNAME', clientname) 219 | try: 220 | data[i]['returnPath'] = data[i]['returnPath'].replace( 221 | 'CLIENTNAME', clientname) 222 | data[i]['replyTo'] = data[i]['replyTo'].replace( 223 | 'CLIENTNAME', clientname) 224 | except: 225 | pass 226 | 227 | 228 | if(args.tester is not None): 229 | data[i]['mailfrom'] = data[i]['mailfrom'].replace( 230 | 'TESTERDOMAIN', args.tester) 231 | data[i]['headerfrom'] = data[i]['headerfrom'].replace( 232 | 'TESTERDOMAIN', args.tester) 233 | data[i]['to'] = data[i]['to'].replace('TESTERDOMAIN', args.tester) 234 | try: 235 | data[i]['returnPath'] = data[i]['returnPath'].replace( 236 | 'TESTERDOMAIN', args.tester) 237 | data[i]['replyTo'] = data[i]['replyTo'].replace( 238 | 'TESTERDOMAIN', args.tester) 239 | except: 240 | pass 241 | 242 | if(args.domain is not None): 243 | data[i]['mailfrom'] = data[i]['mailfrom'].replace( 244 | 'CLIENTDOMAIN', args.domain) 245 | data[i]['headerfrom'] = data[i]['headerfrom'].replace( 246 | 'CLIENTDOMAIN', args.domain) 247 | data[i]['to'] = data[i]['to'].replace('CLIENTDOMAIN', args.domain) 248 | 249 | if(args.server is not None): 250 | data[i]['server'] = data[i]['server'].replace( 251 | 'SERVERIP', args.server) 252 | 253 | # Write the changes in the JSON file 254 | with open(args.json, 'w') as f: 255 | json.dump(data, f) 256 | 257 | # Open the changed JSON file 258 | with open(args.json) as f: 259 | data_new = json.load(f, strict=False) 260 | 261 | x1 = PrettyTable() 262 | 263 | # Generating the emails based on the JSON templates 264 | for i in list(range(len(data_new))): 265 | if("@" in data_new[i]["mailfrom"]): 266 | at_index = data_new[i]["mailfrom"].index("@") 267 | fromdomain = data_new[i]["mailfrom"][at_index:] 268 | messageID = '<' + ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for k in range(48)) + fromdomain + '>' 269 | elif("@" in data_new[i]["headerfrom"]): 270 | at_index = data_new[i]["headerfrom"].index("@") 271 | fromdomain = data_new[i]["headerfrom"][at_index:] 272 | messageID = '<' + ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for k in range(48)) + fromdomain + '>' 273 | else: 274 | messageID = '<' + ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for k in range(48)) + '@' + ''.join(random.choice(string.ascii_lowercase) for l in range(10)) + '.com' + '>' 275 | 276 | if("replyTo" in data_new[i] and "returnPath" not in data_new[i]): 277 | message = f"""From: {data_new[i]["headerfrom"]} 278 | To: {data_new[i]["to"]} 279 | Reply-To: {data_new[i]["replyTo"]} 280 | Subject: {data_new[i]["subject"]} 281 | Message-ID: {messageID} 282 | 283 | {data_new[i]["body"]} 284 | """ 285 | elif("replyTo" not in data_new[i] and "returnPath" in data_new[i]): 286 | message = f"""From: {data_new[i]["headerfrom"]} 287 | To: {data_new[i]["to"]} 288 | Return-Path: {data_new[i]["returnPath"]} 289 | Subject: {data_new[i]["subject"]} 290 | Message-ID: {messageID} 291 | 292 | {data_new[i]["body"]} 293 | """ 294 | elif("replyTo" in data_new[i] and "returnPath" in data_new[i]): 295 | message = f"""From: {data_new[i]["headerfrom"]} 296 | To: {data_new[i]["to"]} 297 | Reply-To: {data_new[i]["replyTo"]} 298 | Return-Path: {data_new[i]["returnPath"]} 299 | Subject: {data_new[i]["subject"]} 300 | Message-ID: {messageID} 301 | 302 | {data_new[i]["body"]} 303 | """ 304 | elif("replyTo" not in data_new[i] and "returnPath" not in data_new[i]): 305 | message = f"""From: {data_new[i]["headerfrom"]} 306 | To: {data_new[i]["to"]} 307 | Subject: {data_new[i]["subject"]} 308 | Message-ID: {messageID} 309 | 310 | {data_new[i]["body"]} 311 | """ 312 | 313 | if os.name == 'nt': 314 | print("\nLIVE results - Scenario " + data_new[i]["scenario_no"]) 315 | else: 316 | prGreen("\nLIVE results - Scenario " + data_new[i]["scenario_no"]) 317 | 318 | try: 319 | # Attempt to send the emails; if there is an SMTP error it will throw an exception 320 | smtpObj = smtplib.SMTP(data_new[i]["server"], args.port) 321 | if args.helo is not None: 322 | smtpObj.helo(args.helo) 323 | smtpObj.sendmail(data_new[i]["mailfrom"], 324 | data_new[i]["to"], message) 325 | if os.name == 'nt': 326 | print("Email successfully sent") 327 | else: 328 | prCyan("Email successfully sent") 329 | x1.field_names = ["No", "Result", "MailFrom", "HeaderFrom", "To", "Description"] 330 | x1.add_row([data_new[i]["scenario_no"], 331 | "Sent", data_new[i]["mailfrom"], data_new[i]["headerfrom"], data_new[i]["to"], data_new[i]["comment"]]) 332 | x1.align["Description"] = "l" 333 | print(x1) 334 | 335 | except SMTPException as e: 336 | if os.name == 'nt': 337 | print ("SMTP Error: ") 338 | print (e) 339 | else: 340 | print (red + "SMTP Error: ") 341 | prRed(e) 342 | x1.field_names = ["No", "Result", "MailFrom", "HeaderFrom", "To", "Description"] 343 | x1.add_row([data_new[i]["scenario_no"], 344 | "Not sent", data_new[i]["mailfrom"], data_new[i]["headerfrom"], data_new[i]["to"], data_new[i]["comment"]]) 345 | x1.align["Description"] = "l" 346 | print(x1) 347 | time.sleep(int(args.delay)) 348 | 349 | print ("\n") 350 | if os.name == 'nt': 351 | print ("FINAL results:") 352 | else: 353 | prCyan("FINAL results:") 354 | x1.align["Description"] = "l" 355 | print(x1) 356 | file.write("\nFINAL Results\n") 357 | file.write(str(x1)) 358 | file.close() 359 | 360 | # fakeemail to fix the tcpdump which is not capturing the last few packets 361 | # Ignore this one in your results if it is shown 362 | try: 363 | smtpObj = smtplib.SMTP(args.server, args.port) 364 | smtpObj.sendmail("fakeemail", 365 | "fakeemail", "fakemessage") 366 | except: 367 | pass 368 | 369 | else: 370 | if os.name == 'nt': 371 | print ( 372 | "NOTE: For futher testing, you should provide a JSON file with the correct templates") 373 | else: 374 | prYellow(BOLD + "NOTE: " + normal + yellow + 375 | "For futher testing, you should provide a JSON file with the correct templates") 376 | 377 | # Use the PCAP file generated by tcpdump and present it in a readable format in an output TXT file 378 | if os.name == 'posix': 379 | if (args.pcap is not None): 380 | f1 = open("Results/" + args.pcap, "a+") 381 | tcpd.send_signal(subprocess.signal.SIGTERM) 382 | packets = rdpcap('Results/capture.cap') 383 | i = 0 384 | j = 0 385 | while (i < len(packets)): 386 | try: 387 | data = packets[i][Raw].load 388 | src = packets[i][IP].src 389 | dst = packets[i][IP].dst 390 | data_string = str(data) 391 | data_string = data_string.replace('b\'', '') 392 | if "ehlo" in data_string or "helo" in data_string: 393 | if (j + 1 <= len(data_new)): 394 | f1.write("\n----------------------\n") 395 | f1.write(" Scenario " + 396 | data_new[j]["scenario_no"] + "\n") 397 | f1.write("----------------------\n") 398 | j = j + 1 399 | if (j <= len(data_new)): 400 | if str(dst) == args.server: 401 | f1.write("CLIENT: ") 402 | else: 403 | f1.write("SERVER: ") 404 | f1.write(data_string) 405 | f1.write("\n") 406 | except: 407 | pass 408 | i = i + 1 409 | with open('Results/' + args.pcap, 'r') as fin: 410 | temp = fin.read().splitlines(True) 411 | with open('Results/' + args.pcap, 'w') as fout: 412 | fout.writelines(temp[2:]) 413 | f1.close() 414 | --------------------------------------------------------------------------------