pdknockr

- a passive dns drive-by tool 🏎️💨
git clone git://git.acid.vegas/-c.git
Log | Files | Refs | Archive | README

commit a69ba1cbc8d50d17a19a7799895ba1c18c275522
parent a3e5a3fef77ab7e9178bce8d72299829115fe92c
Author: acidvegas <acid.vegas@acid.vegas>
Date: Sat, 10 Feb 2024 02:17:19 -0500

Memory efficency attained via generators for handling larger input files. Code cleaned up, etc

Diffstat:
MREADME.md | 12++++--------
Mpdknockr.py | 201++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Risp_dns.txt -> resolvers/isp_dns.txt | 0
Rvpn_dns.txt -> resolvers/vpn_dns.txt | 0
Rrandom_subdomains.txt -> subdomains.txt | 0

5 files changed, 117 insertions(+), 96 deletions(-)

diff --git a/README.md b/README.md
@@ -16,21 +16,17 @@ Th program will start firing off DNS queries to all the resolvers using the cust
 
 ![](.screens/preview.png)
 
-After testing across multiple IP addresses over time, if we ever see `download.event.supernets.org` show up on any passive DNS lookup engines, we can simple use the following command:
-
-```bash
-jq 'to_entries | map({key: .value, value: .key}) | from_entries | ."download.event"' dns_keys.txt
-```
-
-This will return `151.202.0.84`, marking it as a DNS server that is actively logging all DNS queries that pass through.
+After testing across multiple IP addresses over time, if we ever see `download.event.supernets.org` show up on any passive DNS lookup engines, refer to our logs, which will show it was looked up on `151.202.0.84`, marking it as a DNS server that is actively logging all DNS queries that pass through.
 
 
 ## WORK IN PROGRESS (STAY TUNED)
 
 - [ ] Bind server running accepting wildcard DNS lookups on custom domain.
+- [ ] DNS-over-TLS *(DoT)* and DNS-over-HTTPS *(DoH)* support
 - [X] Hunt down specific DNS servers used by ISP's from an ASN lookup
-- [ ] Any way to apply this to custom DNS servers used by VPNs?
+- [X] Any way to apply this to custom DNS servers used by VPNs?
 - [X] Noise generator to abuse known logging servers.
+- [X] Memory effiency attains via yielding generators to handle large input files
 
 This is all very theoretical right now, interested to see how this pans out.
 
diff --git a/pdknockr.py b/pdknockr.py
@@ -1,10 +1,11 @@
 #!/usr/bin/env python
 # Passive DNS Knocker (PDK) - developed by acidvegas in python (https://git.acid.vegas/pdknockr)
 
+import argparse
 import asyncio
-import json
 import logging
 import logging.handlers
+import os
 import random
 import time
 
@@ -14,7 +15,7 @@ except ImportError:
     raise SystemExit('missing required \'aiodns\' module (pip install aiodns)')
 
 
-async def dns_lookup(domain: str, subdomain: str, dns_server: str, dns_type: str, timeout: int, semaphore: asyncio.Semaphore):
+async def dns_lookup(semaphore: asyncio.Semaphore, domain: str, dns_server: str, record_type: str, timeout: int):
     '''
     Perform a DNS lookup on a target domain.
     
@@ -26,115 +27,139 @@ async def dns_lookup(domain: str, subdomain: str, dns_server: str, dns_type: str
     :param semaphore: The semaphore to use for concurrency.
     '''
     async with semaphore:
-        target = f'{subdomain}.{domain}'
         resolver = aiodns.DNSResolver(nameservers=[dns_server], timeout=timeout)
-        logging.info(f'\033[96mKnocking {target}\033[0m on \033[93m{dns_server}\033[0m (\033[90m{dns_type}\033[0m)')
-        try:
-            await resolver.query(target, dns_type)
-        except Exception as e:
-            pass
 
+        logging.info(f'Knocking {dns_server} with {domain} ({record_type})')
 
-def generate_subdomain(sub_domains: list) -> str:
+        try:
+            await resolver.query(domain, record_type)
+        except:
+            pass # We're just knocking so errors are expected and ignored
+        
+        
+def read_domain_file(file_path: str):
     '''
-    Generate a random subdomain.
-
-    :param sub_domains: The list of subdomains to use.
+    Generator function to read domains line by line.
+    
+    :param file_path: The path to the file containing the DNS servers.
     '''
