pdknockr

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

pdknockr.py (5943B)

      1 #!/usr/bin/env python
      2 # Passive DNS Knocker (PDK) - developed by acidvegas in python (https://git.acid.vegas/pdknockr)
      3 
      4 import argparse
      5 import asyncio
      6 import logging
      7 import logging.handlers
      8 import os
      9 import random
     10 import time
     11 
     12 try:
     13     import aiodns
     14 except ImportError:
     15     raise SystemExit('missing required \'aiodns\' module (pip install aiodns)')
     16 
     17 
     18 async def dns_lookup(semaphore: asyncio.Semaphore, domain: str, dns_server: str, record_type: str, timeout: int):
     19     '''
     20     Perform a DNS lookup on a target domain.
     21     
     22     :param domain: The target domain to perform the lookup on.
     23     :param subdomain: The subdomain to look up.
     24     :param dns_server: The DNS server to perform the lookup on.
     25     :param dns_type: The DNS record type to look up.
     26     :param timeout: The timeout for the DNS lookup.
     27     :param semaphore: The semaphore to use for concurrency.
     28     '''
     29     async with semaphore:
     30         resolver = aiodns.DNSResolver(nameservers=[dns_server], timeout=timeout)
     31 
     32         logging.info(f'Knocking {dns_server} with {domain} ({record_type})')
     33 
     34         try:
     35             await resolver.query(domain, record_type)
     36         except:
     37             pass # We're just knocking so errors are expected and ignored
     38         
     39         
     40 def read_domain_file(file_path: str):
     41     '''
     42     Generator function to read domains line by line.
     43     
     44     :param file_path: The path to the file containing the DNS servers.
     45     '''
     46     with open(file_path, 'r') as file:
     47         while True:
     48             for line in file:
     49                 line = line.strip()
     50                 if line:
     51                     yield line
     52 
     53 
     54 def read_dns_file(file_path: str):
     55     '''
     56     Generator function to read DNS servers line by line.
     57     
     58     :param file_path: The path to the file containing the DNS servers.
     59     '''
     60     with open(file_path, 'r') as file:
     61         while True:
     62             for line in file:
     63                 line = line.strip()
     64                 if line:
     65                     yield line
     66 
     67 
     68 def generate_subdomain(sub_domains: list, domain: str, max_size: int):
     69     '''
     70     Generator function to read subdomains line by line.
     71     
     72     :param sub_domains: The list of subdomains to use for generating noise.
     73     '''
     74     while True:
     75         subs = random.sample(sub_domains, random.randint(2, max_size))
     76 
     77         if random.choice([True, False]):
     78             subs_index = random.randint(0, max_size - 1)
     79             subs[subs_index] = subs[subs_index] + str(random.randint(1, 99))
     80 
     81         yield random.choice(['.', '-']).join(subs) + '.' + domain
     82 
     83 
     84 def setup_logging():
     85     '''Setup the logging for the program.'''
     86 
     87     os.makedirs('logs', exist_ok=True)
     88 
     89     sh = logging.StreamHandler()
     90     sh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%I:%M %p'))
     91 
     92     log_filename = time.strftime('pdk_%Y-%m-%d_%H-%M-%S.log')
     93 
     94     fh = logging.handlers.RotatingFileHandler(f'logs/{log_filename}', maxBytes=268435456, encoding='utf-8')
     95     fh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%Y-%m-%d %I:%M %p')) 
     96 
     97     logging.basicConfig(level=logging.NOTSET, handlers=(sh,fh))
     98 
     99 
    100 async def main():
    101     '''Main function for the program.'''
    102     
    103     parser = argparse.ArgumentParser(description='Passive DNS Knocking Tool')
    104     parser.add_argument('-d', '--domains', help='Comma seperate list of domains or file containing list of domains')
    105     parser.add_argument('-s', '--subdomains', help='File containing list of subdomains')
    106     parser.add_argument('-r', '--resolvers', help='File containing list of DNS resolvers')
    107     parser.add_argument('-rt', '--rectype', default='A,AAAA', help='Comma-seperated list of DNS record type (default: A,AAAA)')
    108     parser.add_argument('-c', '--concurrency', type=int, default=25, help='Concurrency limit (default: 50)')
    109     parser.add_argument('-t', '--timeout', type=int, default=3, help='Timeout for DNS lookup (default: 3)')
    110     parser.add_argument('-n', '--noise', action='store_true', help='Enable random subdomain noise')
    111     args = parser.parse_args()
    112     
    113     setup_logging()
    114     
    115     args.rectype = [record_type.upper() for record_type in args.rectype.split(',')]
    116     
    117     if not args.domains:
    118         raise SystemExit('no domains specified')
    119     elif not os.path.exists(args.domains):
    120         raise FileNotFoundError('domains file not found')
    121     
    122     if not args.subdomains:
    123         raise SystemExit('no subdomains file specified')
    124     elif not os.path.exists(args.subdomains):
    125         raise FileNotFoundError('subdomains file not found')
    126     
    127     if not args.resolvers:
    128         raise SystemExit('no resolvers file specified')
    129     elif not os.path.exists(args.resolvers):
    130         raise FileNotFoundError('resolvers file not found')
    131     
    132     valid_record_types = ('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SOA', 'SRV', 'TXT')
    133 
    134     for record_type in args.rectype:
    135         if record_type not in valid_record_types:
    136             raise SystemExit(f'invalid record type: {record_type}')
    137     
    138     semaphore = asyncio.BoundedSemaphore(args.concurrency)
    139     
    140     while True:
    141         tasks = []        
    142 
    143         for domain in read_domain_file(args.domains):
    144 
    145             for dns_server in read_dns_file(args.resolvers):
    146                 sub_domain = generate_subdomain(args.subdomains, domain, 3)
    147                 
    148                 if len(tasks) < args.concurrency:
    149                     query_record = random.choice(args.record_types)
    150                     task = asyncio.create_task(dns_lookup(semaphore, domain, sub_domain, dns_server, query_record, args.timeout))
    151                     tasks.append(task)
    152 
    153                 else:
    154                     done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
    155                     tasks = list(pending)
    156                     
    157         await asyncio.wait(tasks) # Wait for any remaining tasks to complete
    158                     
    159         if not args.noise:
    160             break
    161 
    162 
    163 
    164 if __name__ == '__main__':
    165     asyncio.run(main())