mirror of
https://github.com/MattKeeley/Spoofy.git
synced 2026-02-03 13:33:24 +00:00
Merge pull request #18 from MattKeeley/bugfix-org-records
bugfix: Org Records
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import dns.resolver, tldextract
|
||||
import dns.resolver
|
||||
import tldextract
|
||||
|
||||
|
||||
def get_dmarc_record(domain, dns_server):
|
||||
"""Returns the DMARC record for a given domain."""
|
||||
subdomain = tldextract.extract(domain).registered_domain
|
||||
if subdomain != domain:
|
||||
return get_dmarc_record(subdomain, dns_server)
|
||||
try:
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = [dns_server]
|
||||
@@ -11,15 +16,11 @@ def get_dmarc_record(domain, dns_server):
|
||||
|
||||
for dns_data in dmarc:
|
||||
if "DMARC1" in str(dns_data):
|
||||
dmarc_record = str(dns_data).replace('"','')
|
||||
dmarc_record = str(dns_data).replace('"', '')
|
||||
return dmarc_record
|
||||
|
||||
subdomain = tldextract.extract(domain).registered_domain
|
||||
if subdomain != domain:
|
||||
return get_dmarc_record(subdomain, dns_server)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_details(dmarc_record):
|
||||
"""Returns a tuple containing policy, pct, aspf, subdomain policy,
|
||||
forensic report uri, and aggregate report uri from a DMARC record"""
|
||||
@@ -31,32 +32,50 @@ def get_dmarc_details(dmarc_record):
|
||||
rua = get_dmarc_aggregate_reports(dmarc_record)
|
||||
return p, pct, aspf, sp, fo, rua
|
||||
|
||||
|
||||
def get_dmarc_policy(dmarc_record):
|
||||
"""Returns the policy value from a DMARC record."""
|
||||
if "p=" in str(dmarc_record): return str(dmarc_record).split("p=")[1].split(";")[0]
|
||||
else: return None
|
||||
if "p=" in str(dmarc_record):
|
||||
return str(dmarc_record).split("p=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_pct(dmarc_record):
|
||||
"""Returns the pct value from a DMARC record."""
|
||||
if "pct" in str(dmarc_record): return str(dmarc_record).split("pct=")[1].split(";")[0]
|
||||
else: return None
|
||||
if "pct=" in str(dmarc_record):
|
||||
return str(dmarc_record).split("pct=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_aspf(dmarc_record):
|
||||
"""Returns the aspf value from a DMARC record"""
|
||||
if "aspf=" in str(dmarc_record): return str(dmarc_record).split("aspf=")[1].split(";")[0]
|
||||
else: return None
|
||||
if "aspf=" in str(dmarc_record):
|
||||
return str(dmarc_record).split("aspf=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_subdomain_policy(dmarc_record):
|
||||
"""Returns the policy to apply for subdomains from a DMARC record."""
|
||||
if "sp=" in str(dmarc_record):return str(dmarc_record).split("sp=")[1].split(";")[0]
|
||||
else: return None
|
||||
if "sp=" in str(dmarc_record):
|
||||
return str(dmarc_record).split("sp=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_forensic_reports(dmarc_record):
|
||||
"""Returns the email addresses to which forensic reports should be sent."""
|
||||
if "ruf=" in str(dmarc_record) and "fo=1" in str(dmarc_record): return str(dmarc_record).split("ruf=")[1].split(";")[0]
|
||||
else: return None
|
||||
if "ruf=" in str(dmarc_record) and "fo=1" in str(dmarc_record):
|
||||
return str(dmarc_record).split("ruf=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_aggregate_reports(dmarc_record):
|
||||
"""Returns the email addresses to which aggregate reports should be sent."""
|
||||
if "rua=" in str(dmarc_record): return str(dmarc_record).split("rua=")[1].split(";")[0]
|
||||
else: return None
|
||||
if "rua=" in str(dmarc_record):
|
||||
return str(dmarc_record).split("rua=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
10
libs/dns.py
10
libs/dns.py
@@ -1,6 +1,8 @@
|
||||
import dns.resolver, socket
|
||||
import dns.resolver
|
||||
import socket
|
||||
from . import spf, dmarc
|
||||
|
||||
|
||||
def get_soa_record(domain):
|
||||
"""Returns the SOA record of a given domain."""
|
||||
resolver = dns.resolver.Resolver()
|
||||
@@ -15,6 +17,7 @@ def get_soa_record(domain):
|
||||
return socket.gethostbyname(dns_server)
|
||||
return None
|
||||
|
||||
|
||||
def get_dns_server(domain):
|
||||
"""Finds the DNS server that serves the domain and returns it, along with any SPF or DMARC records."""
|
||||
SOA = get_soa_record(domain)
|
||||
@@ -31,10 +34,11 @@ def get_dns_server(domain):
|
||||
dmarc_record = dmarc.get_dmarc_record(domain, '8.8.8.8')
|
||||
if (spf_record is not None) or (dmarc_record is not None):
|
||||
return '8.8.8.8', spf_record, dmarc_record
|
||||
# No SPF or DMARC record found using 3 different DNS providers.
|
||||
# No SPF or DMARC record found using 3 different DNS providers.
|
||||
# Defaulting back to Cloudflare
|
||||
return '1.1.1.1', spf_record, dmarc_record
|
||||
|
||||
|
||||
def get_txt_record(domain, record_type):
|
||||
"""Returns the TXT record of a given type for a given domain."""
|
||||
resolver = dns.resolver.Resolver()
|
||||
@@ -43,4 +47,4 @@ def get_txt_record(domain, record_type):
|
||||
query = resolver.query(domain, record_type)
|
||||
return str(query[0])
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -12,43 +12,72 @@ def is_spoofable(domain, p, aspf, spf_record, spf_all, spf_includes, sp, pct):
|
||||
6: Indicates that subdomain spoofing might be possible (mailbox dependent) for the domain.
|
||||
7: Indicates that subdomain spoofing is possible, and organizational domain spoofing might be possible.
|
||||
8: Indicates that spoofing is not possible for the domain.
|
||||
"""
|
||||
"""
|
||||
try:
|
||||
if pct and int(pct) != 100: return 3
|
||||
if pct and int(pct) != 100:
|
||||
return 3
|
||||
elif spf_record is None:
|
||||
if p is None: return 0
|
||||
else: return 8
|
||||
if p is None:
|
||||
return 0
|
||||
else:
|
||||
return 8
|
||||
elif spf_includes > 10 and p is None:
|
||||
return 0
|
||||
elif spf_all == "2many":
|
||||
if p == "none": return 3
|
||||
else: return 8
|
||||
elif spf_all and p is None: return 0
|
||||
elif spf_all == "2many":
|
||||
if p == "none":
|
||||
return 3
|
||||
else:
|
||||
return 8
|
||||
elif spf_all and p is None:
|
||||
return 0
|
||||
elif spf_all == "-all":
|
||||
if p and aspf and sp == "none": return 1
|
||||
elif aspf is None and sp == "none": return 1
|
||||
elif p == "none" and (aspf == "r" or aspf is None) and sp is None: return 4
|
||||
elif p == "none" and aspf == "r" and (sp == "reject" or sp == "quarentine"): return 2
|
||||
elif p == "none" and aspf is None and (sp == "reject" or sp == "quarentine"): return 5
|
||||
elif p == "none" and aspf is None and sp == "none": return 7
|
||||
else: return 8
|
||||
if p and aspf and sp == "none":
|
||||
return 1
|
||||
elif aspf is None and sp == "none":
|
||||
return 1
|
||||
elif p == "none" and (aspf == "r" or aspf is None) and sp is None:
|
||||
return 4
|
||||
elif p == "none" and aspf == "r" and (sp == "reject" or sp == "quarentine"):
|
||||
return 2
|
||||
elif p == "none" and aspf is None and (sp == "reject" or sp == "quarentine"):
|
||||
return 5
|
||||
elif p == "none" and aspf is None and sp == "none":
|
||||
return 7
|
||||
else:
|
||||
return 8
|
||||
elif spf_all == "~all":
|
||||
if p == "none" and sp == "reject" or sp == "quarentine": return 2
|
||||
elif p == "none" and sp is None: return 0
|
||||
elif p == "none" and sp == "none": return 7
|
||||
elif (p == "reject" or p == "quarentine") and aspf is None and sp == "none": return 1
|
||||
elif (p == "reject" or p == "quarentine") and aspf and sp == "none": return 1
|
||||
else: return 8
|
||||
if p == "none" and sp == "reject" or sp == "quarentine":
|
||||
return 2
|
||||
elif p == "none" and sp is None:
|
||||
return 0
|
||||
elif p == "none" and sp == "none":
|
||||
return 7
|
||||
elif (p == "reject" or p == "quarentine") and aspf is None and sp == "none":
|
||||
return 1
|
||||
elif (p == "reject" or p == "quarentine") and aspf and sp == "none":
|
||||
return 1
|
||||
else:
|
||||
return 8
|
||||
elif spf_all == "?all":
|
||||
if (p == "reject" or p == "quarentine") and aspf and sp == "none": return 6
|
||||
elif (p == "reject" or p == "quarentine") and aspf is None and sp == "none": return 6
|
||||
elif p == "none" and aspf == "r" and sp is None: return 0
|
||||
elif p == "none" and aspf == "r" and sp == "none": return 7
|
||||
elif p == "none" and aspf == "s" or None and sp == "none": return 7
|
||||
elif p == "none" and aspf == "s" or None and sp is None: return 6
|
||||
elif p == "none" and aspf and (sp == "reject" or sp == "quarentine"):return 5
|
||||
elif p == "none" and aspf is None and sp == "reject": return 5
|
||||
else: return 8
|
||||
else: return 8
|
||||
if (p == "reject" or p == "quarentine") and aspf and sp == "none":
|
||||
return 6
|
||||
elif (p == "reject" or p == "quarentine") and aspf is None and sp == "none":
|
||||
return 6
|
||||
elif p == "none" and aspf == "r" and sp is None:
|
||||
return 0
|
||||
elif p == "none" and aspf == "r" and sp == "none":
|
||||
return 7
|
||||
elif p == "none" and aspf == "s" or None and sp == "none":
|
||||
return 7
|
||||
elif p == "none" and aspf == "s" or None and sp is None:
|
||||
return 6
|
||||
elif p == "none" and aspf and (sp == "reject" or sp == "quarentine"):
|
||||
return 5
|
||||
elif p == "none" and aspf is None and sp == "reject":
|
||||
return 5
|
||||
else:
|
||||
return 8
|
||||
else:
|
||||
return 8
|
||||
except:
|
||||
print("If you hit this error message, Open an issue with your testcase.")
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
from colorama import Fore, Style
|
||||
from colorama import init as color_init
|
||||
import os, pandas as pd
|
||||
import os
|
||||
import pandas as pd
|
||||
|
||||
color_init()
|
||||
|
||||
|
||||
def output_good(line):
|
||||
print(Fore.GREEN + Style.BRIGHT + "[+]" + Style.RESET_ALL, line)
|
||||
|
||||
|
||||
def output_warning(line):
|
||||
print(Fore.YELLOW + Style.BRIGHT + "[?]" + Style.RESET_ALL, line)
|
||||
|
||||
|
||||
def output_bad(line):
|
||||
print(Fore.RED + Style.BRIGHT + "[-]" + Style.RESET_ALL, line)
|
||||
|
||||
|
||||
def output_indifferent(line):
|
||||
print(Fore.BLUE + Style.BRIGHT + "[*]" + Style.RESET_ALL, line)
|
||||
|
||||
|
||||
def output_error(line):
|
||||
print(Fore.RED + Style.BRIGHT + "[-] !!! " + Style.NORMAL, line, Style.BRIGHT + "!!!\n")
|
||||
print(Fore.RED + Style.BRIGHT + "[-] !!! " +
|
||||
Style.NORMAL, line, Style.BRIGHT + "!!!\n")
|
||||
|
||||
|
||||
def output_info(line):
|
||||
print(Fore.WHITE + Style.BRIGHT + "[*]" + Style.RESET_ALL, line)
|
||||
|
||||
|
||||
def write_to_excel(data):
|
||||
"""This function writes a DataFrame of data to an Excel file.
|
||||
If the file does not already exist, it creates the file, and if the file already exists, it appends the new data to the existing data."""
|
||||
@@ -42,7 +51,7 @@ def write_to_excel(data):
|
||||
def printer(domain, subdomain, dns_server, spf_record, spf_all, spf_includes, dmarc_record, p, pct, aspf, sp, fo, rua, spoofable):
|
||||
"""This function is a utility function that takes in various parameters related to the
|
||||
results of DMARC and SPF checks and outputs the results to the console in a human-readable format.
|
||||
|
||||
|
||||
Printer ID Handler:
|
||||
0: Indicates that spoofing is possible for the domain.
|
||||
1: Indicates that subdomain spoofing is possible for the domain.
|
||||
@@ -53,7 +62,7 @@ def printer(domain, subdomain, dns_server, spf_record, spf_all, spf_includes, dm
|
||||
6: Indicates that subdomain spoofing might be possible (mailbox dependent) for the domain.
|
||||
7: Indicates that subdomain spoofing is possible, and organizational domain spoofing might be possible.
|
||||
8: Indicates that spoofing is not possible for the domain.
|
||||
"""
|
||||
"""
|
||||
output_indifferent(f"Domain: {domain}")
|
||||
output_indifferent(f"Is subdomain: {subdomain}")
|
||||
output_indifferent(f"DNS Server: {dns_server}")
|
||||
@@ -66,18 +75,25 @@ def printer(domain, subdomain, dns_server, spf_record, spf_all, spf_includes, dm
|
||||
output_warning("SPF record contains multiple `All` items.")
|
||||
else:
|
||||
output_info(f"SPF all record: {spf_all}")
|
||||
output_info(f"SPF include count: {spf_includes}" if spf_includes <= 10 else f"Too many SPF include lookups {spf_includes}.")
|
||||
output_info(f"SPF include count: {spf_includes}" if spf_includes <=
|
||||
10 else f"Too many SPF include lookups {spf_includes}.")
|
||||
else:
|
||||
output_warning("No SPF record found.")
|
||||
|
||||
if dmarc_record:
|
||||
output_info(f"DMARC record: {dmarc_record}")
|
||||
output_info(f"Found DMARC policy: {p}" if p else "No DMARC policy found.")
|
||||
output_info(f"Found DMARC pct: {pct}" if pct else "No DMARC pct found.")
|
||||
output_info(f"Found DMARC aspf: {aspf}" if aspf else "No DMARC aspf found.")
|
||||
output_info(f"Found DMARC subdomain policy: {sp}" if sp else "No DMARC subdomain policy found.")
|
||||
output_indifferent(f"Forensics reports will be sent: {fo}" if fo else "No DMARC foresnic report location found.")
|
||||
output_indifferent(f"Aggregate reports will be sent to: {rua}" if rua else "No DMARC aggregate report location found.")
|
||||
output_info(
|
||||
f"Found DMARC policy: {p}" if p else "No DMARC policy found.")
|
||||
output_info(
|
||||
f"Found DMARC pct: {pct}" if pct else "No DMARC pct found.")
|
||||
output_info(
|
||||
f"Found DMARC aspf: {aspf}" if aspf else "No DMARC aspf found.")
|
||||
output_info(
|
||||
f"Found DMARC subdomain policy: {sp}" if sp else "No DMARC subdomain policy found.")
|
||||
output_indifferent(
|
||||
f"Forensics reports will be sent: {fo}" if fo else "No DMARC foresnic report location found.")
|
||||
output_indifferent(
|
||||
f"Aggregate reports will be sent to: {rua}" if rua else "No DMARC aggregate report location found.")
|
||||
else:
|
||||
output_warning("No DMARC record found.")
|
||||
|
||||
@@ -85,5 +101,6 @@ def printer(domain, subdomain, dns_server, spf_record, spf_all, spf_includes, dm
|
||||
if spoofable == 8:
|
||||
output_bad("Spoofing not possible for " + domain)
|
||||
else:
|
||||
output_good("Spoofing possible for " + domain if spoofable == 0 else "Subdomain spoofing possible for " + domain if spoofable == 1 else "Organizational domain spoofing possible for " + domain if spoofable == 2 else "Spoofing might be possible for " + domain if spoofable == 3 else "Spoofing might be possible (Mailbox dependant) for " + domain if spoofable == 4 else "Organizational domain spoofing may be possible for " + domain if spoofable == 5 else "Subdomain spoofing might be possible (Mailbox dependant) for " + domain if spoofable == 6 else "Subdomain spoofing might be possible (Mailbox dependant) for " + domain if spoofable == 7 else "")
|
||||
print() # padding
|
||||
output_good("Spoofing possible for " + domain if spoofable == 0 else "Subdomain spoofing possible for " + domain if spoofable == 1 else "Organizational domain spoofing possible for " + domain if spoofable == 2 else "Spoofing might be possible for " + domain if spoofable == 3 else "Spoofing might be possible (Mailbox dependant) for " +
|
||||
domain if spoofable == 4 else "Organizational domain spoofing may be possible for " + domain if spoofable == 5 else "Subdomain spoofing might be possible (Mailbox dependant) for " + domain if spoofable == 6 else "Subdomain spoofing might be possible (Mailbox dependant) for " + domain if spoofable == 7 else "")
|
||||
print() # padding
|
||||
|
||||
10
libs/spf.py
10
libs/spf.py
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
import dns.resolver
|
||||
|
||||
|
||||
def get_spf_record(domain, dns_server):
|
||||
"""Returns the SPF record for a given domain."""
|
||||
try:
|
||||
@@ -15,6 +16,7 @@ def get_spf_record(domain, dns_server):
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_spf_all_string(spf_record):
|
||||
"""Returns the string value of the all mechanism in the SPF record."""
|
||||
all_matches = re.findall(r'[-~?] ?a ?l ?l', spf_record)
|
||||
@@ -25,9 +27,11 @@ def get_spf_all_string(spf_record):
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_spf_includes(domain, count=0):
|
||||
"""Returns the number of includes in the SPF record for a given domain."""
|
||||
if count > 10: return count
|
||||
if count > 10:
|
||||
return count
|
||||
try:
|
||||
spf_record = get_spf_record(domain, '1.1.1.1')
|
||||
if spf_record:
|
||||
@@ -41,5 +45,5 @@ def get_spf_includes(domain, count=0):
|
||||
count = get_spf_includes(url, count + 1)
|
||||
except:
|
||||
pass
|
||||
#print("Could not find SPF record for " + domain)
|
||||
return count
|
||||
# print("Could not find SPF record for " + domain)
|
||||
return count
|
||||
|
||||
42
spoofy.py
42
spoofy.py
@@ -1,9 +1,13 @@
|
||||
#! /usr/bin/env python3
|
||||
import argparse, tldextract, threading
|
||||
import argparse
|
||||
import tldextract
|
||||
import threading
|
||||
import os
|
||||
from libs import dmarc, dns, logic, spf, report
|
||||
|
||||
print_lock = threading.Lock()
|
||||
|
||||
|
||||
def process_domain(domain, output):
|
||||
"""This function takes a list of domains and an output format (either 'xls' or 'stdout')
|
||||
as arguments. It processes each domain, collects its relevant details,
|
||||
@@ -18,21 +22,24 @@ def process_domain(domain, output):
|
||||
spf_includes = spf.get_spf_includes(domain)
|
||||
if dmarc_record:
|
||||
p, pct, aspf, sp, fo, rua = dmarc.get_dmarc_details(dmarc_record)
|
||||
spoofable = logic.is_spoofable(domain, p, aspf, spf_record, spf_all, spf_includes, sp, pct)
|
||||
spoofable = logic.is_spoofable(
|
||||
domain, p, aspf, spf_record, spf_all, spf_includes, sp, pct)
|
||||
if output == "xls":
|
||||
with print_lock:
|
||||
data = [{'DOMAIN': domain, 'SUBDOMAIN': subdomain, 'SPF': spf_record, 'SPF MULTIPLE ALLS': spf_all,
|
||||
'SPF TOO MANY INCLUDES': spf_includes, 'DMARC': dmarc_record, 'DMARC POLICY': p,
|
||||
'DMARC PCT': pct, 'DMARC ASPF': aspf, 'DMARC SP': sp, 'DMARC FORENSIC REPORT': fo,
|
||||
'DMARC AGGREGATE REPORT': rua, 'SPOOFING POSSIBLE': spoofable}]
|
||||
report.write_to_excel(data)
|
||||
'DMARC PCT': pct, 'DMARC ASPF': aspf, 'DMARC SP': sp, 'DMARC FORENSIC REPORT': fo,
|
||||
'DMARC AGGREGATE REPORT': rua, 'SPOOFING POSSIBLE': spoofable}]
|
||||
report.write_to_excel(data)
|
||||
else:
|
||||
with print_lock:
|
||||
report.printer(domain, subdomain, dns_server, spf_record, spf_all, spf_includes, dmarc_record, p, pct, aspf,
|
||||
sp, fo, rua, spoofable)
|
||||
sp, fo, rua, spoofable)
|
||||
except:
|
||||
with print_lock:
|
||||
report.output_error(f"Domain {domain} is offline or format cannot be interpreted.")
|
||||
report.output_error(
|
||||
f"Domain {domain} is offline or format cannot be interpreted.")
|
||||
|
||||
|
||||
def process_domains(domains, output):
|
||||
"""
|
||||
@@ -48,20 +55,29 @@ def process_domains(domains, output):
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("-iL", type=str, required=False, help="Provide an input list.")
|
||||
group.add_argument("-d", type=str, required=False, help="Provide an single domain.")
|
||||
parser.add_argument("-o", type=str, choices=['xls', 'stdout'], required=True, help="Output format stdout or xls")
|
||||
group.add_argument("-iL", type=str, required=False,
|
||||
help="Provide an input list.")
|
||||
group.add_argument("-d", type=str, required=False,
|
||||
help="Provide a single domain.")
|
||||
parser.add_argument("-o", type=str, choices=['xls', 'stdout'],
|
||||
default='stdout', help="Output format: stdout or xls (default: stdout)")
|
||||
options = parser.parse_args()
|
||||
if not any(vars(options).values()): parser.error("No arguments provided. Usage: `spoofy.py -d [DOMAIN] -o [stdout or xls]` OR `spoofy.py -iL [DOMAIN_LIST] -o [stdout or xls]`")
|
||||
options = parser.parse_args()
|
||||
if not any(vars(options).values()):
|
||||
parser.error(
|
||||
"No arguments provided. Usage: `spoofy.py -d [DOMAIN] -o [stdout or xls]` OR `spoofy.py -iL [DOMAIN_LIST] -o [stdout or xls]`")
|
||||
domains = []
|
||||
if options.iL:
|
||||
try:
|
||||
with open(options.iL, "r") as f:
|
||||
for line in f: domains.append(line.strip('\n'))
|
||||
except IOError: report.output_error("File doesnt exist or cannot be read.")
|
||||
for line in f:
|
||||
domains.append(line.strip('\n'))
|
||||
except IOError:
|
||||
report.output_error("File doesnt exist or cannot be read.")
|
||||
if options.d:
|
||||
domains.append(options.d)
|
||||
process_domains(domains, options.o)
|
||||
|
||||
Reference in New Issue
Block a user