Merge pull request #18 from MattKeeley/bugfix-org-records

bugfix: Org Records
This commit is contained in:
Matt Keeley
2023-04-05 15:53:50 -07:00
committed by GitHub
6 changed files with 171 additions and 82 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.")

View File

@@ -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

View File

@@ -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

View File

@@ -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)