From 7e2b90a92b7988f12b690dfbba56a4e1fd793c31 Mon Sep 17 00:00:00 2001 From: acidvegas Date: Mon, 10 Feb 2025 01:30:30 -0500 Subject: [PATCH] Added customer resolvers to arguments --- README.md | 7 +++++++ httpz.py | 51 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c67bdd9..808176e 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ python httpz.py domains.txt [options] | `-mc CODES` | `--match-codes CODES` | Only show specific status codes *(comma-separated)* | | `-ec CODES` | `--exclude-codes CODES` | Exclude specific status codes *(comma-separated)* | | `-p` | `--progress` | Show progress counter | +| `-ax` | `--axfr` | Try AXFR transfer against nameservers | +| `-r FILE` | `--resolvers FILE` | File containing DNS resolvers *(one per line)* | ## Examples @@ -75,4 +77,9 @@ python httpz.py domains.txt -c 100 -o output.jsonl -j -all -to 10 -mc 200,301 -e Scan domains from stdin: ```bash cat domains.txt | python httpz.py - -c 100 -o output.jsonl -j -all -to 10 -mc 200,301 -ec 404,500 -p +``` + +Scan domains with custom resolvers and AXFR checks: +```bash +python httpz.py domains.txt -r resolvers.txt -ax -c 100 -o output.jsonl ``` \ No newline at end of file diff --git a/httpz.py b/httpz.py index effda08..50766bc 100644 --- a/httpz.py +++ b/httpz.py @@ -17,6 +17,7 @@ import os import dns.zone import dns.query import dns.resolver +import random try: import aiohttp @@ -88,17 +89,40 @@ def info(msg: str) -> None: logging.info(msg) -async def resolve_dns(domain: str, timeout: int = 5) -> tuple: +def load_resolvers(resolver_file: str = None) -> list: + ''' + Load DNS resolvers from file or return default resolvers + + :param resolver_file: Path to file containing resolver IPs + :return: List of resolver IPs + ''' + if resolver_file: + try: + with open(resolver_file) as f: + resolvers = [line.strip() for line in f if line.strip()] + if resolvers: + return resolvers + except Exception as e: + debug(f'Error loading resolvers from {resolver_file}: {str(e)}') + + # Default to Cloudflare, Google, Quad9 if no file or empty file + return ['1.1.1.1', '8.8.8.8', '9.9.9.9'] + + +async def resolve_dns(domain: str, timeout: int = 5, nameserver: str = None) -> tuple: ''' Resolve A, AAAA, and CNAME records for a domain :param domain: domain to resolve :param timeout: timeout in seconds + :param nameserver: specific nameserver to use :return: tuple of (ips, cname) ''' resolver = dns.asyncresolver.Resolver() resolver.lifetime = timeout + if nameserver: + resolver.nameservers = [nameserver] ips = [] cname = None @@ -212,7 +236,7 @@ async def get_cert_info(session: aiohttp.ClientSession, url: str) -> dict: return None -async def check_domain(session: aiohttp.ClientSession, domain: str, follow_redirects: bool = False, timeout: int = 5, check_axfr: bool = False) -> dict: +async def check_domain(session: aiohttp.ClientSession, domain: str, follow_redirects: bool = False, timeout: int = 5, check_axfr: bool = False, resolvers: list = None) -> dict: ''' Check a single domain for its status code, title, and body preview @@ -221,8 +245,12 @@ async def check_domain(session: aiohttp.ClientSession, domain: str, follow_redir :param follow_redirects: whether to follow redirects :param timeout: timeout in seconds :param check_axfr: whether to check for AXFR + :param resolvers: list of DNS resolvers to use ''' + # Pick random resolver for this domain + nameserver = random.choice(resolvers) if resolvers else None + if not domain.startswith(('http://', 'https://')): protocols = ['https://', 'http://'] base_domain = domain.rstrip('/') @@ -247,13 +275,15 @@ async def check_domain(session: aiohttp.ClientSession, domain: str, follow_redir 'tls' : None } - # Resolve DNS records - result['ips'], result['cname'] = await resolve_dns(base_domain, timeout) + # Resolve DNS records with chosen nameserver + result['ips'], result['cname'] = await resolve_dns(base_domain, timeout, nameserver) # After DNS resolution, add nameserver lookup: try: resolver = dns.asyncresolver.Resolver() resolver.lifetime = timeout + if nameserver: + resolver.nameservers = [nameserver] ns_records = await resolver.resolve(base_domain, 'NS') result['nameservers'] = [str(ns).rstrip('.') for ns in ns_records] except Exception as e: @@ -452,7 +482,7 @@ def format_status_output(result: dict, debug: bool = False, show_fields: dict = return ' '.join(parts) -async def process_domains(input_source: str = None, debug: bool = False, concurrent_limit: int = 100, show_fields: dict = None, output_file: str = None, jsonl: bool = None, timeout: int = 5, match_codes: set = None, exclude_codes: set = None, show_progress: bool = False, check_axfr: bool = False): +async def process_domains(input_source: str = None, debug: bool = False, concurrent_limit: int = 100, show_fields: dict = None, output_file: str = None, jsonl: bool = None, timeout: int = 5, match_codes: set = None, exclude_codes: set = None, show_progress: bool = False, check_axfr: bool = False, resolver_file: str = None): ''' Process domains from a file or stdin with concurrent requests @@ -466,6 +496,7 @@ async def process_domains(input_source: str = None, debug: bool = False, concurr :param exclude_codes: Set of status codes to exclude :param show_progress: Whether to show progress counter :param check_axfr: Whether to check for AXFR + :param resolver_file: Path to file containing DNS resolvers ''' if input_source and input_source != '-' and not Path(input_source).exists(): @@ -478,6 +509,9 @@ async def process_domains(input_source: str = None, debug: bool = False, concurr tasks = set() processed_domains = 0 # Simple counter for all processed domains + # Load resolvers + resolvers = load_resolvers(resolver_file) + async def write_result(result: dict): '''Write a single result to the output file''' nonlocal processed_domains @@ -526,7 +560,7 @@ async def process_domains(input_source: str = None, debug: bool = False, concurr async with aiohttp.ClientSession() as session: # Start initial batch of tasks for domain in itertools.islice(domain_generator(input_source), concurrent_limit): - task = asyncio.create_task(check_domain(session, domain, follow_redirects=show_fields['follow_redirects'], timeout=timeout, check_axfr=check_axfr)) + task = asyncio.create_task(check_domain(session, domain, follow_redirects=show_fields['follow_redirects'], timeout=timeout, check_axfr=check_axfr, resolvers=resolvers)) tasks.add(task) # Process remaining domains, maintaining concurrent_limit active tasks @@ -541,7 +575,7 @@ async def process_domains(input_source: str = None, debug: bool = False, concurr result = await task await write_result(result) - task = asyncio.create_task(check_domain(session, domain, follow_redirects=show_fields['follow_redirects'], timeout=timeout, check_axfr=check_axfr)) + task = asyncio.create_task(check_domain(session, domain, follow_redirects=show_fields['follow_redirects'], timeout=timeout, check_axfr=check_axfr, resolvers=resolvers)) tasks.add(task) # Wait for remaining tasks @@ -637,6 +671,7 @@ def main(): parser.add_argument('-ec', '--exclude-codes', type=parse_status_codes, help='Exclude these status codes (comma-separated, e.g., 404,500)') parser.add_argument('-p', '--progress', action='store_true', help='Show progress counter') parser.add_argument('-ax', '--axfr', action='store_true', help='Try AXFR transfer against nameservers') + parser.add_argument('-r', '--resolvers', help='File containing DNS resolvers (one per line)') args = parser.parse_args() @@ -672,7 +707,7 @@ def main(): show_fields = {k: True for k in show_fields} try: - asyncio.run(process_domains(args.file, args.debug, args.concurrent, show_fields, args.output, args.jsonl, args.timeout, args.match_codes, args.exclude_codes, args.progress, check_axfr=args.axfr)) + asyncio.run(process_domains(args.file, args.debug, args.concurrent, show_fields, args.output, args.jsonl, args.timeout, args.match_codes, args.exclude_codes, args.progress, check_axfr=args.axfr, resolver_file=args.resolvers)) except KeyboardInterrupt: logging.warning(f'{Colors.YELLOW}Process interrupted by user{Colors.RESET}') sys.exit(1)