#!/usr/bin/perl
#
# Warscan, an Internet Scanner Dispatch
# Copyright (C) 1998 nocarrier@darkridge.com
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#

require 5.004;
use POSIX qw(:signal_h);
use Getopt::Std;
use Socket;
use Cwd; 

### System ###
$Version = "0.7";
$PatchLevel = "2";
$| = 1;
$PID = $$;
$SigSet = POSIX::SigSet->new(SIGINT);
$OSigSet = POSIX::SigSet->new();

### Behaviour ###
$Dispatch = "scan";
$DumpFile = "servers";
$ArgPrepend = "";
$ArgAppend = "";
$Target = "";

$MaxPing = 10;                         
$MaxScan = 20;
$IPLimit = 254;
$PingTimeout = 2;
$ExtraOutput = 0;

$Prepared = 0;
$Verify = 1;
$Debug = 0;
$Dump = 1;


#################
### Functions ###
#################

sub ParseCommandLine {
    my(%Options);

    $State = "parse";

    getopts("hvVDpenf:s:P:S:d:A:B:o:L:t:", \%Options); 

    (&Usage() and exit(0)) if (defined($Options{'h'})); 
    (&Version() and exit(0)) if (defined($Options{'v'}));
 
    ($MaxScan      = $Options{'S'}) if (defined($Options{'S'}));
    ($ArgAppend    = $Options{'A'}) if (defined($Options{'A'}));
    ($ArgPrepend   = $Options{'B'}) if (defined($Options{'B'}));
    ($Dest         = $Options{'d'}) if (defined($Options{'d'}));
    ($IPLimit      = $Options{'L'}) if (defined($Options{'L'}));

    (($PingTimeout = $Options{'t'}) and ($Verify = 1)) 
	if (defined($Options{'t'}));
    (($DumpFile    = $Options{'o'}) and ($Verify = 1) and ($Dump = 1)) 
	if (defined($Options{'o'}));

    ($Dump         = 0            ) if (defined($Options{'n'}));

    die "fatal: option conflict: -n and -o cannot be specified.\n" 
	if (defined($Options{'o'}) and defined($Options{'n'}));

    $Target = $ARGV[0];

    if (defined($Options{'f'})) {
	my($file) = $Options{'f'};

	die "fatal: either load a file or generate from template.\n" 
	    if ($Target);
	die "fatal: $file not found or not readable.\n" 
	    unless (-r $file);

	$Mode = "load";
	$Type = "file";
	$Target = $file;

    } else { 
	(&Usage() and exit) 
	    if ($Target eq "");

	$Mode = "build";
	
	$Type = "DNS" if (!$Type and $Target =~ /[a-zA-Z]/);
	$Type = "IP" if (!$Type);

	my($dotcnt);
	$dotcnt++ while ($Target =~ /\./g);

	if ($Type eq "IP") { 
	    die "fatal: unknown address specified.\n" if ($dotcnt > 3);

	    for ($i = $dotcnt; $i < 3; $i++) {
		$Target .= "\.\%";
	    }
	}
    }

    ($ExtraOutput = 1            ) if (defined($Options{'e'}));
    ($Debug       = 1            ) if (defined($Options{'D'}));
    ($Verify      = 0            ) if (defined($Options{'V'}));
    (($MaxPing    = $Options{'P'}) and ($Verify = 1)) 
	if (defined($Options{'P'}));
    ($PingScan    = $Options{'p'}) if (defined($Options{'p'}));
    ($Dispatch    = $Options{'s'}) if (defined($Options{'s'}));

    die "fatal: option conflict: -p and -s cannot be specified\n"
	if (defined($Options{'s'}) and defined($Options{'p'}));

    if (not $PingScan) {
	my($found) = &Check($Dispatch);
	die "fatal: $Dispatch not found in \$PATH\n" 
	    if (not $found);
	$Dispatch = $found;
    } else {
	die "fatal: must be root to ping scan\n" if ($>);
	die "fatal: not verifying with a ping scan?\n" 
	    if (not $Verify);
    }
    
    if ($> and $Verify) {
	warn "+ Warning: disabling verification without root priveleges.\n";
	$Verify = 0;
    }
}