-    chosen_domains = random.sample(sub_domains, 2)
-    if random.choice([True, False]):
-        chosen_index = random.choice([0, 1])
-        chosen_domains[chosen_index] =  chosen_domains[chosen_index] + str(random.randint(1, 99))
-    return random.choice(['.', '-']).join(chosen_domains)
+    with open(file_path, 'r') as file:
+        while True:
+            for line in file:
+                line = line.strip()
+                if line:
+                    yield line
 
-    
-async def main(args):
+
+def read_dns_file(file_path: str):
     '''
-    Main function for the program.
+    Generator function to read DNS servers line by line.
     
-    :param args: The arguments passed to the program.
+    :param file_path: The path to the file containing the DNS servers.
     '''
-    global dns_keys
+    with open(file_path, 'r') as file:
+        while True:
+            for line in file:
+                line = line.strip()
+                if line:
+                    yield line
 
-    semaphore = asyncio.BoundedSemaphore(args.concurrency)
-    tasks = []
 
+def generate_subdomain(sub_domains: list, domain: str, max_size: int):
+    '''
+    Generator function to read subdomains line by line.
+    
+    :param sub_domains: The list of subdomains to use for generating noise.
+    '''
     while True:
-        for domain in args.domains.split(','):
-            for dns_server in dns_keys:
-                if len(tasks) < args.concurrency:
-                    query_record = random.choice(args.rectype)
-                    task = asyncio.create_task(dns_lookup(domain, dns_keys[dns_server], dns_server, query_record, args.timeout, semaphore))
-                    tasks.append(task)
-                else:
-                    done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
-                    tasks = list(pending)
-        if not args.noise:
-            break
+        subs = random.sample(sub_domains, random.randint(2, max_size))
 
+        if random.choice([True, False]):
+            subs_index = random.randint(0, max_size - 1)
+            subs[subs_index] = subs[subs_index] + str(random.randint(1, 99))
 
+        yield random.choice(['.', '-']).join(subs) + '.' + domain
 
-if __name__ == '__main__':
-    import argparse
-    import os
-    import urllib.request
 
-    parser = argparse.ArgumentParser(description='Passive DNS Knocking Tool')
-    parser.add_argument('-d', '--domains', help='Comma seperate list of domains or file containing list of domains')
-    #parser.add_argument('-s', '--subdomain', help='Subdomain to look up')
-    parser.add_argument('-c', '--concurrency', type=int, default=50, help='Concurrency limit (default: 50)')
-    parser.add_argument('-r', '--resolvers', help='File containing list of DNS resolvers (uses public-dns.info if not specified)')
-    parser.add_argument('-rt', '--rectype', default='A,AAAA', help='Comma-seperated list of  DNS record type (default: A)')
-    parser.add_argument('-t', '--timeout', type=int, default=3, help='Timeout for DNS lookup (default: 3)')
-    parser.add_argument('-n', '--noise', action='store_true', help='Enable random subdomain noise')
-    args = parser.parse_args()
+def setup_logging():
+    '''Setup the logging for the program.'''
+
+    os.makedirs('logs', exist_ok=True)
 
     sh = logging.StreamHandler()
     sh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%I:%M %p'))
-    os.makedirs('logs', exist_ok=True)
+
     log_filename = time.strftime('pdk_%Y-%m-%d_%H-%M-%S.log')
-    fh = logging.handlers.RotatingFileHandler(f'logs/{log_filename}.log', maxBytes=2500000, backupCount=3, encoding='utf-8')
-    fh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(filename)s.%(funcName)s.%(lineno)d | %(message)s', '%Y-%m-%d %I:%M %p')) 
+
+    fh = logging.handlers.RotatingFileHandler(f'logs/{log_filename}', maxBytes=268435456, encoding='utf-8')
+    fh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%Y-%m-%d %I:%M %p')) 
+
     logging.basicConfig(level=logging.NOTSET, handlers=(sh,fh))
 
