#!/usr/bin/env perl

#
# snmpscan - SNMP multithread scanner 
# Version 0.1 by Matteo Cantoni
# Homepage: http://www.nothink.org
#

$|=1;

use strict;
use warnings;

my $can_use_script = eval 'require 5.008; 1';
die "You need Perl v5.8.0!\n" unless $can_use_script;

my $can_use_threads = eval 'use threads; 1';
die "You need Perl with threading support!\n" unless $can_use_threads;

use threads ('yield','stack_size' => 64*4096,'exit' => 'threads_only','stringify');
use threads::shared;
use Thread::Queue;

use Getopt::Long;
use Net::IP;
use Net::SNMP;
use Number::Bytes::Human qw(format_bytes);
use Time::HiRes qw(gettimeofday tv_interval);
use Uniq;

my $name        = "snmpscan";
my $ver         = '0.1';
my $description = "SNMP multithread scanner";
my $copyright   = "Copyright (c) 2010";
my $author      = "Matteo Cantoni";
my $website     = "http://www.nothink.org";

#
# Default settings
#

my @community  = ("public","private");
my $mibDescr   = '1.3.6.1.2.1.1.1.0';
my $mibContact = '1.3.6.1.2.1.1.4.0';
my $snmpver    = 1; # 1/v2c
my $maxthreads = 50;
my $timeout    = 3;
my $port       = 161;
my $retries    = 0;

my ($target,$target_list,$community,$detect_write,$randomize,$verbose,$help);

GetOptions(
	"target=s"       => \$target,
	"target-list=s"  => \$target_list,
	"community=s"    => \$community,
	"detect-write"   => \$detect_write,
	"snmp-version=s" => \$snmpver,
	"randomize"      => \$randomize,
	"port=i"         => \$port,
	"retries=i"      => \$retries,
	"threads=i"      => \$maxthreads,
	"timeout=i"      => \$timeout,
	"verbose"        => \$verbose,
	"help"           => \$help
);

if ($community){
	undef @community;
	push @community,$community;
}

my $community_list = join(",",@community);

Usage() if $help;
Usage() if $target and $target_list;
Usage() if not $target and not $target_list;

$SIG{'KILL'} = sub {
	threads->detach() if ! threads->is_detached();
	threads->exit();
};