sub Check {
    my($path,@paths,$found,$file);
    
    $file = shift;
    $found = 0;
    
    @paths = ($BaseDir);
    push @paths, split (':', $ENV{'PATH'});
    
    foreach $path (@paths) {
	$path =~ s/([^\/])$/$1\//;
    }
    
    while (@paths and !$found) {
	$path = shift @paths;
	$program = $path . $file;
	($found = 1) if (-x "$program");
    }
    
    return ($found?$program:undef);
}


sub LoadServers {
    my($file) = shift;
    my(@servers);

    open(F,$file) or die "fatal: can't open server file: $!\n";
    chop(@servers = <F>);
    close(F) or warn "+ Warning: couldn't close server file?\n";

    die "fatal: no servers read from [$file]\n" if (!@servers);
    print "+ Read in [", scalar(@servers), "] servers from file.\n";
	
    return @servers;
}


sub Generate { 
    my($limit) = $IPLimit;
    my($i,$n,@sites);

    my($template) = shift;

    print "+ Generating server list ...";
    $State = "generate";
    
    $n++ while ($template =~ /%/g);
    @sites = ($template);
    
    while ($template =~ m/%/g) {
	foreach $site (@sites) {
	    for ($i=1; $i <= $limit; $i++) {
		($_ = $site) =~ s/%/$i/;
		push @expanded, $_;
	    }
	}
	@sites = @expanded;
	@expanded = ();
    }

    print " (", scalar(@sites), ") generated.\n";

    return @sites;
}


