#!/usr/bin/perl # # Runs a number of concurrent nmap processes and stores the results in # a specified directory in xml, human and machine formats. Optionally runs # amap using the nmap output as input. Writes the results to an HTML file in the # results directory. # Make sure you have the latest nmap and amap (www.thc.org) and that they play # nicely together. # # NOTE: Change the values below to match your environment and requirements! # ############################################################################# use strict; my $NMAP_COMMAND="/opt/local/bin/nmap"; my $NMAP_OPTIONS="--max_rtt_timeout 2000 --min_parallelism 30"; # -oX -oN -oM are set automatically my $AMAP_COMMAND="/usr/local/bin/amap"; my $AMAP_OPTIONS="-b"; # -o -m are set automatically my $BAN_WIDTH=50; # Maximum banner width, insert a newline character at this position ############################################################################# my @hosts, my $wdir, my $hostFile, my $procs, my $run_amap; my @pids; # Store the currently running pids my %activehosts; # hosts currently being scanned, indexed according to pid my %banners; # store banner info from amap my @HTML; # temporary storage for the output use POSIX qw(:signal_h :errno_h :sys_wait_h); use Getopt::Std; $SIG{CHLD} = \&REAPER; sub REAPER { my $pid; $pid = waitpid(-1, &WNOHANG); if ($pid == -1) { # no child waiting. Ignore it. } elsif (WIFEXITED($?)) { my $found=0; my $i=0; # Check to see if the pid that exited belongs to one of the nmaps while ((!$found) && ($i <= $#pids)) { if ($pid == $pids[$i]) { splice(@pids,$i,1); $found=1; wlog(0,"Nmap completed on: $activehosts{$pid}"); if ($run_amap) { launch_amap($activehosts{$pid}); } delete $activehosts{$pid}; } $i++; } } else { print "False alarm on $pid.\n"; } $SIG{CHLD} = \&REAPER; # in case of unreliable signals } sub getDate { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); return "$year-$mon-$mday $hour:$min:$sec"; } sub wlog { my ($code, @msg) = @_; if ($code ==0) { # Print to screen and log file print "@msg\n"; my $date = getDate();; print LOG "$date|@msg\n"; } elsif ($code ==1) { # Just print to stdout print "@msg\n"; } elsif ($code ==2) { # Just print to the log file print "@msg\n"; } else { print "Unknown error code: $code\n"; print "@msg\n"; } } sub launch { my ($host) = @_; my $OUTPUT="-oX $wdir/xml/$host.nmap.xml -oN $wdir/human/$host.nmap.human -oM $wdir/machine/$host.nmap.machine"; my $RUNSTR="$NMAP_COMMAND $NMAP_OPTIONS $OUTPUT $host"; wlog(0, "Launching: $RUNSTR"); exec($RUNSTR); } sub launch_amap { my ($host) = @_; my $OUTPUT=""; my $RUNSTR="$AMAP_COMMAND $AMAP_OPTIONS -m -i $wdir/machine/$host.nmap.machine -o $wdir/amap/$host.amap"; wlog(0, "Launching: $RUNSTR"); if (!fork()) { exec($RUNSTR); } } sub confirmSettings { print "\nLaunching with the following options:\n"; print "\nReading hosts from:\t\t$hostFile"; print "\nConcurrent nmap processes:\t$procs"; print "\nResults directory:\t\t$wdir"; open (D, $wdir); # Just so we can use the -X operators if (-d D) {print " already exists, CLOBBERING existing data!";} close D; print "\nLaunching nmap:\t\t\t$NMAP_COMMAND $NMAP_OPTIONS"; if (defined($run_amap)) { print "\nLaunching amap:\t\t\t$AMAP_COMMAND $AMAP_OPTIONS"; } print "\n\nConfirm (y,n)?\n"; my $conf = getc; if ($conf ne "y") { print "Aborted.\n"; exit 1; } } sub init { $wdir =~ s/\/$//g; my $line; wlog (0,"Creating directories in $wdir"); if (mkdir($wdir,0750) == 1) { wlog(2,"Directory $wdir created"); } else { wlog(2,"Error creating directory $wdir: $!"); } if (mkdir("$wdir/xml") == 1) { wlog(2,"Created xml directory"); } if (mkdir("$wdir/machine") == 1) { wlog(2,"Created machine readable directory"); } if (mkdir("$wdir/human") == 1) { wlog(2,"Created human readable directory"); } if (mkdir("$wdir/amap") == 1) { wlog(2,"Created amap directory"); } open LOG, ">$wdir/multiscan.log" || die "Fatal Error: could not open log file $wdir/multiscan.log"; open F, $hostFile or die "Fatal error opening file: $hostFile\n"; while ($line=) { chomp $line; wlog(0, "Adding host [$line] to scan list"); push @hosts, $line; } close F; } sub getbanners { my ($host) = @_; my $line, my @vals; open AMAPFILE, "$wdir/amap/$host.amap" || wlog(0, "Error: Can't open file $wdir/amap/$host.amap for reading"); while ($line = ) { if ($line =~ /^$host:([0-9]+)([a-z0-9A-Z\-_]*:){5}(.*)/) { my $port = $1; my $ban = $3; $ban =~ s/:$/No banner found/; $banners{$port} = $ban; print ">> $port = $ban\n"; } } close AMAPFILE; } sub convertToHTML { my $dirname = "$wdir/machine/"; my $line, my $file; my $ip, my $hostname, my $os, my $seq, my @ports, my $port, my $tcpseq, my $portslist; opendir(DIR, $dirname) or die "can't open dir $dirname: $!"; open OUTFILE, ">$wdir/machine/allresults.machine.nmap" || die "Can't open output file: $wdir/machine/allresults.machine.nmap"; while (defined($file = readdir(DIR))) { if (($file =~ /^\./) || ($file =~ /^allresults/)) {next;} open INFILE, "$wdir/machine/$file" || wlog(0,"Error: Can't open results file: $wdir/machine/$file\n"); my @lines = ; close INFILE; print OUTFILE @lines; } close OUTFILE; closedir(DIR); #### Put a header on the html file push @HTML, "\n

