diff --git a/.gitignore b/.gitignore index d6cc938..13d7225 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ dmypy.json # Pyre type checker .pyre/ + +# temporary input file for testing DKIM +dom.txt diff --git a/modules/dkim.py b/modules/dkim.py new file mode 100644 index 0000000..6468473 --- /dev/null +++ b/modules/dkim.py @@ -0,0 +1,133 @@ +import re +import dns.resolver + + +class DKIM: + def __init__(self, domain, dns_server=None): + self.domain = domain + self.dns_server = dns_server + self.dkim_record = self.get_dkim_record() + # self.dkim_query = None + self.combined_txt_records = '' + self.dkim_result = None + + def get_dkim_record(self): + """Returns the DKIM record for a given domain.""" + self.combined_txt_records = '' + + selectors = [ + 'selector', + 'selector1', + 'selector2', + 'default', + 'dkim', + 'mail', + 's1', + 's2', + 's3', + 'key1', + 'key2', + 'key3', + 'k1', + 'k2', + 'k3', + 'zoho', + 'google', + 'googlemail', + 'protonmail', + 'fm1', + 'fm2', + 'fm3', + 'mandrill', + 'sendgrid', + 's1024', + 'm1', + 'm2', + 'ms', + 'amazonses', + 'zendesk1', + 'zendesk2', + 'everlytickey1', + 'everlytickey2', + 'litesrv', + 'sig1', + 'sm', + 'ctct1', + 'ctct2', + 'spop1024', + 'dk', + 'a1', + 'aweber_key_a', + 'aweber_key_b', + 'aweber_key_c', + 'cm', + 'clab1', + 'dkim1024', + 'e2ma-k1', + 'e2ma-k2', + 'e2ma-k3', + 'sable', + 'hs1', + 'hs2', + 'kl', + 'kl2', + 'mailjet', + 'mailpoet1', + 'mailpoet2', + 'm101', + 'm102', + 'ecm1', + 'nce2048', + 'smtp', + 'class', + 'smtpapi', + 'domk', + 'smtpout', + 'authsmtp', + 'proddkim', + 'testdkim', + 'primary', + 'ses', + 'yousendit', + 'ed-dkim', + 'publickey', + 'sasl', + 'qcdkim', + 'x', + 'm', + 'mikd', + 'private', + 'ei', + 'spop', + 'spop1024', + 'mxvault', + 'krs', + 'mailo', + 'pic', + 'mta', + 'email', + 'acdkim1' + ] + + for selector in selectors: + try: + resolver = dns.resolver.Resolver() + + # set OpenDNS for TXT/CNAME query + resolver.nameservers = ["208.67.222.222"] + self.dkim_result = resolver.resolve(f"{selector}._domainkey.{self.domain}", "TXT") + + if self.dkim_result != None: + if self.dkim_result.response.answer != None: # dkim_result.canonical_name not empty + for record in self.dkim_result.chaining_result.answer: + trim_record = str(record.strings[0][:128])+"...(trimmed)" + self.combined_txt_records += f"[*] {selector}._domainkey.{self.domain} -> {trim_record}" + "\r\n" + else: + continue + except: + continue + + if self.combined_txt_records != '': + return self.combined_txt_records + else: + return None diff --git a/modules/dns.py b/modules/dns.py index 68a75b5..e098f2d 100644 --- a/modules/dns.py +++ b/modules/dns.py @@ -5,6 +5,7 @@ from .spf import SPF from .dmarc import DMARC from .bimi import BIMI +from .dkim import DKIM class DNS: @@ -14,6 +15,7 @@ def __init__(self, domain): self.dns_server = None self.spf_record = None self.dmarc_record = None + self.dkim_record = None self.bimi_record = None self.get_soa_record() @@ -48,6 +50,7 @@ def get_dns_server(self): 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.dkim_record = DKIM(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 @@ -72,5 +75,6 @@ def __str__(self): 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"DKIM Record: {self.dkim_record.dkim_record}\n" f"BIMI Record: {self.bimi_record.bimi_record}" ) diff --git a/modules/report.py b/modules/report.py index 4116fa5..7b54a3c 100644 --- a/modules/report.py +++ b/modules/report.py @@ -48,6 +48,7 @@ def printer(**kwargs): sp = kwargs.get("DMARC_SP") fo = kwargs.get("DMARC_FORENSIC_REPORT") rua = kwargs.get("DMARC_AGGREGATE_REPORT") + dkim_record = kwargs.get("DKIM") bimi_record = kwargs.get("BIMI_RECORD") vbimi = kwargs.get("BIMI_VERSION") location = kwargs.get("BIMI_LOCATION") @@ -116,6 +117,11 @@ def printer(**kwargs): else: output_message("[?]", "No DMARC record found.", "warning") + if dkim_record: + output_message("[*]", f"DKIM selectors: \r\n{dkim_record}", "info") + else: + output_message("[?]", f"No known DKIM selectors enumerated on {domain}.", "warning") + if bimi_record: output_message("[*]", f"BIMI record: {bimi_record}", "info") output_message("[*]", f"BIMI version: {vbimi}", "info") diff --git a/spoofy.py b/spoofy.py index 701a714..fd91302 100755 --- a/spoofy.py +++ b/spoofy.py @@ -8,6 +8,7 @@ from modules.spf import SPF from modules.dmarc import DMARC from modules.bimi import BIMI +from modules.dkim import DKIM from modules.spoofing import Spoofing from modules import report @@ -15,10 +16,11 @@ def process_domain(domain): - """Process a domain to gather DNS, SPF, DMARC, and BIMI records, and evaluate spoofing potential.""" + """Process a domain to gather DNS, SPF, DMARC, BIMI records, enumerate known DKIM selectors, and evaluate spoofing potential.""" dns_info = DNS(domain) spf = SPF(domain, dns_info.dns_server) dmarc = DMARC(domain, dns_info.dns_server) + dkim = DKIM(domain, dns_info.dns_server) bimi_info = BIMI(domain, dns_info.dns_server) spf_record = spf.spf_record @@ -34,6 +36,8 @@ def process_domain(domain): dmarc_fo = dmarc.fo dmarc_rua = dmarc.rua + dkim_record = dkim.dkim_record + bimi_record = bimi_info.bimi_record bimi_version = bimi_info.version bimi_location = bimi_info.location @@ -70,6 +74,7 @@ def process_domain(domain): "DMARC_SP": dmarc_sp, "DMARC_FORENSIC_REPORT": dmarc_fo, "DMARC_AGGREGATE_REPORT": dmarc_rua, + "DKIM": dkim_record, "BIMI_RECORD": bimi_record, "BIMI_VERSION": bimi_version, "BIMI_LOCATION": bimi_location, @@ -97,7 +102,7 @@ def worker(domain_queue, print_lock, output, results): def main(): parser = argparse.ArgumentParser( - description="Process domains to gather DNS, SPF, DMARC, and BIMI records." + description="Process domains to gather DNS, SPF, DMARC, BIMI records and known DKIM selectors." ) group = parser.add_mutually_exclusive_group(required=True) group.add_argument("-d", type=str, help="Single domain to process.")