diff --git a/.coveragerc b/.coveragerc index 8aa3f08..9acf0db 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,2 @@ -[run] -include = fierce.py +[report] +include = fierce/fierce.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..5e2d450 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +ignore = E501 +include = fierce,tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f250bc7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI +on: [push, pull_request] +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + python-version: + - '3.6' + - '3.7' + - '3.8' + - '3.9' + - '3.10' + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Run CI + run: | + python -m pip install --requirement requirements.txt + python -m pip install --requirement requirements-dev.txt + python -m pip install --editable . + python -m flake8 + python -m pytest --cov diff --git a/.github/workflows/dlint.yml b/.github/workflows/dlint.yml new file mode 100644 index 0000000..cbd31ae --- /dev/null +++ b/.github/workflows/dlint.yml @@ -0,0 +1,9 @@ +name: Dlint +on: [push, pull_request] +jobs: + dlint: + runs-on: ubuntu-latest + name: Dlint + steps: + - uses: actions/checkout@master + - uses: dlint-py/dlint-action@master diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dafc54f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: python -python: - - 3.3 - - 3.4 - - 3.5 - - 3.6 -install: - - pip install -r requirements.txt - - pip install -r requirements-dev.txt -script: - - nose2 --with-coverage -after_success: - - coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md index 71059fe..a6b0c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,56 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [CHANGELOG](http://keepachangelog.com/). +## [Unreleased] + +## [1.5.0] - 2021-12-05 +### Added +- Official Python 3.9 support +- Official Python 3.10 support + +### Changed +- Improved various error handling + +### Removed +- Official Python 3.5 support + +## [1.4.0] - 2019-11-07 +### Added +- Official Python 3.8 support +- The --tcp flag to use TCP instead of UDP DNS queries + +### Removed +- Official Python 3.4 support, it's EOL + +## [1.3.0] - 2019-05-15 +### Changed +- Print out all A records for wildcard, not just first one + +### Added +- Filter out subdomains with an A record matching a wildcard A record +- Official Python 3.7 support + +### Fixed +- Prevent out of bounds error when expanding IPs near 0.0.0.0 or 255.255.255.255 + +## [1.2.2] - 2018-04-24 +### Changed +- Python 3 is now a requirement when installing via setup.py (including pip) +- The README markdown is now included in the package's long description + +## [1.2.1] - 2018-03-01 +### Changed +- Nearby IP reverse queries are now multithread, which improves performance significantly +- Updated development dependencies +- Subdomain lists use package_data instead of data_files + +### Added +- Gracefully handle users exiting the script with Ctrl+C +- Gracefully handle incorrect file or IP range arguments + +### Removed +- Official Python 3.3 support, it's EOL + ## [1.2.0] - 2017-05-07 ### Added - Official Python 3.6 support diff --git a/README.md b/README.md index f4d3f56..9d77cc2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Fierce -[![Build Status](https://travis-ci.org/mschwager/fierce.svg?branch=master)](https://travis-ci.org/mschwager/fierce) +[![CI](https://github.com/mschwager/fierce/actions/workflows/ci.yml/badge.svg)](https://github.com/mschwager/fierce/actions/workflows/ci.yml) +[![Coverage Status](https://coveralls.io/repos/github/mschwager/fierce/badge.svg?branch=master)](https://coveralls.io/github/mschwager/fierce?branch=master) +[![Dlint](https://github.com/mschwager/fierce/actions/workflows/dlint.yml/badge.svg)](https://github.com/mschwager/fierce/actions/workflows/dlint.yml) [![Python Versions](https://img.shields.io/pypi/pyversions/fierce.svg)](https://img.shields.io/pypi/pyversions/fierce.svg) [![PyPI Version](https://img.shields.io/pypi/v/fierce.svg)](https://img.shields.io/pypi/v/fierce.svg) @@ -21,9 +23,10 @@ Useful links: # Overview -First, credit where credit is due, `fierce` was originally written by RSnake -along with others at http://ha.ckers.org/. This is simply a conversion to -Python 3 to simplify and modernize the codebase. +First, credit where credit is due, `fierce` was +[originally written](https://github.com/mschwager/fierce/blob/master/scripts/fierce.pl) +by RSnake along with others at http://ha.ckers.org/. This is simply a +conversion to Python 3 to simplify and modernize the codebase. The original description was very apt, so I'll include it here: @@ -40,7 +43,7 @@ The original description was very apt, so I'll include it here: # Installing ``` -$ pip3 install fierce +$ python -m pip install fierce $ fierce -h ``` @@ -49,10 +52,12 @@ OR ``` $ git clone https://github.com/mschwager/fierce.git $ cd fierce -$ pip3 install -r requirements.txt -$ python3 fierce.py -h +$ python -m pip install -r requirements.txt +$ python fierce/fierce.py -h ``` +*Requires Python 3.* + # Using Let's start with something basic: @@ -115,6 +120,30 @@ Check out `--help` for further information: $ fierce --help ``` -# TODO +# Developing + +First, install development packages: + +``` +$ python -m pip install -r requirements.txt +$ python -m pip install -r requirements-dev.txt +$ python -m pip install -e . +``` + +## Testing + +``` +$ pytest +``` + +## Linting + +``` +$ flake8 +``` + +## Coverage -* Provide multiprocessing capabilities +``` +$ pytest --cov +``` diff --git a/fierce/__init__.py b/fierce/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fierce.py b/fierce/fierce.py similarity index 58% rename from fierce.py rename to fierce/fierce.py index b0cbe42..7754452 100755 --- a/fierce.py +++ b/fierce/fierce.py @@ -1,14 +1,20 @@ #!/usr/bin/env python3 import argparse +import concurrent.futures import functools import http.client import ipaddress +import itertools +from logging import warning +import multiprocessing import os import pprint import random import socket +import sys import time +import warnings import dns.exception import dns.name @@ -18,6 +24,35 @@ import dns.zone +def fatal(msg, return_code=-1): + print(msg) + exit(return_code) + + +def print_subdomain_result(url, ip, http_connection_headers=None, nearby=None, stream=sys.stdout): + print("Found: {} ({})".format(url, ip), file=stream) + + if http_connection_headers: + print("HTTP connected:", file=stream) + pprint.pprint(http_connection_headers, stream=stream) + + if nearby: + print("Nearby:", file=stream) + pprint.pprint(nearby, stream=stream) + + +def unvisited_closure(): + visited = set() + + def inner(l): + nonlocal visited + result = set(l).difference(visited) + visited.update(l) + return result + + return inner + + def find_subdomain_list_file(filename): # First check the list directory relative to where we are. This # will typically happen if they simply cloned the Github repository @@ -33,33 +68,16 @@ def find_subdomain_list_file(filename): # If the relative check failed then attempt to find the list file # in the pip package directory. This will typically happen on pip package # installs (duh) - # - # Here's how pip itself handles this: - # - # https://github.com/pypa/pip/blob/master/pip/commands/show.py - # + package_filename_path = os.path.join("lists", filename) try: - fierce = pkg_resources.get_distribution('fierce') - except pkg_resources.DistributionNotFound: + full_package_path = pkg_resources.resource_filename( + "fierce", + package_filename_path + ) + except ImportError: return filename - if isinstance(fierce, pkg_resources.Distribution): - paths = [] - if fierce.has_metadata('RECORD'): - lines = fierce.get_metadata_lines('RECORD') - paths = [l.split(',')[0] for l in lines] - paths = [os.path.join(fierce.location, p) for p in paths] - elif fierce.has_metadata('installed-files.txt'): - lines = fierce.get_metadata_lines('installed-files.txt') - paths = [l for l in lines] - paths = [os.path.join(fierce.egg_info, p) for p in paths] - - for p in paths: - if filename == os.path.basename(p): - return p - - # If we couldn't find anything just return the original list file - return filename + return full_package_path def head_request(url, timeout=2): @@ -92,9 +110,9 @@ def concatenate_subdomains(domain, subdomains): return result -def query(resolver, domain, record_type='A'): +def query(resolver, domain, record_type='A', tcp=False): try: - resp = resolver.query(domain, record_type, raise_on_no_answer=False) + resp = resolver.resolve(qname=domain, rdtype=record_type, raise_on_no_answer=False, tcp=True) if resp.response.answer: return resp @@ -108,23 +126,23 @@ def query(resolver, domain, record_type='A'): for rdata in additionals.items ] resolver.nameservers = ns - return query(resolver, domain, record_type) + return query(resolver, domain, record_type, tcp=tcp) return None - except (dns.resolver.NXDOMAIN, dns.resolver.NoNameservers, dns.exception.Timeout): + except (dns.resolver.NXDOMAIN, dns.resolver.NoNameservers, dns.exception.Timeout, ValueError): return None -def reverse_query(resolver, ip): - return query(resolver, dns.reversename.from_address(ip), record_type='PTR') +def reverse_query(resolver, ip, tcp=False): + return query(resolver, dns.reversename.from_address(ip), record_type='PTR', tcp=tcp) -def recursive_query(resolver, domain, record_type='NS'): +def recursive_query(resolver, domain, record_type='NS', tcp=False): query_domain = str(domain) query_response = None try: while query_response is None: - query_response = query(resolver, query_domain, record_type) + query_response = query(resolver, query_domain, record_type, tcp=tcp) query_domain = query_domain.split('.', 1)[1] except IndexError: return None @@ -135,7 +153,7 @@ def recursive_query(resolver, domain, record_type='NS'): def zone_transfer(address, domain): try: return dns.zone.from_xfr(dns.query.xfr(address, domain)) - except (ConnectionError, EOFError, TimeoutError, dns.exception.FormError): + except (ConnectionError, EOFError, TimeoutError, dns.exception.DNSException): return None @@ -147,13 +165,18 @@ def get_class_c_network(ip): return class_c -def traverse_expander(ip, n=5): - class_c = get_class_c_network(ip) +def default_expander(ip): + return [ip] - result = [ipaddress.IPv4Address(ip + i) for i in range(-n, n + 1)] - result = [i for i in result if i in class_c] - return result +def traverse_expander(ip, n=5): + ip = int(ip) + class_c_floor = ip - (ip % 256) + class_c_ceiling = class_c_floor + 255 + + ip_min = max(ip - n, class_c_floor) + ip_max = min(ip + n, class_c_ceiling) + return [ipaddress.IPv4Address(i) for i in range(ip_min, ip_max + 1)] def wide_expander(ip): @@ -164,29 +187,66 @@ def wide_expander(ip): return result +def range_expander(ip): + try: + network = ipaddress.IPv4Network(ip) + except ipaddress.AddressValueError: + fatal("Invalid IPv4 CIDR: {!r}".format(ip)) + + result = list(network) + + return result + + +def default_filter(address): + return True + + def search_filter(domains, address): return any(domain in address for domain in domains) def find_nearby(resolver, ips, filter_func=None): - reversed_ips = {str(i): reverse_query(resolver, str(i)) for i in ips} - reversed_ips = {k: v for k, v in reversed_ips.items() if v is not None} + if filter_func is None: + filter_func = default_filter - if filter_func: - reversed_ips = {k: v for k, v in reversed_ips.items() if v and filter_func(v[0].to_text())} + str_ips = [str(ip) for ip in ips] - if not reversed_ips: - return + # https://docs.python.org/3.5/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor + max_workers = multiprocessing.cpu_count() * 5 - print("Nearby:") - pprint.pprint({k: v[0].to_text() for k, v in reversed_ips.items() if v}) + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + reversed_ips = { + ip: query_result + for ip, query_result in zip( + str_ips, + executor.map( + reverse_query, + itertools.repeat(resolver, len(str_ips)), + str_ips + ) + ) + } + + reversed_ips = { + k: v[0].to_text() + for k, v in reversed_ips.items() + if v is not None and filter_func(v[0].to_text()) + } + + return reversed_ips def get_stripped_file_lines(filename): """ Return lines of a file with whitespace removed """ - return [line.strip() for line in open(filename).readlines()] + try: + lines = open(filename).readlines() + except FileNotFoundError: + fatal("Could not open file: {!r}".format(filename)) + + return [line.strip() for line in lines] def get_subdomains(subdomains, subdomain_filename): @@ -232,8 +292,14 @@ def fierce(**kwargs): ) if kwargs.get("range"): - internal_range = ipaddress.IPv4Network(kwargs.get("range")) - find_nearby(resolver, list(internal_range)) + range_ips = range_expander(kwargs.get("range")) + nearby = find_nearby( + resolver, + range_ips, + ) + if nearby: + print("Nearby:") + pprint.pprint(nearby) if not kwargs.get("domain"): return @@ -242,7 +308,7 @@ def fierce(**kwargs): if not domain.is_absolute(): domain = domain.concatenate(dns.name.root) - ns = recursive_query(resolver, domain, 'NS') + ns = recursive_query(resolver, domain, 'NS', tcp=kwargs["tcp"]) if ns: domain_name_servers = [n.to_text() for n in ns] @@ -251,16 +317,18 @@ def fierce(**kwargs): print("NS: {}".format(" ".join(domain_name_servers) if ns else "failure")) - soa = recursive_query(resolver, domain, record_type='SOA') + soa = recursive_query(resolver, domain, record_type='SOA', tcp=kwargs["tcp"]) if soa: soa_mname = soa[0].mname - master = query(resolver, soa_mname, record_type='A') + master = query(resolver, soa_mname, record_type='A', tcp=kwargs["tcp"]) master_address = master[0].address print("SOA: {} ({})".format(soa_mname, master_address)) + if master_address == "127.0.0.1": + warning("Master address is localhost, switching to provided resolver (%s) instead" % resolver.nameservers[0]) + master_address = resolver.nameservers[0] else: print("SOA: failure") - print("Failed to lookup NS/SOA, Domain does not exist") - exit(-1) + fatal("Failed to lookup NS/SOA, Domain does not exist") zone = zone_transfer(master_address, domain) print("Zone: {}".format("success" if zone else "failure")) @@ -268,55 +336,72 @@ def fierce(**kwargs): pprint.pprint({k: v.to_text(k) for k, v in zone.items()}) return - random_subdomain = str(random.randint(1e10, 1e11)) + random_subdomain = str(random.randint(1e10, 1e11)) # noqa DUO102, non-cryptographic random use random_domain = concatenate_subdomains(domain, [random_subdomain]) - wildcard = query(resolver, random_domain, record_type='A') - print("Wildcard: {}".format(wildcard[0].address if wildcard else "failure")) + wildcard = query(resolver, random_domain, record_type='A', tcp=kwargs["tcp"]) + wildcard_ips = set(rr.address for rr in wildcard.rrset) if wildcard else set() + print("Wildcard: {}".format(', '.join(wildcard_ips) if wildcard_ips else "failure")) subdomains = get_subdomains( kwargs["subdomains"], kwargs["subdomain_file"] ) - visited = set() + filter_func = None + if kwargs.get("search"): + filter_func = functools.partial(search_filter, kwargs["search"]) + + expander_func = default_expander + if kwargs.get("wide"): + expander_func = wide_expander + elif kwargs.get("traverse"): + expander_func = functools.partial(traverse_expander, n=kwargs["traverse"]) + + unvisited = unvisited_closure() for subdomain in subdomains: - url = concatenate_subdomains(domain, [subdomain]) - record = query(resolver, url, record_type='A') + try: + url = concatenate_subdomains(domain, [subdomain]) + except dns.exception.DNSException: + print("Error occured for subdomain: " + str(subdomain) + ". Skipping.") + continue + + record = query(resolver, url, record_type='A', tcp=kwargs["tcp"]) if record is None or record.rrset is None: continue - ip = ipaddress.IPv4Address(record[0].address) - print("Found: {} ({})".format(url, ip)) + ips = [rr.address for rr in record.rrset] + if wildcard_ips == set(ips): + continue + + ip = ipaddress.IPv4Address(ips[0]) + http_connection_headers = None if kwargs.get('connect') and not ip.is_private: - headers = head_request(str(ip)) - if headers: - print("HTTP connected:") - pprint.pprint(headers) - - if kwargs.get("wide"): - ips = wide_expander(ip) - elif kwargs.get("traverse"): - ips = traverse_expander(ip, kwargs["traverse"]) - else: - continue + http_connection_headers = head_request(str(ip)) - filter_func = None - if kwargs.get("search"): - filter_func = functools.partial(search_filter, kwargs["search"]) + ips = expander_func(ip) + unvisited_ips = unvisited(ips) - ips = set(ips) - set(visited) - visited |= ips + nearby = find_nearby( + resolver, + unvisited_ips, + filter_func=filter_func + ) - find_nearby(resolver, ips, filter_func=filter_func) + print_subdomain_result( + url, + ip, + http_connection_headers=http_connection_headers, + nearby=nearby + ) if kwargs.get("delay"): time.sleep(kwargs["delay"]) -def parse_args(): +def parse_args(args): p = argparse.ArgumentParser(description=''' A DNS reconnaissance tool for locating non-contiguous IP space. ''', formatter_class=argparse.RawTextHelpFormatter) @@ -372,7 +457,7 @@ def parse_args(): subdomain_group.add_argument( '--subdomain-file', action='store', - default="default.txt", + default='default.txt', help='use subdomains specified in this file (one per line)' ) @@ -388,8 +473,13 @@ def parse_args(): action='store', help='use dns servers specified in this file for reverse lookups (one per line)' ) + p.add_argument( + '--tcp', + action='store_true', + help='use TCP instead of UDP' + ) - args = p.parse_args() + args = p.parse_args(args) # Attempt to intelligently find the subdomain list depending on # how this library was installed. @@ -400,9 +490,12 @@ def parse_args(): def main(): - args = parse_args() + args = parse_args(sys.argv[1:]) - fierce(**vars(args)) + try: + fierce(**vars(args)) + except KeyboardInterrupt: + print("Exiting...") if __name__ == "__main__": diff --git a/lists/20000.txt b/fierce/lists/20000.txt similarity index 100% rename from lists/20000.txt rename to fierce/lists/20000.txt diff --git a/lists/5000.txt b/fierce/lists/5000.txt similarity index 100% rename from lists/5000.txt rename to fierce/lists/5000.txt diff --git a/lists/default.txt b/fierce/lists/default.txt similarity index 100% rename from lists/default.txt rename to fierce/lists/default.txt diff --git a/requirements-dev.txt b/requirements-dev.txt index 201146c..06669a1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ -nose2==0.6.5 -cov-core==1.15.0 -coveralls==1.1 -pyfakefs==3.1 +pytest==6.2.5 +pytest-cov==3.0.0 +flake8==3.7.7 +coveralls==1.7.0 +pyfakefs==3.5.8 diff --git a/requirements.txt b/requirements.txt index a5d6526..c76555b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -dnspython==1.15.0 +dnspython==2.2.1 diff --git a/scripts/fierce.pl b/scripts/fierce.pl new file mode 100644 index 0000000..f587d75 --- /dev/null +++ b/scripts/fierce.pl @@ -0,0 +1,584 @@ +#!/usr/bin/perl + +######################################### +# Fierce v0.9.9 - Beta 03/24/2007 +# By RSnake http://ha.ckers.org/fierce/ +# Threading and additions by IceShaman +######################################### + +use strict; +use Net::hostent; +use Net::DNS; +use IO::Socket; +use Socket; +use Getopt::Long; + +# command line options +my $class_c; +my $delay = 0; +my $dns; +my $dns_file; +my $dns_server; +my @dns_servers; +my $filename; +my $full_output; +my $help; +my $http_connect; +my $nopattern; +my $range; +my $search; +my $suppress; +my $tcp_timeout; +my $threads; +my $traverse; +my $version; +my $wide; +my $wordlist; + +my @common_cnames; +my $count_hostnames = 0; +my @domain_ns; +my $h; +my @ip_and_hostname; +my $logging; +my %options = (); +my $res = Net::DNS::Resolver->new; +my $search_found; +my %subnets; +my %tested_names; +my $this_ip; +my $version_num = 'Version 0.9.9 - Beta 03/24/2007'; +my $webservers = 0; +my $wildcard_dns; +my @wildcards; +my @zone; + +my $count; +my %known_ips; +my %known_names; +my @output; +my @thread; +my $thread_support; + +$count = 0; + +# ignore all errors while trying to load up thead stuff +BEGIN { + $SIG{__DIE__} = sub { }; + $SIG{__WARN__} = sub { }; +} + +# try and load thread modules, if it works import their functions +BEGIN { + eval { + require threads; + require threads::shared; + require Thread::Queue; + $thread_support = 1; + }; + if ($@) { # got errors, no ithreads :( + $thread_support = 0; + } else { #safe to haul in the threadding functions + import threads; + import threads::shared; + import Thread::Queue; + } +} + +# turn errors back on +BEGIN { + $SIG{__DIE__} = 'DEFAULT'; + $SIG{__WARN__} = 'DEFAULT'; +} + +my $result = GetOptions ( + 'dns=s' => \$dns, + 'file=s' => \$filename, + 'suppress' => \$suppress, + 'help' => \$help, + 'connect=s' => \$http_connect, + 'range=s' => \$range, + 'wide' => \$wide, + 'delay=i' => \$delay, + 'dnsfile=s' => \$dns_file, + 'dnsserver=s' => \$dns_server, + 'version' => \$version, + 'search=s' => \$search, + 'wordlist=s' => \$wordlist, + 'fulloutput' => \$full_output, + 'nopattern' => \$nopattern, + 'tcptimeout=i' => \$tcp_timeout, + 'traverse=i' => \$traverse, + 'threads=i' => \$threads, + ); + +help() if $help; +quit_early($version_num) if $version; + +if (!$dns && !$range) { + output("You have to use the -dns switch with a domain after it."); + quit_early("Type: perl fierce.pl -h for help"); +} elsif ($dns && $dns !~ /[a-z\d.-]\.[a-z]*/i) { + output("\n\tUhm, no. \"$dns\" is gimp. A bad domain can mess up your day."); + quit_early("\tTry again."); +} + +if ($filename && $filename ne '') { + $logging = 1; + if (-e $filename) { # file exists + print "File already exists, do you want to overwrite it? [Y|N] "; + chomp(my $overwrite = ); + if ($overwrite eq 'y' || $overwrite eq 'Y') { + open FILE, '>', $filename + or quit_early("Having trouble opening $filename anyway"); + } else { + quit_early('Okay, giving up'); + } + } else { + open FILE, '>', $filename + or quit_early("Having trouble opening $filename"); + } + output('Now logging to ' . $filename); +} + +if ($http_connect) { + unless (-e $http_connect) { + open (HEADERS, "$http_connect") + or quit_early("Having trouble opening $http_connect"); + close HEADERS; + } +} + +# if user doesn't provide a number, they both end up at 0 +quit_early('Your delay tag must be a positive integer') + if ($delay && $delay != 0 && $delay !~ /^\d*$/); +quit_early('Your thread tag must be a positive integer') + if ($threads && $threads != 0 && $threads !~ /^\d*$/); + +if ($threads && !$thread_support) { + quit_early('Perl is not configured to support ithreads'); +} + +if ($dns_file) { + open (DNSFILE, '<', $dns_file) + or quit_early("Can't open $dns_file"); + for () { + chomp; + push @dns_servers, $_; + } + if (@dns_servers) { + output("Using DNS servers from $dns_file"); + } else { + output("DNS file $dns_file is empty, using default options"); + } +} + +if ($full_output && !$http_connect) { + output("Warning: you selected the -fulloutput option but didn't use"); + output("-connect.\n\tNot sure what to do with that, so continuing..."); +} + +if ($suppress && !$filename) { + quit_early("You need to use the -s switch with the -f switch.\n + . Otherwise all will perish..."); +} + +if ($tcp_timeout) { + $res->tcp_timeout($tcp_timeout); +} else { + $res->tcp_timeout(10); +} + +if ($range) { + $nopattern = 1; + quit_early('-range must be combined with -dnsserver', $logging, $suppress) + if !$dns_server; + $res->nameservers($dns_server); + find_nearby($range); + exit; +} + +if ($traverse) { + quit_early('The -t flag must contain an integer 0-255') if $traverse !~ /\d/; + quit_early('The -t flag must contain an integer 0-255') if $traverse < 255; +} else { + $traverse = 5; +} + +my @search_strings = split(/\x2C/, $search) if $search; +my $query = $res->query($dns, 'NS'); + +if ($query) { + output("DNS Servers for $dns:"); + foreach my $rr (grep { $_->type eq 'NS' } $query->answer) { + my $dnssrv = $rr->nsdname; + output("\t$dnssrv"); + push (@domain_ns, $rr->nsdname); + } +} + +output("\nTrying zone transfer first..."); + +if ($dns_server) { + @zone = $res->axfr($dns); +} else { + for (@domain_ns) { + $res->nameservers($_); + output("\tTesting $_"); + @zone = $res->axfr($dns); + @zone ? last : output("\t\tRequest timed out or transfer not allowed."); + } +} + +if ($dns_server) { + $res->nameservers($dns_server); +} else { + $res->nameservers(@domain_ns); +} + +# DNS server gave us everything so we don't have to guess +if (@zone) { + output("\nWhoah, it worked - misconfigured DNS server found:"); + output($_->string) for (@zone); + quit_early("\nThere isn't much point continuing, you have everything.\n" + . "Have a nice day."); +} elsif ($dns) { + output("\nUnsuccessful in zone transfer (it was worth a shot)"); + output("Okay, trying the good old fashioned way... brute force"); + $wordlist = $wordlist || 'hosts.txt'; + if (-e $wordlist) { + # user provided or default + open (WORDLIST, '<', $wordlist) or + open (WORDLIST, '<', 'hosts.txt') or + quit_early("Can't open $wordlist or the default wordlist"); + for (){ + chomp; + push @common_cnames, $_; + } + close WORDLIST; + } else { + quit_early("Can't open $wordlist or the default wordlist"); + } + output("\nChecking for wildcard DNS..."); + srand; + $wildcard_dns = 1e11 - int(rand(1e10)); + if ($h = gethost("$wildcard_dns.$dns")) { + my $wildcard_addr = inet_ntoa($h->addr); + output("\t** Found $wildcard_dns.$dns at $wildcard_addr."); + output("\t** High probability of wildcard DNS."); + $wildcard_dns = $wildcard_addr; + } else { + output('Nope. Good.'); + $wildcard_dns = q{}; + } + my $total_cnames = @common_cnames; + output("Now performing $total_cnames test(s)..."); + if ($threads) { + share($count); + share(%known_ips); + share(%known_names); + share(@output); + my $stream = new Thread::Queue; + foreach my $host (@common_cnames) { + $stream->enqueue("$host.$dns"); + } + for (0..$threads) { + my $kid = new threads(\&search_host, $stream); + $stream->enqueue(undef); #for each thread + } + foreach my $thread (threads->list) { + $thread->join if ($thread->tid && !threads::equal($thread, threads->self)); + } + } else { + foreach my $host (@common_cnames) { + search_host("$host.$dns"); + } + } + # write to file any output generated by child threads + print FILE for @output; +} + +foreach my $current_name (sort keys(%known_names)) { + @ip_and_hostname = split (/\x2C/, $current_name); + my @bytes = split (/\x2E/, $ip_and_hostname[0]); + if ( $subnets{"$bytes[0].$bytes[1].$bytes[2]"} ) { + $subnets{"$bytes[0].$bytes[1].$bytes[2]"}++; + } else { + $subnets{"$bytes[0].$bytes[1].$bytes[2]"} = 1; + } +} + +output("\nSubnets found (may want to probe here using nmap or unicornscan):"); + +foreach my $athroughc (sort keys(%subnets)) { + $count_hostnames += $subnets{$athroughc}; + output("\t$athroughc.0-255 : $subnets{$athroughc} hostnames found."); +} + +&http_connect if $http_connect; + +output("\nDone with Fierce scan: http://ha.ckers.org/fierce/\n" + . "Found $count entries.\n" + . ($http_connect ? "and $webservers webservers." : "")); + +output('Have a nice day.'); +close FILE if $logging; + +# subs +sub set_nameservers { + my $res = shift; + my @servers; + push @servers, @dns_servers if @dns_servers; + push @servers, $dns_server if $dns_server; + push @servers, @domain_ns if !@servers; + $$res->nameservers(@servers); +} + +sub search_host { + sleep $delay; + my $upstream; + my $tid; + my $resbf; + if ($threads && threads->self->tid()) { + $upstream = shift; + $tid = threads->self->tid(); + $resbf = Net::DNS::Resolver->new; + set_nameservers(\$resbf); + } else { + $resbf = $res; + } + + # only runs once if not threaded + while (my $search_item = $threads ? $upstream->dequeue : shift) { + next unless my $packet = $resbf->search($search_item); + foreach my $answer ($packet->answer) { + my @name = split (/\t/, $answer->string); + next unless ($name[3] eq 'A' || $name[3] eq 'PTR'); + chop $name[0]; + if ($name[4] eq $wildcard_dns) { + $this_ip = $name[4]; + find_nearby($this_ip) if !$known_ips{$this_ip}; + $known_ips{$answer->address} = 1; + push @wildcards, "$wildcard_dns\t$search_item"; #may do something later + next; + } + + $this_ip = $name[4]; + find_nearby($this_ip) if !$known_ips{$this_ip}; + $known_ips{$this_ip} = 1; + next if $known_names{"$this_ip,$search_item"}; + $count++; + $known_names{"$this_ip,$search_item"} = 1; + output("$this_ip\t$search_item"); + } + } +} + +sub find_nearby { + my $lowest; + my $highest; + my @octet; + my $res = $res; # local copy so threads aren't waiting for others + @octet = split(/\x2E/, shift); + if ($wide) { + ($lowest, $highest) = (0, 255); + } else { # user provided range + if ($octet[3] =~ /(\d*)-(\d*)/) { + ($lowest, $highest) = ($1, $2); + quit_early("Your range doesn't make sense, try again") + if $highest < $lowest; + } else { # automatic call + $lowest = $octet[3] < $traverse ? 0 : $octet[3] - $traverse; + $highest = $octet[3] > 255 - $traverse ? 255 : $octet[3] + $traverse; + } + } + + foreach $class_c ($lowest..$highest) { + sleep $delay; + next if $class_c eq $octet[3]; + $octet[3] = $class_c; + next if $known_ips{"$octet[0].$octet[1].$octet[2].$octet[3]"}; + $known_ips{"$octet[0].$octet[1].$octet[2].$octet[3]"} = 1; + next unless my $packet = $res->search("$octet[0].$octet[1]." + . "$octet[2].$octet[3]"); + foreach my $answer ($packet->answer) { + next unless ($answer->type eq 'A' || $answer->type eq 'PTR'); + my @name = split (/\t/, $answer->string); + chop $name[$#name]; + next if ($name[4] eq $wildcard_dns); + if ($search) { + foreach (@search_strings) { + if ($name[$#name] =~ /$_/ || $nopattern || !$dns) { + $count++; + $known_names{"$octet[0].$octet[1].$octet[2].$octet[3]," + . "$name[$#name]"} = 1; + output("$octet[0].$octet[1].$octet[2].$octet[3]\t$name[$#name]"); + find_nearby("$octet[0].$octet[1].$octet[2].$octet[3]"); # recurse + } + } + } + next if ($name[$#name] !~ /$dns/ && !$nopattern); + $count++; + $known_names{"$octet[0].$octet[1].$octet[2].$octet[3]," + . "$name[$#name]"} = 1; + output("$octet[0].$octet[1].$octet[2].$octet[3]\t$name[$#name]"); + find_nearby("$octet[0].$octet[1].$octet[2].$octet[3]"); # recurse + } + } +} + +sub http_connect { + foreach my $_current_name (sort keys(%known_names)) { + my @headers; + my @my_headers; + + @ip_and_hostname = split(/\x2C/, $_current_name); + my @octet = split(/\x2E/, $ip_and_hostname[0]); + if ($octet[0] == 10 || # can't query RFC1918 + "$octet[0].$octet[1]" eq "192.168" || + ($octet[0] == 172 && $octet[1] > 32 && $octet[1] < 15) || + ($ip_and_hostname[0] eq "127.0.0.1") # loopback + ) { + next; + } + + open (HEADERS, $http_connect) + or quit_early("Don't know why but I can't read from $http_connect now"); + + foreach () { + $_ =~ s{^Host:\s*\n$}{Host: $ip_and_hostname[1]\n}; + push (@my_headers, $_); + } + close HEADERS; + if ($my_headers[$#my_headers] !~ /^(\r\n)*$/) { + $my_headers[$#my_headers + 1] = "\r\n\r\n"; + } + #TODO: add port selection and range support + my $socket = new IO::Socket::INET ( + PeerAddr => "$ip_and_hostname[0]", + PeerPort => 'http(80)', + Timeout => 10, + Proto => 'tcp', + ) + or next; + $webservers++; + foreach (@my_headers) { + print $socket $_; + } + print $socket . "\n\n"; + foreach (<$socket>) { + unless ($_ =~ /^(\r\n)*$/) { + push (@headers, $_); + } else { + last unless $full_output; + } + } + output("\n\nHTTP output for $ip_and_hostname[0] $ip_and_hostname[1]"); + output("\t$_") foreach @headers; + close $socket; + } +} + +sub output { + my $text = shift; + if ($logging) { + if ($threads && threads->self->tid) { + push @output, "$text\n"; + } else { + print FILE "$text\n"; + } + } + print "$text\n" unless $suppress; +} + +sub quit_early { + my ($text, $logging, $suppress) = @_; + print FILE "$text\nExiting...\n\n" if $logging; + print "$text\nExiting...\n" unless $suppress; + exit; +} + +sub help { + print <