{ my $flip = "|"; sub wheel { print $flip = { reverse split //, '|/\|-\/-|', -1 }->{ $flip }, "\b"; } }

my @hosts;

if ($target){
	print "[*] Creating IP address list... ";
	my $ip = new Net::IP ("$target");
	do { &wheel; push @hosts,$ip->ip(); } while (++$ip);
	print "\n";
} elsif ($target_list){
	open (IPLIST, "<$target_list") || die "[*] Cannot open the ip address list file: $!\n";
		chomp (@hosts = <IPLIST>);
	close (IPLIST);
}

@hosts = uniq sort @hosts;

if ($randomize){
	print "[*] Randoming target hosts...\n";
	@hosts = Shuffle(\@hosts);
}

if ($verbose){
	print "[*] Options\n";
	print "    . SNMP port      : $port\n";
	print "    . SNMP version   : $snmpver\n";
	print "    . SNMP community : $community_list\n";
	print "    . retries        : $retries\n";
	print "    . timeout        : $timeout\n";
	print "    . threads        : $maxthreads\n";
	print "    . randomize enabled\n" if $randomize;
	print "    . detect SNMP write access\n" if $detect_write;
}

###

our ($q,$t_read,$t_write) :shared;
$q = new Thread::Queue;

$t_read = 0;
$t_write = 0;

foreach (@hosts){
	$q->enqueue($_);
}

###

my $MonitorRunningThreads = threads->new(\&MonitorRunningThreads);

my $tot = $q->pending;
print "[*] $tot hosts to scan\n";

my $start_time = [gettimeofday];

while ($q->pending > 0) {
	if ((threads->list(threads::running)) <= $maxthreads) {
		my $thr = threads->new(\&Scan,$q->dequeue_nb);
	}
}

while ((threads->list(threads::running)) > 1){};

my $end_time = [gettimeofday];
my $elapsed  = tv_interval($start_time,$end_time);

sleep 1;

foreach my $thr (threads->list(threads::running)){
	$thr->kill('KILL')->detach();
}

printf "[*] Enumerated $tot in %.2f seconds\n",$elapsed;
print  "[*] Found $t_read hosts with read access\n";
print  "[*] Found $t_write hosts with write access\n" if $detect_write;

exit(0);

sub Scan {
	my $host = shift;

	foreach my $community (@community){

		print "[-] Checking $host (read/$community/v$snmpver)\n" if $verbose;

		my ($session,$error) = Net::SNMP->session(
			Hostname  => $host,
			Community => $community,
			Domain    => 'udp',
			Port      => $port,
			Version   => $snmpver,
			Timeout   => $timeout,
			Retries   => $retries,
			Debug     => 0,
			Translate => [
				-timeticks => 0x0
			]
		);

		if ($session){
			my $result = $session->get_request(
				-varbindlist => [ $mibDescr ]
			);
	
			if ($result){

				$t_read++;

				my $res = $result->{$mibDescr} || "n/a";
				$res =~ s/\r|\n//g;

				my $write_check;

				# write access check; only if read access exists
				if ($detect_write){
	
					print "[-] Checking $host (write/$community/v$snmpver)\n" if $verbose;

					my $contact = $session->get_request(
						-varbindlist => [ $mibContact ]
					);
					if ($contact){
						$write_check = $session->set_request(
							-varbindlist => [$mibContact, OCTET_STRING ,'testwriteaccess']
						);
						if ($write_check){
							$t_write++;
							$write_check = 'write';
							$session->set_request(
								-varbindlist => [$mibContact, OCTET_STRING ,$contact]
							);
						}
					}
				}
		
				if ($write_check){
					print "$host|$community|v$snmpver|$res|$write_check\n";
				} else {
					print "$host|$community|v$snmpver|$res\n";
				}
			}

			$session->close();
		}
	}
}

sub MonitorRunningThreads {
	while (1){
		foreach my $thr (threads->list(threads::joinable)) { 
			# don't join the main or monitor threads or ourselves 
			if ($thr->tid && !threads::equal($thr, threads->self) && !threads::equal($thr,$MonitorRunningThreads)){ 
				$thr->join; 
			}
		}
	}
}

sub Shuffle {
	my $array = shift;
	my $i = scalar(@$array);
	my $j;
	foreach my $item (@$array){
		--$i;
		$j = int rand ($i+1);
		next if $i == $j;
		@$array [$i,$j] = @$array[$j,$i];
	}
	return @$array;
}

sub Usage {

print <<USAGE;
$name - $description
Version $ver by $author
Homepage: $website

 Usage: $name [options] --target <IP address/network>

\t--target            target IP address/network (CIDR)
\t--target-list       target list 
\t--community         SNMP community; default $community_list
\t--detect-write      detect SNMP write access
\t--snmp-version      SNMP version (1 or v2c); default $snmpver
\t--randomize         enable randomize targets 
\t--port              SNMP port; default $port
\t--retries           number of times to retry sending a SNMP message; default $retries
\t--threads           threads to be used; default $maxthreads
\t--timeout           number of seconds wait for a response; default $timeout
\t--verbose           enable verbose
\t--help              show this help message and exit

 More parameters in 'Default settings'. See the home page (http://www.nothink.org).

 Example usage:

  * $name --randomize --threads 100 --timeout 2 --community public --target 192.168.0.0/24
  * $name --detect-write --randomize --timeout 1 --verbose --target 192.168.0.0-192.168.3.255

USAGE

exit();
}
