#!/usr/bin/env perl ####################################################################### ## lyle@digitalfoo.net http://digitalfoo.net ## ## ABOUT ## dig-shovel is a tool that basically parses the output of a ## zone-tranfer via dig. ## ## The program initially digs for nameservers belonging to a domain, ## attempts to do a zone transfer of each nameserver, and finally ## parses the host records of a successful transfer. Obviously, the ## last phase only happens if a nameserver was found to allow zone ## transfers. ## ## LICENSE ## Go to one of my sites and click on a banner. Then you can do ## what ever you want with this code. :) Come to think of it, ## simply linking to http://digitalfoo.net would be great. ## ## DEPENDENCIES ## The following libraries are needed for this perl script. Most ## are available at CPAN, but you should be able to install most ## via your operating system's package manager. ## ## Perl6::Form ## http://search.cpan.org/~dconway/Perl6-Form-0.04/Form.pm ## ## FEEDBACK ## If you have a bug, comment, or suggestion for a feature, contact ## me via my websites or the contact posted above. ## ####################################################################### use strict; use warnings; #use Data::Dumper; use Perl6::Form; use Getopt::Std; use File::Basename; ### Parse command line options my %options = (); getopts('vqt:r:i:d:e:f:a:o:l:', \%options); sub usage () { print "\n", "********************************************************************************\n", " DIGN :: a tool to parse records found in a zone transfer (via DiG).\n", "\n", " Lyle Scott, III lyle\@digitalfoo.net\n", " http://lylescott.ws http://digitalfoo.net\n", "--------------------------------------------------------------------------------\n", " USAGE: " ,basename($0), " [-v] [-e [-f -a ]]\n", "--------------------------------------------------------------------------------\n", " -d : domain to dig for a transfer\n", " -v : verbose output\n", " -q : quite mode; disable printing to STDOUT\n", " -t : seconds to pause between zone-transfer attempts\n", " -r : Comma delimited list of record types, Default is NS,A,AAAA,NS,MX.\n", " -i : DEFAULT IPv4 and IPv6\n", " 4 IPv4 only\n", " 6 IPv6 only\n", " -e : STDOUT DEFAULT; no logging\n", " normal export STDOUT output\n", " hosts_lined export hosts line-by-line\n", " hosts_delimit export hosts user specified user delimiters\n", " -f : output file for export method\n", " -o : hosts | ips output hostnames (DEFAULT) or IPs in logfile\n", " -l : save the hosts in one line with delimiter\n", "--------------------------------------------------------------------------------\n", " EXAMPLES\n", " " ,basename($0), " -d domain.tld \n", " " ,basename($0), " -d domain.tld -e normal -f dig_axfr_output.txt\n", " " ,basename($0), " -d domain.tld -e hosts_lined -f dig_axfr_output.txt -o ips\n", " " ,basename($0), " -d domain.tld -e hosts_delimit -f dig_axfr_output.txt -l ,\n", " " ,basename($0), " -d domain.tld -v -r AAAA,MX\n", "--------------------------------------------------------------------------------\n", "********************************************************************************\n"; } ### make sure DiG is available on the system unless (length(`whereis dig`)) { print "\nERROR, 'dig' was not found on your system. Please install it!\n\n"; exit; } ### require a domain name to dig unless (defined $options{d}) { usage(); warn "\nERROR, no domain name was given. Add '-d some-domain.tld' to the program arguments.'\n\n"; exit; } ### defaults (stay the same unless overridden via args) my $VERBOSE = 0; my $QUIET = 0; my $SLEEP = 1; my @QUERY_RECORDS = qw(NS A AAAA CNAME MX); my $LOG_DELIMITER = ","; ### parse command line arguments into sentinel values my $DOMAIN = $options{d} if (defined $options{d}); $VERBOSE = 1 if (defined $options{v}); $QUIET = 1 if (defined $options{q}); $SLEEP = $options{t} if (defined $options{t}); my $IPV_TYPE = $options{i} if (defined $options{i}); my $LOG_METHOD = $options{e} if (defined $options{e}); my $LOG_FILE = $options{f} if (defined $options{f}); my $LOG_HOST_OUT = $options{o} if (defined $options{o}); $LOG_DELIMITER = $options{l} if (defined $options{l}); @QUERY_RECORDS = split(",", $options{r}) if (defined $options{r}); ### $_[0] = domain to DiG for name servers sub extract_initial_ns_records ($) { my @records; my $dig_output = `dig NS $_[0]`; if ($dig_output =~ /ANSWER:\s([\d]+),/) { if ($1 == 0) { print "\nERROR, could not dig up nameservers for $DOMAIN!\nThe domain might not exist.\n\n"; exit; } } while ($dig_output =~ /.*\tNS\t(.*)\./g) { push (@records, $1); } return \@records; } ### $_[0] = hostname to resolve ### $_[1] = @a_records array ref sub resolve ($$) { my $result; for my $i (0..$#{$_[1]}) { if ($_[1][$i]{hostname} =~ /.*gooroo.*/) { $result = $_[1][$i]{ip}; last; } else { $result = "off domain."; # TODO: fix off domain handling } } return $result; } ### $_[0] = entire dig output of zone transfer ### $_[1] = reference to @a_records sub extract_a_records ($) { my @records; while ($_[0] =~ /(.*)\..*\t[A]{1}\t(.*)/g) { push (@records, { hostname => $1, ip => $2 }); } return \@records; } ### $_[0] = entire dig output of zone transfer ### $_[1] = reference to @aaaa_records sub extract_aaaa_records ($) { my @records; while ($_[0] =~ /(.*)\..*[A]{4}\t(.*)/g) { push (@records, { hostname => $1, ip => $2 }); } return \@records; } ### $_[0] = entire dig output of zone transfer ### $_[1] = reference to @ns_records sub extract_ns_records ($$) { my @records; while ($_[0] =~ /(.*)\..*\tNS\t(.*)./g) { my $r; unless (($r = resolve($2, $_[1]))) { $r = "" }; push (@records, { nameserver => $2, domain => $1, ip => $r }); } return \@records; } ### $_[0] = entire dig output of zone transfer ### $_[1] = reference to @cname_records sub extract_cname_records ($$) { my @records; while ($_[0] =~ /(.*)\.\t.*(CNAME)\t(.*)./g) { my $r; unless (($r = resolve($3, $_[1]))) { $r = "" }; push (@records, { alias => $1, host => $3, ip => $r }); } return \@records; } ### $_[0] = entire dig output of zone transfer ### $_[1] = reference to @mx_records sub extract_mx_records ($$) { my @records; while ($_[0] =~ /(.*)\.\t.*(MX)\t([\d]+)\s(.*)./g) { my $r; unless (($r = resolve($4, $_[1]))) { $r = "" }; push (@records, { domain => $1, priority => $3, exchange => $4, ip => $r }); } return \@records; } ### $_[0] = reference to @a_records sub print_a_records ($) { foreach my $i (0..$#{$_[0]}) { print form 'A : {[{*}[} {[{*}[}', $_[0][$i]{hostname}, $_[0][$i]{ip}; } } ### $_[0] = reference to @aaaa_records sub print_aaaa_records ($) { foreach my $i (0..$#{$_[0]}) { print form 'AAAA : {[{*}[} {[{*}[}', $_[0][$i]{hostname}, $_[0][$i]{ip}; } } ### $_[0] = reference to @ns_records sub print_ns_records ($) { my $blank = ""; foreach my $i (0..$#{$_[0]}) { if ($VERBOSE == 1) { print form 'NS : {[{*}[} {[{*}[}', $_[0][$i]{nameserver}, $_[0][$i]{domain}, ' {[{*}[} {[{*}[}', $blank, $_[0][$i]{ip}, } else { print form 'NS : {[{*}[} {[{*}[}', $_[0][$i]{nameserver}, $_[0][$i]{ip}; } } } ### $_[0] = reference to @cname_records sub print_cname_records ($) { my $blank = ""; foreach my $i (0..$#{$_[0]}) { if ($VERBOSE == 1) { print form 'CNAME : {[{*}[} {[{*}[}', $_[0][$i]{alias}, $_[0][$i]{host}, ' {[{*}[} {[{*}[}', $blank, $_[0][$i]{ip}; } else { print form 'CNAME : {[{*}[} {[{*}[}', $_[0][$i]{alias}, $_[0][$i]{host}; } } } ### $_[0] = reference to @mx_records sub print_mx_records ($) { my $blank = ""; foreach my $i (0..$#{$_[0]}) { if ($VERBOSE == 1) { print form 'MX : {[{*}[} {[{*}[}', $_[0][$i]{domain}, $_[0][$i]{priority}. " " .$_[0][$i]{exchange}, ' {[{*}[} {[{*}[}', $blank, $_[0][$i]{ip}; } else { print form 'MX : {[{*}[} {[{*}[}', $_[0][$i]{domain}, $_[0][$i]{exchange}; } } } ### Log normal columnized STDOUT output sub log_normal ($$$$$$) { my $fh = shift; my $a_records = shift; my $aaaa_records = shift; my $ns_records = shift; my $cname_records = shift; my $mx_records = shift; my $blank = ""; my $i; ### A records for $i (0..$#{$a_records}) { print $fh form 'A : {[{*}[} {[{*}[}', ${$a_records}[$i]{hostname}, ${$a_records}[$i]{ip}; } ### AAAA records for $i (0..$#{$aaaa_records}) { print $fh form 'AAAA : {[{*}[} {[{*}[}', ${$aaaa_records}[$i]{hostname}, ${$aaaa_records}[$i]{ip}; } ### NS records for $i (0..$#{$ns_records}) { if ($VERBOSE == 1) { print $fh form 'NS : {[{*}[} {[{*}[}', ${$ns_records}[$i]{nameserver}, ${$ns_records}[$i]{domain}, ' {[{*}[} {[{*}[}', $blank, ${$ns_records}[$i]{ip}; } else { print $fh form 'NS : {[{*}[} {[{*}[}', ${$ns_records}[$i]{nameserver}, ${$ns_records}[$i]{ip}; } } ### CNAME records for $i (0..$#{$cname_records}) { if ($VERBOSE == 1) { print $fh form 'CNAME : {[{*}[} {[{*}[}', ${$cname_records}[$i]{alias}, ${$cname_records}[$i]{host}, ' {[{*}[} {[{*}[}', $blank, ${$cname_records}[$i]{ip}; } else { print $fh form 'CNAME : {[{*}[} {[{*}[}', ${$cname_records}[$i]{alias}, ${$cname_records}[$i]{host}; } } ### MX records for $i (0..$#{$mx_records}) { if ($VERBOSE == 1) { print $fh form 'MX : {[{*}[} {[{*}[}', ${$mx_records}[$i]{domain}, ${$mx_records}[$i]{priority}. " " .${$mx_records}[$i]{exchange}, ' {[{*}[} {[{*}[}', $blank, ${$mx_records}[$i]{ip}; } else { print $fh form 'MX : {[{*}[} {[{*}[}', ${$mx_records}[$i]{domain}, ${$mx_records}[$i]{exchange}; } } } ### Log each host in it's own line sub log_hosts_lined ($$$) { my $fh = shift; my $a_records = shift; my $aaaa_records = shift; my $i; ### A records for $i (0..$#{$a_records}) { if (!defined $LOG_HOST_OUT || $LOG_HOST_OUT eq "hosts") { print $fh "${$a_records}[$i]{hostname}\n"; } elsif ($LOG_HOST_OUT eq "ips") { print $fh "${$a_records}[$i]{ip}\n"; } } ### AAAA records for $i (0..$#{$aaaa_records}) { if (!defined $LOG_HOST_OUT || $LOG_HOST_OUT eq "hosts") { print $fh "${$aaaa_records}[$i]{hostname}\n"; } elsif ($LOG_HOST_OUT eq "ips") { print $fh "${$aaaa_records}[$i]{ip}\n"; } } } ### Log hosts on one line delimited by user specified argument sub log_hosts_delimit ($$$$) { my $fh = shift; my $a_records = shift; my $aaaa_records = shift; my $delimiter = shift; my $i; ### A records for $i (0..$#{$a_records}) { print $fh $delimiter unless $i == 0; if (!defined $LOG_HOST_OUT || $LOG_HOST_OUT eq "hosts") { print $fh "${$a_records}[$i]{hostname}"; } elsif ($LOG_HOST_OUT eq "ips") { print $fh "${$a_records}[$i]{ip}"; } print $fh $delimiter if $i == $#{$a_records} && $#{$a_records} > 0; } ### AAAA records for $i (0..$#{$aaaa_records}) { print $fh $delimiter unless $i == 0 || $#{$a_records} == 0; if (!defined $LOG_HOST_OUT || $LOG_HOST_OUT eq "hosts") { print $fh "${$aaaa_records}[$i]{hostname}"; } elsif ($LOG_HOST_OUT eq "ips") { print $fh "${$aaaa_records}[$i]{ip}"; } } print $fh "\n"; } ### go through each domain and try to do a zone transfer sub transfer ($) { my $a_records; my $aaaa_records; my $ns_records; my $cname_records; my $mx_records; my $file_handle; foreach my $nameserver (@{$_[0]}) { my $axfr_output = `dig \@$nameserver axfr $DOMAIN`; if ($axfr_output =~ /XFR/) { unless ($QUIET) { print "\n*********************************************************************\n"; } print "SUCCESSFUL TRANSFER: $nameserver\n"; ### Extract the selected types of hosts out of the zone transfer dump $a_records = extract_a_records($axfr_output) if (!defined $IPV_TYPE || $IPV_TYPE eq "4"); $aaaa_records = extract_aaaa_records($axfr_output) if (!defined $IPV_TYPE || $IPV_TYPE eq "6"); if (grep {$_ eq "NS"} @QUERY_RECORDS) { $ns_records = extract_ns_records($axfr_output, $a_records); } if (grep {$_ eq "CNAME"} @QUERY_RECORDS) { $cname_records = extract_cname_records($axfr_output, $a_records); } if (grep {$_ eq "MX"} @QUERY_RECORDS) { $mx_records = extract_mx_records($axfr_output, $a_records); } ### print to STDOUT unless ($QUIET) { print "*********************************************************************\n"; if (grep {$_ eq "A"} @QUERY_RECORDS) { print_a_records($a_records); } if (grep {$_ eq "AAAA"} @QUERY_RECORDS) { print_aaaa_records($aaaa_records); } print_ns_records($ns_records); print_cname_records($cname_records); print_mx_records($mx_records); print "*********************************************************************\n"; } ### log stuff if specified in command line args if (defined $LOG_METHOD && defined $LOG_FILE) { ### figure out what direction to write te file in open ($file_handle, ">", $LOG_FILE); ### parse the host output into specified style if ($LOG_METHOD eq "normal") { log_normal($file_handle, $a_records, $aaaa_records, $ns_records, $cname_records, $mx_records); } elsif ($LOG_METHOD eq "hosts_lined") { log_hosts_lined($file_handle, $a_records, $aaaa_records); } elsif ($LOG_METHOD eq "hosts_delimit") { log_hosts_delimit($file_handle, $a_records, $aaaa_records, $LOG_DELIMITER); } else { warn "ERROR: unknown logging method... no log written!\n"; } close($file_handle); } ### no more stuff to do exit; } else { print "failed to transfer using : $nameserver\n"; } ### prevent tripping over self sleep $SLEEP; } } ### initiate program my $initial_ns_records = extract_initial_ns_records($DOMAIN); transfer($initial_ns_records); #EOF