Merge pull request #27 from MattKeeley/26-spoofing-not-possible-without-spf-or-dmarc-record

New improvements and test cases
This commit is contained in:
Matt Keeley
2024-08-12 15:57:45 -07:00
committed by GitHub
20 changed files with 1035 additions and 563 deletions

View File

@@ -6,12 +6,20 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: python3 test.py
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.x"
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install ruff
- name: Run Ruff
run: ruff check .
- name: Run tests
run: python3 test.py

View File

@@ -5,42 +5,56 @@
Spoofy
</h1>
[![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)](https://www.python.org/)
[![forthebadge](https://forthebadge.com/images/badges/contains-tasty-spaghetti-code.svg)](https://www.thewholesomedish.com/spaghetti/)
[![forthebadge](https://forthebadge.com/images/badges/it-works-why.svg)](https://www.youtube.com/watch?v=kyti25ol438)
## WHAT
`Spoofy` is a program that checks if a list of domains can be spoofed based on SPF and DMARC records. You may be asking, "Why do we need another tool that can check if a domain can be spoofed?"
Well, Spoofy is different and here is why:
> 1. Authoritative lookups on all lookups with known fallback (Cloudflare DNS)
> 2. Accurate bulk lookups
> 3. Custom, manually tested spoof logic (No guessing or speculating, real world test results)
> 4. SPF lookup counter
> 3. Custom, manually tested spoof logic (No guessing or speculating, real world test results)
> 4. SPF DNS query counter
## PASSING TESTS
[![Spoofy CI](https://github.com/MattKeeley/Spoofy/actions/workflows/ci.yml/badge.svg)](https://github.com/MattKeeley/Spoofy/actions/workflows/ci.yml)
## HOW TO USE
`Spoofy` requires **Python 3+**. Python 2 is not supported. Usage is shown below:
```console
Usage:
./spoofy.py -d [DOMAIN] -o [stdout or xls]
./spoofy.py -d [DOMAIN] -o [stdout or xls] -t [NUMBER_OF_THREADS]
OR
./spoofy.py -iL [DOMAIN_LIST] -o [stdout or xls]
./spoofy.py -iL [DOMAIN_LIST] -o [stdout or xls] -t [NUMBER_OF_THREADS]
Options:
-d : Process a single domain.
-iL : Provide a file containing a list of domains to process.
-o : Specify the output format: stdout (default) or xls.
-t : Set the number of threads to use (default: 4).
Examples:
./spoofy.py -d example.com -t 10
./spoofy.py -iL domains.txt -o xls
Install Dependencies:
pip3 install -r requirements.txt
```
## HOW DO YOU KNOW ITS SPOOFABLE
(The spoofability table lists every combination of SPF and DMARC configurations that impact deliverability to the inbox, except for DKIM modifiers.)
[Download Here](/files/Master_Table.xlsx)
## METHODOLOGY
## METHODOLOGY
The creation of the spoofability table involved listing every relevant SPF and DMARC configuration, combining them, and then conducting SPF and DMARC information collection using an early version of Spoofy on a large number of US government domains. Testing if an SPF and DMARC combination was spoofable or not was done using the email security pentesting suite at [emailspooftest](https://emailspooftest.com/) using Microsoft 365. However, the initial testing was conducted using Protonmail and Gmail, but these services were found to utilize reverse lookup checks that affected the results, particularly for subdomain spoof testing. As a result, Microsoft 365 was used for the testing, as it offered greater control over the handling of mail.
After the initial testing using Microsoft 365, some combinations were retested using Protonmail and Gmail due to the differences in their handling of banners in emails. Protonmail and Gmail can place spoofed mail in the inbox with a banner or in spam without a banner, leading to some SPF and DMARC combinations being reported as "Mailbox Dependent" when using Spoofy. In contrast, Microsoft 365 places both conditions in spam. The testing and data collection process took several days to complete, after which a good master table was compiled and used as the basis for the Spoofy spoofability logic.
@@ -63,7 +77,6 @@ Logo: cobracode
Tool was inspired by [Bishop Fox's](https://github.com/BishopFox/) project called [spoofcheck](https://github.com/BishopFox/spoofcheck/).
## LICENSE
This project is licensed under the Creative Commons Zero v1.0 Universal - see the [LICENSE](LICENSE) file for details

View File

View File

@@ -1,46 +0,0 @@
import dns.resolver
def get_bimi_record(domain, dns_server):
"""Returns the BIMI record for a given domain."""
try:
resolver = dns.resolver.Resolver()
resolver.nameservers = [dns_server, '1.1.1.1', '8.8.8.8']
query_result = resolver.resolve('default._bimi.' + domain, 'TXT')
for record in query_result:
if 'v=BIMI' in str(record):
return record
return None
except:
return None
def get_bimi_details(bimi_record):
"""Returns a tuple containing policy, pct, aspf, subdomain policy,
forensic report uri, and aggregate report uri from a BIMI record"""
version = get_bimi_version(bimi_record)
location = get_bimi_location(bimi_record)
authority = get_bimi_authority(bimi_record)
return version, location, authority
def get_bimi_version(bimi_record):
"""Returns the version value from a BIMI record."""
if "v=" in str(bimi_record):
return str(bimi_record).split("v=")[1].split(";")[0]
else:
return None
def get_bimi_location(bimi_record):
"""Returns the location value from a BIMI record."""
if "l=" in str(bimi_record):
return str(bimi_record).split("l=")[1].split(";")[0]
else:
return None
def get_bimi_authority(bimi_record):
"""Returns the authority value from a BIMI record."""
if "a=" in str(bimi_record):
return str(bimi_record).split("a=")[1].split(";")[0]
else:
return None

View File

@@ -1,81 +0,0 @@
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]
dmarc = resolver.resolve(f"_dmarc.{domain}", "TXT")
except:
return None
for dns_data in dmarc:
if "DMARC1" in str(dns_data):
dmarc_record = str(dns_data).replace('"', '')
return dmarc_record
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"""
p = get_dmarc_policy(dmarc_record)
pct = get_dmarc_pct(dmarc_record)
aspf = get_dmarc_aspf(dmarc_record)
sp = get_dmarc_subdomain_policy(dmarc_record)
fo = get_dmarc_forensic_reports(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
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
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
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
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
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

View File

@@ -1,58 +0,0 @@
import dns.resolver
import socket
from . import spf, dmarc, bimi
def get_soa_record(domain):
"""Returns the SOA record of a given domain."""
resolver = dns.resolver.Resolver()
resolver.nameservers = ['1.1.1.1']
try:
query = resolver.resolve(domain, 'SOA')
except:
return None
if query:
for data in query:
dns_server = str(data.mname)
try:
return socket.gethostbyname(dns_server)
except:
return None
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)
spf_record = dmarc_record = partial_spf_record = partial_dmarc_record = bimi_record = None
if SOA:
spf_record = spf.get_spf_record(domain, SOA)
dmarc_record = dmarc.get_dmarc_record(domain, SOA)
bimi_record = bimi.get_bimi_record(domain, SOA)
if spf_record and dmarc_record:
return SOA, spf_record, dmarc_record, bimi_record
for ip_address in ['1.1.1.1', '8.8.8.8', '9.9.9.9']:
spf_record = spf.get_spf_record(domain, ip_address)
dmarc_record = dmarc.get_dmarc_record(domain, ip_address)
bimi_record = bimi.get_bimi_record(domain, SOA)
if spf_record and dmarc_record:
return ip_address, spf_record, dmarc_record, bimi_record
if spf_record:
partial_spf_record = spf_record
if dmarc_record:
partial_dmarc_record = dmarc_record
return '1.1.1.1', partial_spf_record, partial_dmarc_record, bimi_record
def get_txt_record(domain, record_type):
"""Returns the TXT record of a given type for a given domain."""
resolver = dns.resolver.Resolver()
resolver.nameservers = [get_dns_server(domain)]
try:
query = resolver.query(domain, record_type)
return str(query[0])
except:
return None

View File

@@ -1,83 +0,0 @@
def is_spoofable(domain, p, aspf, spf_record, spf_all, spf_includes, sp, pct):
"""This function takes in DMARC and SPF data for a domain, as well as subdomain policy and percentage options,
and determines if the domain is vulnerable to email spoofing. The function returns an integer value indicating
the class of vulnerability.
ID Handler:
0: Indicates that spoofing is possible for the domain.
1: Indicates that subdomain spoofing is possible for the domain.
2: Indicates that organizational domain spoofing is possible for the domain.
3: Indicates that spoofing might be possible for the domain.
4: Indicates that spoofing might be possible (mailbox dependent) for the domain.
5: Indicates that organizational domain spoofing may be possible for the domain.
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
elif spf_record is None:
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 == "-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
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
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
except:
print("If you hit this error message, Open an issue with your testcase.")

View File

@@ -1,119 +0,0 @@
from colorama import Fore, Style
from colorama import init as color_init
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")
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."""
file_name = "report.xlsx"
if not os.path.exists(file_name):
open(file_name, 'w').close()
if os.path.getsize(file_name) > 0:
existing_df = pd.read_excel(file_name)
new_df = pd.DataFrame(data)
combined_df = pd.concat([existing_df, new_df])
combined_df.to_excel(file_name, index=False)
else:
df = pd.DataFrame(data)
df.to_excel(file_name, index=False)
def printer(domain, subdomain, dns_server, spf_record, spf_all, spf_includes, dmarc_record, p, pct, aspf, sp, fo, rua, bimi_record, vbimi, location, authority, 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.
2: Indicates that organizational domain spoofing is possible for the domain.
3: Indicates that spoofing might be possible for the domain.
4: Indicates that spoofing might be possible (mailbox dependent) for the domain.
5: Indicates that organizational domain spoofing may be possible for the domain.
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}")
if spf_record:
output_info(f"SPF record: {spf_record}")
if spf_all is None:
output_info("SPF does not contain an `All` items.")
elif spf_all == "2many":
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}.")
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 forensics 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.")
if(bimi_record):
output_info(f"BIMI record : {bimi_record}")
output_info(f"BIMI version : {vbimi}")
output_info(f"BIMI location : {location}")
output_info(f"BIMI authority : {authority}")
if spoofable in [0, 1, 2, 3, 4, 5, 6, 7, 8]:
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

View File

@@ -1,49 +0,0 @@
import re
import dns.resolver
def get_spf_record(domain, dns_server):
"""Returns the SPF record for a given domain."""
try:
resolver = dns.resolver.Resolver()
resolver.nameservers = [dns_server, '1.1.1.1', '8.8.8.8']
query_result = resolver.resolve(domain, 'TXT')
for record in query_result:
if 'spf1' in str(record):
spf_record = str(record).replace('"', '')
return spf_record
return None
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)
if len(all_matches) == 1:
return all_matches[0]
elif len(all_matches) > 1:
return '2many'
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
try:
spf_record = get_spf_record(domain, '1.1.1.1')
if spf_record:
count += len(re.compile("[ ,+]a[ ,:]").findall(spf_record))
count += len(re.compile("[ ,+]mx[ ,:]").findall(spf_record))
count += len(re.compile("[ ]ptr[ ]").findall(spf_record))
count += len(re.compile("exists[:]").findall(spf_record))
for item in spf_record.split(' '):
if "include:" in item:
url = item.replace('include:', '')
count = get_spf_includes(url, count + 1)
except:
pass
# print("Could not find SPF record for " + domain)
return count

1
list.txt Normal file
View File

@@ -0,0 +1 @@
reddit.com

1
modules/__init__.py Normal file
View File

@@ -0,0 +1 @@
# modules/__init__.py

62
modules/bimi.py Normal file
View File

@@ -0,0 +1,62 @@
# modules/bimi.py
import dns.resolver
class BIMI:
def __init__(self, domain, dns_server=None):
self.domain = domain
self.dns_server = dns_server
self.bimi_record = self.get_bimi_record()
self.version = None
self.location = None
self.authority = None
if self.bimi_record:
self.version = self.get_bimi_version()
self.location = self.get_bimi_location()
self.authority = self.get_bimi_authority()
def get_bimi_record(self):
"""Returns the BIMI record for the domain."""
try:
resolver = dns.resolver.Resolver()
if self.dns_server:
resolver.nameservers = [self.dns_server]
bimi = resolver.resolve(f"default._bimi.{self.domain}", "TXT")
for record in bimi:
if "v=BIMI" in str(record):
return record
return None
except Exception:
return None
def get_bimi_version(self):
"""Returns the version value from a BIMI record."""
if "v=" in str(self.bimi_record):
return str(self.bimi_record).split("v=")[1].split(";")[0]
return None
def get_bimi_location(self):
"""Returns the location value from a BIMI record."""
if "l=" in str(self.bimi_record):
return str(self.bimi_record).split("l=")[1].split(";")[0]
return None
def get_bimi_authority(self):
"""Returns the authority value from a BIMI record."""
if "a=" in str(self.bimi_record):
return str(self.bimi_record).split("a=")[1].split(";")[0]
return None
def get_bimi_details(self):
"""Returns a tuple containing version, location, and authority from a BIMI record."""
return self.version, self.location, self.authority
def __str__(self):
return (
f"BIMI Record: {self.bimi_record}\n"
f"Version: {self.version}\n"
f"Location: {self.location}\n"
f"Authority: {self.authority}"
)

94
modules/dmarc.py Normal file
View File

@@ -0,0 +1,94 @@
# modules/dmarc.py
import dns.resolver
import tldextract
class DMARC:
def __init__(self, domain, dns_server=None):
self.domain = domain
self.dns_server = dns_server
self.dmarc_record = self.get_dmarc_record()
self.policy = None
self.pct = None
self.aspf = None
self.sp = None
self.fo = None
self.rua = None
if self.dmarc_record:
self.policy = self.get_dmarc_policy()
self.pct = self.get_dmarc_pct()
self.aspf = self.get_dmarc_aspf()
self.sp = self.get_dmarc_subdomain_policy()
self.fo = self.get_dmarc_forensic_reports()
self.rua = self.get_dmarc_aggregate_reports()
def get_dmarc_record(self):
"""Returns the DMARC record for the domain."""
subdomain = tldextract.extract(self.domain).registered_domain
if subdomain != self.domain:
return self.get_dmarc_record_for_domain(subdomain)
return self.get_dmarc_record_for_domain(self.domain)
def get_dmarc_record_for_domain(self, domain):
try:
resolver = dns.resolver.Resolver()
if self.dns_server:
resolver.nameservers = [self.dns_server]
dmarc = resolver.resolve(f"_dmarc.{domain}", "TXT")
except Exception:
return None
for dns_data in dmarc:
if "DMARC1" in str(dns_data):
return str(dns_data).replace('"', "")
return None
def get_dmarc_policy(self):
"""Returns the policy value from a DMARC record."""
if "p=" in str(self.dmarc_record):
return str(self.dmarc_record).split("p=")[1].split(";")[0]
return None
def get_dmarc_pct(self):
"""Returns the pct value from a DMARC record."""
if "pct=" in str(self.dmarc_record):
return str(self.dmarc_record).split("pct=")[1].split(";")[0]
return None
def get_dmarc_aspf(self):
"""Returns the aspf value from a DMARC record"""
if "aspf=" in str(self.dmarc_record):
return str(self.dmarc_record).split("aspf=")[1].split(";")[0]
return None
def get_dmarc_subdomain_policy(self):
"""Returns the policy to apply for subdomains from a DMARC record."""
if "sp=" in str(self.dmarc_record):
return str(self.dmarc_record).split("sp=")[1].split(";")[0]
return None
def get_dmarc_forensic_reports(self):
"""Returns the email addresses to which forensic reports should be sent."""
if "ruf=" in str(self.dmarc_record) and "fo=1" in str(self.dmarc_record):
return str(self.dmarc_record).split("ruf=")[1].split(";")[0]
return None
def get_dmarc_aggregate_reports(self):
"""Returns the email addresses to which aggregate reports should be sent."""
if "rua=" in str(self.dmarc_record):
return str(self.dmarc_record).split("rua=")[1].split(";")[0]
return None
def __str__(self):
return (
f"DMARC Record: {self.dmarc_record}\n"
f"Policy: {self.policy}\n"
f"Pct: {self.pct}\n"
f"ASPF: {self.aspf}\n"
f"Subdomain Policy: {self.sp}\n"
f"Forensic Report URI: {self.fo}\n"
f"Aggregate Report URI: {self.rua}"
)

76
modules/dns.py Normal file
View File

@@ -0,0 +1,76 @@
# modules/dns.py
import dns.resolver
import socket
from .spf import SPF
from .dmarc import DMARC
from .bimi import BIMI
class DNS:
def __init__(self, domain):
self.domain = domain
self.soa_record = None
self.dns_server = None
self.spf_record = None
self.dmarc_record = None
self.bimi_record = None
self.get_soa_record()
self.get_dns_server()
def get_soa_record(self):
"""Sets the SOA record and DNS server of a given domain."""
resolver = dns.resolver.Resolver()
resolver.nameservers = ["1.1.1.1"]
try:
query = resolver.resolve(self.domain, "SOA")
except Exception:
return
if query:
for data in query:
dns_server = str(data.mname)
try:
self.soa_record = socket.gethostbyname(dns_server)
self.dns_server = self.soa_record
except Exception:
self.soa_record = None
def get_dns_server(self):
"""Finds the DNS server that serves the domain and retrieves associated SPF, DMARC, and BIMI records."""
if self.soa_record:
self.spf_record = SPF(self.domain, self.soa_record)
self.dmarc_record = DMARC(self.domain, self.soa_record)
self.bimi_record = BIMI(self.domain, self.soa_record)
if self.spf_record.spf_record and self.dmarc_record.dmarc_record:
return
for ip_address in ["1.1.1.1", "8.8.8.8", "9.9.9.9"]:
self.spf_record = SPF(self.domain, ip_address)
self.dmarc_record = DMARC(self.domain, ip_address)
self.bimi_record = BIMI(self.domain, ip_address)
if self.spf_record.spf_record and self.dmarc_record.dmarc_record:
self.dns_server = ip_address
return
self.dns_server = "1.1.1.1"
def get_txt_record(self, record_type):
"""Returns the TXT record of a given type for the domain."""
resolver = dns.resolver.Resolver()
resolver.nameservers = [self.dns_server]
try:
query = resolver.query(self.domain, record_type)
return str(query[0])
except Exception:
return None
def __str__(self):
return (
f"Domain: {self.domain}\n"
f"SOA Record: {self.soa_record}\n"
f"DNS Server: {self.dns_server}\n"
f"SPF Record: {self.spf_record.spf_record}\n"
f"DMARC Record: {self.dmarc_record.dmarc_record}\n"
f"BIMI Record: {self.bimi_record.bimi_record}"
)

130
modules/report.py Normal file
View File

@@ -0,0 +1,130 @@
# modules/report.py
import os
import pandas as pd
from colorama import init, Fore, Style
# Initialize colorama
init()
def output_message(symbol, message, level="info"):
"""Generic function to print messages with different colors and symbols based on the level."""
colors = {
"good": Fore.GREEN + Style.BRIGHT,
"warning": Fore.YELLOW + Style.BRIGHT,
"bad": Fore.RED + Style.BRIGHT,
"indifferent": Fore.BLUE + Style.BRIGHT,
"error": Fore.RED + Style.BRIGHT + "!!! ",
"info": Fore.WHITE + Style.BRIGHT,
}
color = colors.get(level, Fore.WHITE + Style.BRIGHT)
print(color + f"{symbol} {message}" + Style.RESET_ALL)
def write_to_excel(data, file_name="output.xlsx"):
"""Writes a DataFrame of data to an Excel file, appending if the file exists."""
if os.path.exists(file_name) and os.path.getsize(file_name) > 0:
existing_df = pd.read_excel(file_name)
new_df = pd.DataFrame(data)
combined_df = pd.concat([existing_df, new_df])
combined_df.to_excel(file_name, index=False)
else:
pd.DataFrame(data).to_excel(file_name, index=False)
def printer(**kwargs):
"""Utility function to print the results of DMARC, SPF, and BIMI checks in the original format."""
domain = kwargs.get("DOMAIN")
subdomain = kwargs.get("DOMAIN_TYPE") == "subdomain"
dns_server = kwargs.get("DNS_SERVER")
spf_record = kwargs.get("SPF")
spf_all = kwargs.get("SPF_MULTIPLE_ALLS")
spf_dns_query_count = kwargs.get("SPF_NUM_DNS_QUERIES")
dmarc_record = kwargs.get("DMARC")
p = kwargs.get("DMARC_POLICY")
pct = kwargs.get("DMARC_PCT")
aspf = kwargs.get("DMARC_ASPF")
sp = kwargs.get("DMARC_SP")
fo = kwargs.get("DMARC_FORENSIC_REPORT")
rua = kwargs.get("DMARC_AGGREGATE_REPORT")
bimi_record = kwargs.get("BIMI_RECORD")
vbimi = kwargs.get("BIMI_VERSION")
location = kwargs.get("BIMI_LOCATION")
authority = kwargs.get("BIMI_AUTHORITY")
spoofable = kwargs.get("SPOOFING_POSSIBLE")
spoofing_type = kwargs.get("SPOOFING_TYPE")
output_message("[*]", f"Domain: {domain}", "indifferent")
output_message("[*]", f"Is subdomain: {subdomain}", "indifferent")
output_message("[*]", f"DNS Server: {dns_server}", "indifferent")
if spf_record:
output_message("[*]", f"SPF record: {spf_record}", "info")
if spf_all is None:
output_message("[*]", "SPF does not contain an `All` item.", "info")
elif spf_all == "2many":
output_message(
"[?]", "SPF record contains multiple `All` items.", "warning"
)
else:
output_message("[*]", f"SPF all record: {spf_all}", "info")
output_message(
"[*]",
f"SPF DNS query count: {spf_dns_query_count}"
if spf_dns_query_count <= 10
else f"Too many SPF DNS query lookups {spf_dns_query_count}.",
"info",
)
else:
output_message("[?]", "No SPF record found.", "warning")
if dmarc_record:
output_message("[*]", f"DMARC record: {dmarc_record}", "info")
output_message(
"[*]", f"Found DMARC policy: {p}" if p else "No DMARC policy found.", "info"
)
output_message(
"[*]", f"Found DMARC pct: {pct}" if pct else "No DMARC pct found.", "info"
)
output_message(
"[*]",
f"Found DMARC aspf: {aspf}" if aspf else "No DMARC aspf found.",
"info",
)
output_message(
"[*]",
f"Found DMARC subdomain policy: {sp}"
if sp
else "No DMARC subdomain policy found.",
"info",
)
output_message(
"[*]",
f"Forensics reports will be sent: {fo}"
if fo
else "No DMARC forensics report location found.",
"indifferent",
)
output_message(
"[*]",
f"Aggregate reports will be sent to: {rua}"
if rua
else "No DMARC aggregate report location found.",
"indifferent",
)
else:
output_message("[?]", "No DMARC record found.", "warning")
if bimi_record:
output_message("[*]", f"BIMI record: {bimi_record}", "info")
output_message("[*]", f"BIMI version: {vbimi}", "info")
output_message("[*]", f"BIMI location: {location}", "info")
output_message("[*]", f"BIMI authority: {authority}", "info")
if spoofing_type:
level = "good" if spoofable else "bad"
symbol = "[+]" if level == "good" else "[-]"
output_message(symbol, spoofing_type, level)
print() # Padding

102
modules/spf.py Normal file
View File

@@ -0,0 +1,102 @@
# modules/spf.py
import dns.resolver
import re
class SPF:
def __init__(self, domain, dns_server=None):
self.domain = domain
self.dns_server = dns_server
self.spf_record = self.get_spf_record()
self.all_mechanism = None
self.spf_dns_query_count = 0
self.too_many_dns_queries = False
if self.spf_record:
self.all_mechanism = self.get_spf_all_string()
self.spf_dns_query_count = self.get_spf_dns_queries()
self.too_many_dns_queries = self.spf_dns_query_count > 10
def get_spf_record(self, domain=None):
"""Fetches the SPF record for the specified domain."""
try:
if not domain:
domain = self.domain
resolver = dns.resolver.Resolver()
resolver.nameservers = [self.dns_server, "1.1.1.1", "8.8.8.8"]
query_result = resolver.resolve(domain, "TXT")
for record in query_result:
if "spf1" in str(record):
spf_record = str(record).replace('"', "")
return spf_record
return None
except Exception:
return None
def get_spf_all_string(self):
"""Returns the string value of the 'all' mechanism in the SPF record."""
spf_record = self.spf_record
visited_domains = set()
while spf_record:
all_matches = re.findall(r"[-~?+]all", spf_record)
if len(all_matches) == 1:
return all_matches[0]
elif len(all_matches) > 1:
return "2many"
redirect_match = re.search(r"redirect=([\w.-]+)", spf_record)
if redirect_match:
redirect_domain = redirect_match.group(1)
if redirect_domain in visited_domains:
break # Prevent infinite loops in case of circular redirects
visited_domains.add(redirect_domain)
spf_record = self.get_spf_record(redirect_domain)
else:
break
return None
def get_spf_dns_queries(self):
"""Returns the number of dns queries, redirects, and other mechanisms in the SPF record for a given domain."""
def count_dns_queries(spf_record):
count = 0
for item in spf_record.split():
if item.startswith("include:") or item.startswith("redirect="):
if item.startswith("include:"):
url = item.replace("include:", "")
elif item.startswith("redirect="):
url = item.replace("redirect=", "")
count += 1
try:
# Recursively fetch and count dns queries or redirects in the SPF record of the referenced domain
answers = dns.resolver.resolve(url, "TXT")
for rdata in answers:
for txt_string in rdata.strings:
txt_record = txt_string.decode("utf-8")
if txt_record.startswith("v=spf1"):
count += count_dns_queries(txt_record)
except Exception:
pass
# Count occurrences of 'a', 'mx', 'ptr', and 'exists' mechanisms
count += len(re.findall(r"[ ,+]a[ ,:]", spf_record))
count += len(re.findall(r"[ ,+]mx[ ,:]", spf_record))
count += len(re.findall(r"[ ]ptr[ ]", spf_record))
count += len(re.findall(r"exists[:]", spf_record))
return count
return count_dns_queries(self.spf_record)
def __str__(self):
return (
f"SPF Record: {self.spf_record}\n"
f"All Mechanism: {self.all_mechanism}\n"
f"DNS Query Count: {self.spf_dns_query_count}\n"
f"Too Many DNS Queries: {self.too_many_dns_queries}"
)

187
modules/spoofing.py Normal file
View File

@@ -0,0 +1,187 @@
# modules/spoofing.py
import tldextract
from .syntax import validate_record_syntax
class Spoofing:
def __init__(
self,
domain,
dmarc_record,
p,
aspf,
spf_record,
spf_all,
spf_dns_queries,
sp,
pct,
):
self.domain = domain
self.dmarc_record = dmarc_record
self.p = p
self.aspf = aspf
self.spf_record = spf_record
self.spf_all = spf_all
self.spf_dns_queries = spf_dns_queries
self.sp = sp
self.pct = pct
self.domain_type = self.get_domain_type()
self.spoofable = self.is_spoofable()
self.spoofing_possible, self.spoofing_type = self.evaluate_spoofing()
def get_domain_type(self):
"""Determines whether the domain is a domain or subdomain."""
subdomain = bool(tldextract.extract(self.domain).subdomain)
return "subdomain" if subdomain else "domain"
def is_spoofable(self):
"""Determines the spoofability based on DMARC and SPF data."""
try:
if self.pct and int(self.pct) != 100:
return 3
elif self.spf_record is None:
if self.p is None:
return 0
elif self.p == "none":
return 4
else:
return 8
elif self.spf_dns_queries > 10 and self.p is None:
return 0
elif self.spf_all == "2many":
if self.p == "none":
return 3
else:
return 8
elif self.spf_all and self.p is None:
return 0
elif self.spf_all == "-all":
if self.p and self.aspf and self.sp == "none":
return 1
elif self.aspf is None and self.sp == "none":
return 1
elif (
self.p == "none"
and (self.aspf == "r" or self.aspf is None)
and self.sp is None
):
return 4
elif (
self.p == "none"
and self.aspf == "r"
and (self.sp == "reject" or self.sp == "quarantine")
):
return 2
elif (
self.p == "none"
and self.aspf is None
and (self.sp == "reject" or self.sp == "quarantine")
):
return 5
elif self.p == "none" and self.aspf is None and self.sp == "none":
return 7
else:
return 8
elif self.spf_all == "~all":
if self.p == "none" and self.sp == "reject" or self.sp == "quarantine":
return 2
elif self.p == "none" and self.sp is None:
return 0
elif self.p == "none" and self.sp == "none":
return 7
elif (
(self.p == "reject" or self.p == "quarantine")
and self.aspf is None
and self.sp == "none"
):
return 1
elif (
(self.p == "reject" or self.p == "quarantine")
and self.aspf
and self.sp == "none"
):
return 1
else:
return 8
elif self.spf_all == "?all":
if (
(self.p == "reject" or self.p == "quarantine")
and self.aspf
and self.sp == "none"
):
return 6
elif (
(self.p == "reject" or self.p == "quarantine")
and self.aspf is None
and self.sp == "none"
):
return 6
elif self.p == "none" and self.aspf == "r" and self.sp is None:
return 0
elif self.p == "none" and self.aspf == "r" and self.sp == "none":
return 7
elif (
self.p == "none" and self.aspf == "s" or None and self.sp == "none"
):
return 7
elif self.p == "none" and self.aspf == "s" or None and self.sp is None:
return 6
elif (
self.p == "none"
and self.aspf
and (self.sp == "reject" or self.sp == "quarantine")
):
return 5
elif self.p == "none" and self.aspf is None and self.sp == "reject":
return 5
else:
return 8
else:
return 8
except Exception:
# If you are here, this means you caught a domain with a syntax error!
spf_valid = validate_record_syntax(self.spf_record, "SPF")
dmarc_valid = validate_record_syntax(self.dmarc_record, "DMARC")
if (not spf_valid and not dmarc_valid) or (spf_valid and not dmarc_valid):
return 0
elif not spf_valid and dmarc_valid and self.p == "none":
return 3
else:
return 8
def evaluate_spoofing(self):
"""Evaluates and returns whether spoofing is possible and the type of spoofing."""
spoofing_types = {
0: f"Spoofing possible for {self.domain}.",
1: f"Subdomain spoofing possible for {self.domain}.",
2: f"Organizational domain spoofing possible for {self.domain}.",
3: f"Spoofing might be possible for {self.domain}.",
4: f"Spoofing might be possible (Mailbox dependent) for {self.domain}.",
5: f"Organizational domain spoofing might be possible for {self.domain}.",
6: f"Subdomain spoofing might be possible (Mailbox dependent) for {self.domain}.",
7: f"Subdomain spoofing is possible and organizational domain spoofing might be possible for {self.domain}.",
8: f"Spoofing is not possible for {self.domain}.",
}
spoofing_type = spoofing_types.get(
self.spoofable, f"Unknown spoofing type for {self.domain}."
)
if self.spoofable in {0, 1, 3, 7}:
spoofing_possible = True
elif self.spoofable == 8:
spoofing_possible = False
else:
spoofing_possible = None # "maybe"
return spoofing_possible, spoofing_type
def __str__(self):
return (
f"Domain: {self.domain}\n"
f"Domain Type: {self.domain_type}\n"
f"Spoofing Possible: {self.spoofing_possible}\n"
f"Spoofing Type: {self.spoofing_type}"
)

96
modules/syntax.py Normal file
View File

@@ -0,0 +1,96 @@
# modules/syntax.py
import re
def validate_record_syntax(record, record_type):
"""Validate the syntax of a DNS record (SPF or DMARC)."""
if record_type == "SPF":
mechanism_patterns = {
"all": r"^all$",
"include": r"^include:[\w\.\-]+\.[a-zA-Z]{2,}$",
"a": r"^a(:[\w\.\-]+)?$",
"mx": r"^mx(:[\w\.\-]+)?$",
"ptr": r"^ptr(:[\w\.\-]+)?$",
"ip4": r"^ip4:(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$",
"ip6": r"^ip6:[a-fA-F0-9:]+(\/\d{1,3})?$",
"exists": r"^exists:[\w\.\-]+\.[a-zA-Z]{2,}$",
}
modifier_patterns = {
"redirect": r"^redirect=[\w\.\-]+\.[a-zA-Z]{2,}$",
"exp": r"^exp=[\w\.\-]+\.[a-zA-Z]{2,}$",
}
elements = record.split()
if len(elements) == 0 or elements[0].strip().lower() != "v=spf1":
return False
for element in elements[1:]:
element = element.strip()
if ":" in element:
mechanism, value = element.split(":", 1)
elif "=" in element:
modifier, value = element.split("=", 1)
mechanism = modifier
else:
mechanism = element
value = None
if mechanism in mechanism_patterns:
pattern = mechanism_patterns[mechanism]
if value:
if not re.match(pattern, element):
return False
elif not re.match(r"^" + mechanism + r"$", element):
return False
elif mechanism in modifier_patterns:
pattern = modifier_patterns[mechanism]
if not re.match(pattern, element):
return False
else:
return False
elif record_type == "DMARC":
tag_patterns = {
"v": r"^DMARC1$",
"p": r"^(none|quarantine|reject)$",
"sp": r"^(none|quarantine|reject)$",
"pct": r"^(100|[1-9]?[0-9])$",
"rua": r"^[\w\.\-]+@[\w\.\-]+\.[a-zA-Z]{2,}$",
"ruf": r"^[\w\.\-]+@[\w\.\-]+\.[a-zA-Z]{2,}$",
"rf": r"^(afrf)$",
"fo": r"^(0|1|d|s)$",
"ri": r"^\d+$",
"aspf": r"^(r|s)$",
"adkim": r"^(r|s)$",
}
tags = record.split(";")
if len(tags) < 2 or not tags[0].strip().startswith("v=DMARC1"):
return False
for tag in tags:
tag = tag.strip()
if not tag:
continue
tag_key_value = tag.split("=")
if len(tag_key_value) != 2:
return False
tag_key, tag_value = tag_key_value
tag_key = tag_key.lower().strip()
tag_value = tag_value.strip()
if tag_key in tag_patterns:
if not re.match(tag_patterns[tag_key], tag_value):
return False
else:
return False
else:
return False
return True

215
spoofy.py
View File

@@ -1,100 +1,153 @@
#! /usr/bin/env python3
# spoofy.py
import argparse
import tldextract
import threading
import os
from libs import bimi, dmarc, dns, logic, spf, report
from queue import Queue
from modules.dns import DNS
from modules.spf import SPF
from modules.dmarc import DMARC
from modules.bimi import BIMI
from modules.spoofing import Spoofing
from modules import 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,
and outputs the results to the console or an Excel file."""
try:
dns_server = spf_record = dmarc_record = None
spf_all = spf_includes = p = pct = aspf = sp = fo = rua = vbimi = location = authority = None
subdomain = bool(tldextract.extract(domain).subdomain)
with print_lock:
dns_server, spf_record, dmarc_record, bimi_record = dns.get_dns_server(domain)
if spf_record:
spf_all = spf.get_spf_all_string(spf_record)
spf_includes = spf.get_spf_includes(domain)
if dmarc_record:
p, pct, aspf, sp, fo, rua = dmarc.get_dmarc_details(dmarc_record)
if bimi_record:
vbimi, location, authority = bimi.get_bimi_details(bimi_record)
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,
'BIMI_RECORD': bimi_record,
'BIMI_VERSION': vbimi,
'BIMI_LOCATION': location,
'BIMI_AUTHORITY': authority,
'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, bimi_record, vbimi, location, authority, spoofable)
except Exception as e:
raise e
with print_lock:
report.output_error(
f"Domain {domain} is offline or format cannot be interpreted.")
def process_domain(domain):
"""Process a domain to gather DNS, SPF, DMARC, and BIMI records, and evaluate spoofing potential."""
dns_info = DNS(domain)
spf = SPF(domain, dns_info.dns_server)
dmarc = DMARC(domain, dns_info.dns_server)
bimi_info = BIMI(domain, dns_info.dns_server)
spf_record = spf.spf_record
spf_all = spf.all_mechanism
spf_dns_query_count = spf.spf_dns_query_count
spf_too_many_dns_queries = spf.too_many_dns_queries
dmarc_record = dmarc.dmarc_record
dmarc_p = dmarc.policy
dmarc_pct = dmarc.pct
dmarc_aspf = dmarc.aspf
dmarc_sp = dmarc.sp
dmarc_fo = dmarc.fo
dmarc_rua = dmarc.rua
bimi_record = bimi_info.bimi_record
bimi_version = bimi_info.version
bimi_location = bimi_info.location
bimi_authority = bimi_info.authority
spoofing_info = Spoofing(
domain,
dmarc_record,
dmarc_p,
dmarc_aspf,
spf_record,
spf_all,
spf_dns_query_count,
dmarc_sp,
dmarc_pct,
)
domain_type = spoofing_info.domain_type
spoofing_possible = spoofing_info.spoofing_possible
spoofing_type = spoofing_info.spoofing_type
result = {
"DOMAIN": domain,
"DOMAIN_TYPE": domain_type,
"DNS_SERVER": dns_info.dns_server,
"SPF": spf_record,
"SPF_MULTIPLE_ALLS": spf_all,
"SPF_NUM_DNS_QUERIES": spf_dns_query_count,
"SPF_TOO_MANY_DNS_QUERIES": spf_too_many_dns_queries,
"DMARC": dmarc_record,
"DMARC_POLICY": dmarc_p,
"DMARC_PCT": dmarc_pct,
"DMARC_ASPF": dmarc_aspf,
"DMARC_SP": dmarc_sp,
"DMARC_FORENSIC_REPORT": dmarc_fo,
"DMARC_AGGREGATE_REPORT": dmarc_rua,
"BIMI_RECORD": bimi_record,
"BIMI_VERSION": bimi_version,
"BIMI_LOCATION": bimi_location,
"BIMI_AUTHORITY": bimi_authority,
"SPOOFING_POSSIBLE": spoofing_possible,
"SPOOFING_TYPE": spoofing_type,
}
return result
def process_domains(domains, output):
"""
This function is for multithreading woot woot!
"""
threads = []
def worker(domain_queue, print_lock, output, results):
"""Worker function to process domains and output results."""
while True:
domain = domain_queue.get()
if domain is None:
break
result = process_domain(domain)
with print_lock:
if output == "stdout":
report.printer(**result)
else:
results.append(result)
domain_queue.task_done()
def main():
parser = argparse.ArgumentParser(
description="Process domains to gather DNS, SPF, DMARC, and BIMI records."
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-d", type=str, help="Single domain to process.")
group.add_argument(
"-iL", type=str, help="File containing a list of domains to process."
)
parser.add_argument(
"-o",
type=str,
choices=["stdout", "xls"],
default="stdout",
help="Output format: stdout or xls (default: stdout).",
)
parser.add_argument(
"-t", type=int, default=4, help="Number of threads to use (default: 4)"
)
args = parser.parse_args()
if args.d:
domains = [args.d]
elif args.iL:
with open(args.iL, "r") as file:
domains = [line.strip() for line in file]
domain_queue = Queue()
results = []
for domain in domains:
thread = threading.Thread(target=process_domain, args=(domain, output))
domain_queue.put(domain)
threads = []
for _ in range(min(args.t, len(domains))):
thread = threading.Thread(
target=worker, args=(domain_queue, print_lock, args.o, results)
)
thread.start()
threads.append(thread)
domain_queue.join()
if args.o == "xls" and results:
report.write_to_excel(results)
print("Results written to output.xlsx")
for _ in range(len(threads)):
domain_queue.put(None)
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 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()
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.")
if options.d:
domains.append(options.d)
process_domains(domains, options.o)
main()

141
test.py
View File

@@ -1,49 +1,134 @@
import unittest
from libs import logic
from modules.spoofing import Spoofing
class TestSpoofy(unittest.TestCase):
'''
0: Indicates that spoofing is possible for the domain.
1: Indicates that subdomain spoofing is possible for the domain.
2: Indicates that organizational domain spoofing is possible for the domain.
3: Indicates that spoofing might be possible for the domain.
4: Indicates that spoofing might be possible (mailbox dependent) for the domain.
5: Indicates that organizational domain spoofing may be possible for the domain.
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.
'''
def test_spoofing_is_possible(self):
unittest.TestCase().assertEqual(logic.is_spoofable('test_0.com', 'none', 'r', 'v=spf1 include:fake.gov', '~all', 3, None, 100), 0)
spoofing = Spoofing(
domain="test_0.com",
dmarc_record="v=DMARC1; p=none;",
p="none",
aspf="r",
spf_record="v=spf1 include:fake.gov",
spf_all="~all",
spf_dns_queries=3,
sp=None,
pct=100,
)
self.assertEqual(spoofing.spoofable, 0)
def test_subdomain_spoofing(self):
unittest.TestCase().assertEqual(logic.is_spoofable("test_1.com", 'reject', None, 'v=spf1 include:fakest.domain.com', '-all', 3, 'none', None), 1)
spoofing = Spoofing(
domain="test_1.com",
dmarc_record="v=DMARC1; p=reject;",
p="none",
aspf=None,
spf_record="v=spf1 include:fakest.domain.com",
spf_all="-all",
spf_dns_queries=3,
sp="none",
pct=100,
)
self.assertEqual(spoofing.spoofable, 1)
def test_organizational_domain_spoofing(self):
unittest.TestCase().assertEqual(logic.is_spoofable('test_2.com', 'none', 'r', 'v=spf1 include:fakest.domain.com include:faker.domain.com', '-all', 2, 'reject', 100), 2)
spoofing = Spoofing(
domain="test_2.com",
dmarc_record="v=DMARC1; p=none;",
p="none",
aspf="r",
spf_record="v=spf1 include:fakest.domain.com include:faker.domain.com",
spf_all="-all",
spf_dns_queries=2,
sp="reject",
pct=100,
)
self.assertEqual(spoofing.spoofable, 2)
def test_spoofing_might_be_possible(self):
unittest.TestCase().assertEqual(logic.is_spoofable('test_3.com', 'none', None, 'v=spf1 include:fakest.domain.com', '~all', 1, 'quarantine', 90), 3)
spoofing = Spoofing(
domain="test_3.com",
dmarc_record="v=DMARC1; p=none;",
p="none",
aspf=None,
spf_record="v=spf1 include:fakest.domain.com",
spf_all="~all",
spf_dns_queries=1,
sp="quarantine",
pct=90,
)
self.assertEqual(spoofing.spoofable, 3)
def test_spoofing_might_be_possible_mbd(self):
unittest.TestCase().assertEqual(logic.is_spoofable('test_4.com', 'none', None, 'v=spf1 include:fakest.domain.com', '-all', 1, None, 100), 4)
spoofing = Spoofing(
domain="test_4.com",
dmarc_record="v=DMARC1; p=none;",
p="none",
aspf=None,
spf_record="v=spf1 include:fakest.domain.com",
spf_all="-all",
spf_dns_queries=1,
sp=None,
pct=100,
)
self.assertEqual(spoofing.spoofable, 4)
def test_org_domain_spoofing_might_be_possible(self):
unittest.TestCase().assertEqual(logic.is_spoofable('test_5.com', 'none', None, 'v=spf1 include:fakest.domain.com', '-all', 1, 'reject', 100), 5)
spoofing = Spoofing(
domain="test_5.com",
dmarc_record="v=DMARC1; p=none;",
p="none",
aspf=None,
spf_record="v=spf1 include:fakest.domain.com",
spf_all="-all",
spf_dns_queries=1,
sp="reject",
pct=100,
)
self.assertEqual(spoofing.spoofable, 5)
def test_subdomain_spoofing_might_be_possible_mbd(self):
unittest.TestCase().assertEqual(logic.is_spoofable('test_6.com', 'reject', 'r', 'v=spf1 include:fakest.domain.com', '?all', 1, 'none', 100), 6)
spoofing = Spoofing(
domain="test_6.com",
dmarc_record="v=DMARC1; p=reject;",
p="reject",
aspf="r",
spf_record="v=spf1 include:fakest.domain.com",
spf_all="?all",
spf_dns_queries=1,
sp="none",
pct=100,
)
self.assertEqual(spoofing.spoofable, 6)
def test_subdomain_spoofing_and_org_spoofing_might_be_possible(self):
unittest.TestCase().assertEqual(logic.is_spoofable('test_7.com', 'none', None, 'v=spf1 include:fakest.domain.com', '~all', 3, 'none', 100), 7)
spoofing = Spoofing(
domain="test_7.com",
dmarc_record="v=DMARC1; p=none;",
p="none",
aspf=None,
spf_record="v=spf1 include:fakest.domain.com",
spf_all="~all",
spf_dns_queries=3,
sp="none",
pct=100,
)
self.assertEqual(spoofing.spoofable, 7)
def test_spoofing_not_possible(self):
unittest.TestCase().assertEqual(logic.is_spoofable('test_8.com', 'none', 's', 'v=spf1 include:fakest.domain.com', '~all', 1, 'quarantine', 100), 8)
spoofing = Spoofing(
domain="test_8.com",
dmarc_record="v=DMARC1; p=none;",
p="none",
aspf="s",
spf_record="v=spf1 include:domain.com",
spf_all="-all",
spf_dns_queries=1,
sp="reject",
pct=100,
)
self.assertEqual(spoofing.spoofable, 8)
def test_possible_bug_fix1(self):
unittest.TestCase().assertEqual(logic.is_spoofable('sub.test_9.com', None, None, None, None, None, None, None), 0)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()