sub Validate {
    my(@Sites) = @_;
    my(@sitebuf, @valbuf);
    my($scancnt);

    print "+ Validation beginning.\n";
    $State = "validate";

    $proto = getprotobyname('icmp');
    socket(SOCK, PF_INET, SOCK_RAW, $proto) 
	or die "fatal: couldn't create socket in Validate(): $!.\n";

    for ('0' .. int($#Sites / $MaxPing)) {
	@sitebuf = splice(@Sites, 0, $MaxPing);
	@valbuf = &massping(@sitebuf);
	push @validated, @valbuf;
	$scancnt += scalar(@sitebuf);
	print "+ Validated [", scalar(@valbuf), " of ", scalar(@sitebuf), 
	      "] (", scalar(@validated), 
              " of $scancnt total).\n";
    }
    print "+ Validation complete.\n";
    
    return @validated;
}


sub Prepare {
    $State = "prepare";
    my($dir) = shift;

    $BaseDir .= "/$dir";
    print "+ Destination is [$BaseDir].\n";
    mkdir("$BaseDir",0755) if (! -d "$BaseDir");
    chdir("$BaseDir") 
	or warn "+ Warning: could not prepare, using current directory.\n";

    $Prepared = 1;
}


sub Dump {
    my(@array) = @_;
    my($dumpdir) = $BaseDir?"$BaseDir":"/tmp";
    my($dumpfile) = (substr($DumpFile,0,1) ne "/")
	?"$dumpdir/$DumpFile"
	:"$DumpFile"; 
    
    $State = "dump";
    
    print "+ Dumping servers to [$dumpfile].\n";
    open(F,">$dumpfile") 
	or warn "+ Warning: couldn't open server dump file.\n";
    print F join("\n",@array), "\n" if (fileno(F));
    close(F) 
	or warn "+ Warning: could not close server dump file.\n";
}


sub Probe {
    my($Server) = shift;
    return unless $Server;

    $Level++;
    $State = "recurse-$Level";
    my($Pipe) = "pipe-$Server-$Level";

    sigprocmask(SIG_BLOCK, $SigSet, $OSigSet);    

    my($daddy) = open($Pipe,"-|");
    if (not defined $daddy) {
	warn "+ Warning: [$Server] fork() error: $!\n";
	sigprocmask(SIG_UNBLOCK, $OSigSet);
	sleep(1);
    } elsif (!$daddy) {
	my(@args);
	$SIG{INT} = 'IGNORE';
	sigprocmask(SIG_UNBLOCK, $OSigSet);

	push @args, split(' ',$ArgPrepend);
	push @args, $Server;
	push @args, split(' ',$ArgAppend);

	exec($Dispatch,@args);
    } else {
	sigprocmask(SIG_UNBLOCK, $OSigSet);
    }
    
    &Probe(@_);

    if ($ExtraOutput) {
	@lines = <$Pipe>;
	print @lines if (@lines);
    }
    
    close $Pipe;
    print "+ Probed: $Server\n" if ($Debug);
}


sub Catch {
    print "+ ($$) BREAK: $State.\n";

    $_ = $State;
    
  SWITCH: {
      /validate/ and do { 
	  if ($Dump) {
	      my($state) = $State;
	      &Prepare($Dest) if ($Dest and not $Prepared);
	      &Dump(@validated);
	      $State = $state;
	  }
	  last SWITCH;
      };
      /generate/ and do {
	  print "+ Stopping in IP Generation.\n";
	  last SWITCH;
      };
      /recurse/ and do {
	  if ($Dump) {
	      my($Unscanned,@Unscanned);
	      &Prepare($Dest) if ($Dest and not $Prepared);	  
	      $Unscanned = join(',', @Buffer) . ',' . join(',', @Servers);
	      @Unscanned = split(',', $Unscanned);
	      &Dump(@Unscanned);
	  }
	  last SWITCH;
      };
  }
    print "+ Exiting.\n";
    exit;
}


sub massping {
    $icmp_struct = "C2 S3 A0"; 
    $ICMP_ECHO = 8;
    $ICMP_ECHOREPLY = 0;

    my(@hosts) = @_;
    my(@valid) = ();

    my($count,$seq,$host,$checksum,$msg,$len_msg,$finish);
    my($iaddr,$saddr,$rbits);
    my($resp,$recv_msg,$timeout);
    my($f_saddr,$f_iaddr,$f_port,$f_type,$f_subcode,$f_chk,
       $f_pid,$f_seq,$f_msg,$f_host);

    $count = scalar(@hosts);
    $seq = 0;
    foreach $host (@hosts) {
	$seq++;
	$checksum 
	    = &checksum(pack($icmp_struct, $ICMP_ECHO, 0, 0, $$, $seq, ""));
	$msg = pack($icmp_struct, $ICMP_ECHO, 0, $checksum, $$, $seq, "");
	$len_msg = length($msg);
	
	print "ping SEND: host[$host] seq[$seq] cksum[$checksum]\n" 
	    if ($Debug);

	$iaddr = inet_aton($host);
	$saddr = sockaddr_in(0,$iaddr);
	
	send(SOCK, $msg, 0, $saddr);
    }

    $rbits = "";
    vec($rbits, fileno(SOCK), 1) = 1;

    $timeout = $PingTimeout;
    $finish = time() + $timeout;
    while ($count and $timeout > 0) { 
	$timeout = $finish - time();
	if ($resp = select($rbits, undef, undef, 1)) {
	    $recv_msg = "";
	    $f_saddr = recv(SOCK, $recv_msg, 1500, 0);
	    ($f_port, $f_iaddr) = unpack_sockaddr_in($f_saddr);
	    $f_host = gethostbyaddr($f_iaddr,AF_INET);
	    $f_host = "unkown" if (not $f_host);
	    ($f_type, $f_subcode, $f_chk, $f_pid, $f_seq, $f_msg) = 
		unpack ($icmp_struct, 
			substr($recv_msg, length($recv_msg) - $len_msg, 
			       $len_msg));

	    print "ping REPLY: type[$f_type] ip[$f_host] pid[$f_pid] " .
		              "seq[$f_seq].\n" if ($Debug);
	    if (($f_type == $ICMP_ECHOREPLY) and
		(not defined $seen{$f_host})) {
		push(@valid,$hosts[$f_seq-1]);
		$count--;
	    }
	    $seen{$f_host}++;
	}
    }

    return @valid;
}    


sub checksum {
    my($msg) = shift;
    my ($len_msg,$num_short,$short,$chk);

    $len_msg = length($msg);
    $num_short = $len_msg / 2;
    $chk = 0;
    foreach $short (unpack("S$num_short", $msg)) {
        $chk += $short;
    }                                           
    $chk += unpack("C", substr($msg, $len_msg - 1, 1)) if $len_msg % 2;
    $chk = ($chk >> 16) + ($chk & 0xffff);
    return(~(($chk >> 16) + $chk) & 0xffff);    
}


sub Date {
    my($min,$hour) = (localtime)[1,2];
    return (sprintf "%02d:%02d", $hour, $min);
}


sub Version {
    print "\nThis is Warscan v$Version.$PatchLevel.\n\n";
}


sub Usage {
    print "\n[$Version.$PatchLevel] warscan [options] [host template]\n";
    print " options are:\n";
    print "   -s <script>  Run script with generated host list. Default is \"scan\".\n";
    print "   -f <file>    Read in host list from file, 1 per line.\n";
    print "   -o <file>    File to dump verified servers to. Default is \"servers\".\n";  
    print "   -d <dir>     Put results/run in directory \"dir\".\n";
    print "   -A <str>     Arguments to pass to the script after the hostname.\n";
    print "   -B <str>     Arguments to pass to the script before the hostname.\n";
    print "   -P <num>     Number of pings to run in parallel. Default is 10.\n";
    print "   -S <num>     Number of scans to run concurrently. Default is 20.\n";
    print "   -L <num>     Upper limit for IP/DNS generation. Default is 254.\n";
    print "   -t <num>     Ping timeout. Default is 2 (seconds).\n";
    print "   -n           No server dump file. Don't attempt to save anything.\n";
    print "   -e           Extra output. Wait for and print output from scripts.\n";
    print "   -p           Ping scan only. Only ping hosts, don't run scan.\n";
    print "   -D           Turn on debugging. Increases verbosity.\n";
    print "   -V           Turn off verification phase.\n";
    print "   -v           Print version information and exit.\n";
    print "   -h           What you're reading now.\n\n";
}


############
### MAIN ###
############

$SIG{INT} = \&Catch;
$BaseDir = fastcwd();

die "fatal: could not get current directory!\n" 
    if (!$BaseDir);

&ParseCommandLine();

print "+ [$Version.$PatchLevel] Warscan is running (PID: $PID).\n";

if ($Type eq "file") {
     @Servers = &LoadServers($Target);
}

print "+ Target is [$Target] (Mode: $Mode, Type: $Type)\n";
print "+ Script is [$Dispatch]\n"; 

print "+ Doing Ping Scan, $MaxPing hosts at a time.\n" 
    if ($PingScan);

if ($Type ne "file") {
    @Servers = &Generate($Target);
}

&Prepare($Dest) if ($Dest and not $Prepared);

@Servers = &Validate(@Servers) if ($Verify);

&Dump(@Servers) if (($Mode ne "file") and $Verify and $Dump);

(print "+ Ping Scan complete.\n" and exit)
    if ($PingScan);

$ScanCount = 0;
$ServCount = $ServTotal = scalar(@Servers);

die "+ Nothing to probe!\n" if (!$ServCount);

print "+ Probe beginning.\n";
for ('0' .. int($#Servers / $MaxScan) ) {
    @Buffer = splice(@Servers, 0, $MaxScan);
    $Level = 1;
    &Probe(@Buffer);

    $ServCount = scalar(@Servers);
    $ScanCount += scalar(@Buffer);

    print "+ Probe Results: (", int(($ScanCount/$ServTotal)*100), "\%) ", 
          "$ScanCount probed, $ServCount left.\n";
}

print "+ Probe complete.\n";