Nmap results

\n"; #### Process the file open FILE, "$wdir/machine/allresults.machine.nmap" || die "Error opening $wdir/machine/allresults.nmap"; my @lines = ; close FILE; foreach $line (@lines) { if ($line =~ /^Host:\s+([0-9\.]+)\s+\((\S*)\)\s+Ports:\s+(.*)\s+(?=Ignored)Ignored\ State\:(.*)/) { my ($ip, $hostname, $portslist,$rest) = ($1,$2,$3,$4); getbanners($ip); my ($os, $seq) = ("Unknown", "Unknown"); push @HTML,"\n\n"; push @HTML," \n"; if (length($hostname > 1)) { push @HTML," \n"; } if ($rest =~ /(.*)(?=OS)OS\:(.*)(?=IPID)IPID\s+Seq\:(.*)/) { ($rest,$os, $seq) = ($1,$2,$3); push @HTML," \n"; #push @HTML," \n"; push @HTML," \n"; } push @HTML,"
IP Address$ip
Hostname$hostname
OS Guess$os
TCP Sequence$tcpseq
IPID Sequence$seq
Ports\n"; push @HTML," "; if ($run_amap) { push @HTML, ""; } push @HTML, "\n"; my @ports = split(/\,/, $portslist); foreach $port (@ports) { $port =~ s/^\s+//; my @list = split(/\//, $port); push @HTML," \n"; push @HTML," \n"; push @HTML," \n"; push @HTML," \n"; if ($run_amap) { my $banner = $banners{$list[0]}; $banner =~ s/\/\>/g; $banner =~ s/\\n/\\n
/g; push @HTML," "; } push @HTML, "\n"; } push @HTML,"
NumStatusProtoNameBanner
$list[0]$list[1]$list[2]$list[4]$banner
\n

"; } } #### Put a footer on the HTML push @HTML, "\n"; open OUT, ">$wdir/namap.html"; print OUT @HTML; close OUT; } sub usage { print STDERR << "EOF"; Usage: $0 -f file -d dir -p processes [-n "options"] [-a] [-h] -f file : text file containing IP addresses to scan -d dir : directory to store output files -p processes : the number of concurrent nmap scans to run -n "options" : additional nmap options -a : run amap -h : this help Notes: Change the values of $NMAP_OPTIONS and $AMAP_OPTIONS in the code to save from typing your favourite options on the cmd line. EOF } ############################################################################### # Start main program my %options; getopts("f:d:p:n:ah", \%options); $hostFile = $options{"f"}; $wdir = $options{"d"}; $procs = $options{"p"}; my $temp = $options{"n"}; $run_amap = $options{"a"}; $temp =~ s/\"//g; $NMAP_OPTIONS = $NMAP_OPTIONS . " " . $temp; my $error=0; if (length($hostFile) == 0) { print "Error: Must specify a host file with -f file\n"; $error=1; } unless (open(F, $hostFile)) { print "Error: can't open file $hostFile for reading\n"; } close F; if (length($wdir) == 0) { print "Error: Must specify a working directory with -d dir\n"; $error=1; } if (length($procs) == 0){ print "Error: Must specify the number of processes to run with -p processes\n"; $error=1; } if ($error == 1) { usage(); exit(1); } confirmSettings(); init(); my $i=0, my $host, my $currentpid; while ($#hosts+1 > 0) { if ($i < $procs ) { $i++; $host = pop @hosts; $currentpid = fork(); push @pids, $currentpid; if ($currentpid == 0) { launch($host); exit(0); } else { $activehosts{$currentpid}=$host; } } my $total=0; for (my $n=0;$n <= $#pids;$n++) { if ($pids[$n] != 0) {$total++;} } $i = $total; sleep 1; } wait; convertToHTML; close LOG;