+
+async def main():
+    '''Main function for the program.'''
+    
+    parser = argparse.ArgumentParser(description='Passive DNS Knocking Tool')
+    parser.add_argument('-d', '--domains', help='Comma seperate list of domains or file containing list of domains')
+    parser.add_argument('-s', '--subdomains', help='File containing list of subdomains')
+    parser.add_argument('-r', '--resolvers', help='File containing list of DNS resolvers')
+    parser.add_argument('-rt', '--rectype', default='A,AAAA', help='Comma-seperated list of DNS record type (default: A,AAAA)')
+    parser.add_argument('-c', '--concurrency', type=int, default=25, help='Concurrency limit (default: 50)')
+    parser.add_argument('-t', '--timeout', type=int, default=3, help='Timeout for DNS lookup (default: 3)')
+    parser.add_argument('-n', '--noise', action='store_true', help='Enable random subdomain noise')
+    args = parser.parse_args()
+    
+    setup_logging()
+    
+    args.rectype = [record_type.upper() for record_type in args.rectype.split(',')]
+    
     if not args.domains:
         raise SystemExit('no domains specified')
+    elif not os.path.exists(args.domains):
+        raise FileNotFoundError('domains file not found')
     
-    if args.rectype:
-        valid_record_types = ('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SOA', 'SRV', 'TXT')
-        if ',' in args.rectype:
-            args.rectype = args.rectype.split(',')
-            for record_type in args.rectype:
-                if record_type not in valid_record_types:
-                    logging.fatal('invalid record type')
-        elif args.rectype not in valid_record_types:
-            logging.fatal('invalid record type')
-        else:
-            args.rectype = [args.rectype]
+    if not args.subdomains:
+        raise SystemExit('no subdomains file specified')
+    elif not os.path.exists(args.subdomains):
+        raise FileNotFoundError('subdomains file not found')
     
-    if args.resolvers:
-        if os.path.exists(args.resolvers):
-            with open(args.resolvers, 'r') as file:
-                dns_servers = [item.strip() for item in file.readlines() if item.strip()]
-                logging.info(f'Loaded {len(dns_servers):,} DNS servers from file')
-        else:
-            logging.fatal('DNS servers file does not exist')
-    else:
-        dns_servers = urllib.request.urlopen('https://public-dns.info/nameservers.txt').read().decode().split('\n')
-        logging.info(f'Loaded {len(dns_servers):,} DNS servers from public-dns.info')
-
-    # Command line argument needed for this still
-    if os.path.exists('random_subdomains.txt'):
-        with open('random_subdomains.txt', 'r') as file:
-            sub_domains = [item.strip() for item in file.readlines() if item.strip()]
-            logging.info(f'Loaded {len(sub_domains):,} subdomains from file')
-    else:
-        logging.fatal('random_subdomains.txt is missing')
-   
-    dns_keys = dict()
-    for dns_server in dns_servers:
-        dns_keys[dns_server] = generate_subdomain(sub_domains)
-    with open('dns_keys.txt', 'w') as file:
-        json.dump(dns_keys, file)
+    if not args.resolvers:
+        raise SystemExit('no resolvers file specified')
+    elif not os.path.exists(args.resolvers):
+        raise FileNotFoundError('resolvers file not found')
+    
+    valid_record_types = ('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SOA', 'SRV', 'TXT')
+
+    for record_type in args.rectype:
+        if record_type not in valid_record_types:
+            raise SystemExit(f'invalid record type: {record_type}')
     
-    asyncio.run(main(args))
-\ No newline at end of file
+    semaphore = asyncio.BoundedSemaphore(args.concurrency)
+    
+    while True:
+        tasks = []        
+
+        for domain in read_domain_file(args.domains):
+
+            for dns_server in read_dns_file(args.resolvers):
+                sub_domain = generate_subdomain(args.subdomains, domain, 3)
+                
+                if len(tasks) < args.concurrency:
+                    query_record = random.choice(args.record_types)
+                    task = asyncio.create_task(dns_lookup(semaphore, domain, sub_domain, dns_server, query_record, args.timeout))
+                    tasks.append(task)
+
+                else:
+                    done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
+                    tasks = list(pending)
+                    
+        await asyncio.wait(tasks) # Wait for any remaining tasks to complete
+                    
+        if not args.noise:
+            break
+
+
+
+if __name__ == '__main__':
+    asyncio.run(main())
+\ No newline at end of file
diff --git a/isp_dns.txt b/resolvers/isp_dns.txt
diff --git a/vpn_dns.txt b/resolvers/vpn_dns.txt
diff --git a/random_subdomains.txt b/subdomains.txt