Multimap is a multithreaded wrapper for NMap designed to run a number of concurrent NMap scans and speed up the scan of large networks. Optionally it will launch AMap on the open ports and generate an HTML file of the results.
Tool source:
#!/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. Tested with nmap 3.27 and amap 4.2.
#
# NOTE: Change the values below to match your environment and requirements!
#
# by
# Stephen de Vries - stephen at twisteddelight dot org
# www.twisteddelight.org/security
#
#############################################################################
use strict;
my $NMAP_COMMAND="/usr/local/bin/nmap";
my $NMAP_OPTIONS="--max_rtt_timeout 2000"; # -oX -oN -oM are set automatically
my $AMAP_COMMAND="/usr/local/bin/amap";
my $AMAP_OPTIONS="-b -1"; # -o -m are set automatically
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=<F>) {
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 = <AMAPFILE>) {
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 = <INFILE>;
close INFILE;
print OUTFILE @lines;
}
close OUTFILE;
closedir(DIR);
#### Put a header on the html file
push @HTML, "<HTML><HEAD></HEAD><BODY>\n<H3>Nmap results</H3>\n";
#### Process the file
open FILE, "$wdir/machine/allresults.machine.nmap" || die "Error opening $wdir/machine/allresults.nmap";
my @lines = <FILE>;
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<table width=\"80%\" border=1><tr>\n";
push @HTML," <td width=120><b>IP Address</b></td><td>$ip</td></tr>\n";
if (length($hostname > 1)) {
push @HTML," <tr><td><b>Hostname</b></b></td><td>$hostname</td></tr>\n";
}
if ($rest =~ /(.*)(?=OS)OS\:(.*)(?=IPID)IPID\s+Seq\:(.*)/) {
($rest,$os, $seq) = ($1,$2,$3);
push @HTML," <tr><td><b>OS Guess</b></td><td>$os</td></tr>\n";
#push @HTML," <tr><td><b>TCP Sequence</b></td><td>$tcpseq</td></tr>\n";
push @HTML," <tr><td><b>IPID Sequence</b></td><td>$seq</td></tr>\n";
}
push @HTML," <tr><td valign=top><b>Ports</b></td><td><table width=\"100%\" border=1>\n";
push @HTML," <tr><td><b>Num</b></td><td><b>Status</b></td><td><b>Proto</b></td><td><b>Name</b></td>";
if ($run_amap) {
push @HTML, "<td><b>Banner</b></td>";
}
push @HTML, "</tr>\n";
my @ports = split(/\,/, $portslist);
foreach $port (@ports) {
$port =~ s/^\s+//;
my @list = split(/\//, $port);
push @HTML," <tr><td valign=top>$list[0]</td>\n";
push @HTML," <td valign=top>$list[1]</td>\n";
push @HTML," <td valign=top>$list[2]</td>\n";
push @HTML," <td valign=top>$list[4]</td>\n";
if ($run_amap) {
my $banner = $banners{$list[0]};
$banner =~ s/\</\</g;
$banner =~ s/\>/\>/g;
$banner =~ s/\\n/\\n<br>/g;
push @HTML," <td valign=top>$banner</td>";
}
push @HTML, "</tr>\n";
}
push @HTML," </table></td></tr></table>\n<br><br>";
}
}
#### Put a footer on the HTML
push @HTML, "</BODY></HTML>\n";
open OUT, ">$wdir/multimap.html";
print OUT @HTML;
close OUT;
}
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;