package Nmap::Scanner::Scanner; use File::Spec; use Nmap::Scanner::Backend::XML; use strict; =pod =head1 DESCRIPTION This is the primary class of this module; it is the driver for the Nmap::Scanner hierarchy. To use it, create an instance of Nmap::Scanner, possibly set the path to Nmap with nmap_location() (the default behaviour of this class is to search for nmap in the PATH variable. my $nmap = new Nmap::Scanner(); $nmap->nmap_location('/usr/local/bin/nmap'); Set any options you wish to use in order to run the scan in either batch or event mode, and then call scan() to start scanning. The results are return in an Nmap::Scanner::Backend::Results object. my $results = $nmap->scan(); For information on the options presented below, see the man page for nmap or go to http://www.insecure.org/nmap/. =head2 NOTE Some descriptions of methods here are taken directly from the nmap man page. =head2 EXAMPLE See examples/ directory in the distribution for many more) use Nmap::Scanner; my $scan = Nmap::Scanner->new(); $scan->add_target('localhost'); $scan->add_target('host.i.administer'); $scan->add_scan_port('1-1024'); $scan->add_scan_port('31337'); $scan->tcp_syn_scan(); $scan->noping(); my $results = $scan->scan(); my $hosts = $results->gethostlist(); while (my $host = $hosts->get_next()) { print "On " . $host->hostname() . ": \n"; my $ports = $host->get_port_list(); while (my $port = $ports->get_next()) { print join(' ', 'Port', $port->service() . '/' . $port->portid(), 'is in state', $port->state(), "\n" ); } } =cut sub new { my $class = shift; my $you = { OPTS => {'-oX' => '-'}, DEBUG => 0}; return bless $you, $class; } =pod =head2 SCAN EVENTS Register for any of the below events if you wish to use Nmap::Scanner in event-driven mode. =head2 register_scan_complete_event(\&host_done) Register for this event to be notified when a scan of a host is complete. Pass in a function reference that can accept a $self object reference and a reference to an Nmap::Scanner::Host object. host_done($self, $host); =cut sub register_scan_complete_event { $_[0]->{'SCAN_COMPLETE_EVENT'} = [$_[0], $_[1]]; return $_[0]; } =pod =head2 register_scan_started_event(\&scan_started); Register for this event to be notified when nmap has started to scan one of the targets specified in add_target. Pass in a function reference that can accept a $self object reference, and a reference to an Nmap::Scanner::Host object. scan_started($self, $host); =cut sub register_scan_started_event { $_[0]->{'SCAN_STARTED_EVENT'} = [$_[0], $_[1]]; return $_[0]; } =pod =head2 register_host_closed_event(\&host_closed); Register to be notified if a scanned host is found to be closed (no open ports). Pass in a function reference that can take an $self object reference and a reference to a Host object. XXX --- TBD (not implemented yet). host_closed($self, $host); =cut sub register_host_closed_event { $_[0]->{'HOST_CLOSED_EVENT'} = [$_[0], $_[1]]; return $_[0]; } =pod =head2 register_port_found_event(\&port_found); Register to be notified when a port is scanned on a host. The port may be in any state ... closed, open, filtered. Pass a reference to a function that takes a $self object reference, a Host reference, and a Port reference. port_found($self, $host, $port); =cut sub register_port_found_event { $_[0]->{'PORT_FOUND_EVENT'} = [$_[0], $_[1]]; return $_[0]; } =pod =head2 register_no_ports_open_event(\&port_found); Register to be notified in the event that no ports are found to be open on a host. Pass in a reference to a function that takes a $self object reference, a Host reference, and a reference to an ExtraPorts object. The ExtraPorts object describes ports that are in a state other than open (e.g. flitered, closed). port_found($self, $host, $extra_ports); =cut # Function pointer that receives host name, IP, and status of all ports sub register_no_ports_open_event { $_[0]->{'NO_PORTS_OPEN_EVENT'} = [$_[0], $_[1]]; return $_[0]; } =pod =head2 debug() Set this to a non-zero value to see debugging output. =cut sub debug { (defined $_[1]) ? ($_[0]->{DEBUG} = $_[1]) : return $_[0]->{DEBUG}; } =pod =head2 norun() Set this to non-zero to have Nmap::Scanner::Scanner print the nmap command line and exit when scan() is called. =cut sub norun { $_[0]->{'NORUN'} = $_[1]; return $_[0]; } =pod =head2 SCAN TYPES See the nmap man page for descriptions of all these. Not all nmap scan types are supported with this release due to time limitations. =head2 tcp_connect_scan() =head2 tcp_syn_scan() =head2 fin_scan() =head2 xmas_scan() =head2 null_scan() =head2 ping_scan() =head2 udp_scan() =head2 protocol_scan() If this scan is used, the protocols can be retrieve from the Nmap::Scanner::Host objects using the method get_protocol_list() .. this will return a list of Nmap::Scanner::Port object references of type ip. =head2 idle_scan($zombie_host, $probe_port) =head2 ack_scan() =head2 window_scan() =head2 version_scan() =head2 rpc_scan() XXX: Need to implement code to support the results returned from this. START HERE in man page. =cut sub use_interface { $_[0]->{OPTS}->{'-e'} = shift; return $_[0]; } sub tcp_connect_scan { $_[0]->{TYPE} = 'T'; return $_[0]; } sub tcp_syn_scan { $_[0]->{TYPE} = 'S'; return $_[0]; } sub fin_scan { $_[0]->{TYPE} = 'F'; return $_[0]; } sub xmas_scan { $_[0]->{TYPE} = 'X'; return $_[0]; } sub null_scan { $_[0]->{TYPE} = 'N'; return $_[0]; } sub ping_scan { $_[0]->{TYPE} = 'P'; } sub udp_scan { $_[0]->{UDPSCAN} = 'U'; return $_[0]; } sub protocol_scan { $_[0]->{TYPE} = 'O'; return $_[0]; } sub idle_scan { $_[0]->{TYPE} = "I $_[1]"; $_[0]->{TYPE} .= ":$_[2]" if $_[2]; return $_[0]; } sub ack_scan { $_[0]->{TYPE} = 'A'; return $_[0]; } sub window_scan { $_[0]->{TYPE} = 'W'; return $_[0]; } sub rpc_scan { $_[0]->{RPCSCAN} = 'R'; return $_[0]; } sub version_scan { $_[0]->{'OPTS'}->{'-sV'} = ''; return $_[0]; } =pod =head2 SPECIFYING PORTS TO SCAN Use add_scan_port($port_spec) to add one or more ports to scan. $port_spec can be a single port or a range ... $n->add_scan_port(80) or $n->add_scan_port('80-1023'); Use delete_scan_port($portspec) to delete a port or range of ports. Use reset_scan_ports() to cancel any adds done with add_scan_port(). Use getports to get a hash reference in which the keys are the ports you specified with add_scan_port(). =cut sub add_scan_port { my $self = shift; for my $port_spec (@_) { $self->{PORTS}->{$port_spec} = 1; } return $self; } sub delete_scan_port { my $self = shift; for my $port_spec (@_) { delete $self->{'PORTS'}->{$port_spec} if exists $self->{'PORTS'}->{$port_spec}; } return $self; } sub reset_scan_ports { $_[0]->{PORTS} = undef; return $_[0]; } sub getports { return $_[0]->{PORTS}; } =pod =head2 SPECIFYING TARGETS TO SCAN See the nmap documentation for the full syntax nmap supports for specifying hosts / subnets / networks to scan. Use add_target($hostspec) to add a target to scan. Use delete_target($hostspec) to delete a target from the list of hosts/networks to scan (must match text used in add_target($hostspec)). Use reset_targets() to cancel any targets you specified with add_target(). =cut sub add_target { my $self = shift; for my $host_spec (@_) { $self->{'TARGETS'}->{$host_spec} = 1; } return $self; } sub delete_target { my $self = shift; for my $host_spec (@_) { $self->{'TARGETS'}->{$host_spec} = 1; delete $self->{'TARGETS'}->{$host_spec} if exists $self->{'TARGETS'}->{$host_spec}; } return $self; } sub reset_targets { $_[0]->{'TARGETS'} = undef; return $_[0]; } =pod =head2 PING OPTIONS nmap has a very flexible mechanism for setting how a ping is interpreted for hosts during a scan. See the nmap documentation for more details. Use no_ping() to not ping hosts before scanning them. Use ack_ping($port) to use a TCP ACK packet as a ping to the port specified on each host to be scanned. Use syn_ping($port) to use a TCP SYN packet as a ping to the port specified on each host to be scanned. Use icmp_ping() to use a true ICMP ping for each host to be scanned. Use ack_icmp_ping($port) to use an ICMP ping, then a TCP ACK packet as a ping (if the ICMP ping fails) to the port specified on each host to be scanned. This is the default behaviour if no ping options are specified. =cut sub no_ping { $_[0]->{'OPTS'}->{'-P'} = "0"; return $_[0]; } sub ack_ping { $_[0]->{'OPTS'}->{'-P'} = "T$_[1]"; return $_[0]; } sub syn_ping { $_[0]->{'OPTS'}->{'-P'} = "S$_[1]"; return $_[0]; } sub icmp_ping { $_[0]->{'OPTS'}->{'-P'} = "I"; return $_[0]; } sub ack_icmp_ping { $_[0]->{'OPTS'}->{'-P'} = "B$_[1]"; return $_[0]; } =pod =head2 TIMING OPTIONS Use these methods to set how quickly or slowly nmap scans a host. For more detail on these methods, see the nmap documentation. From slowest to fastest: =item * paranoid_timing() =item * sneaky_timing() =item * polite_timing() =item * normal_timing() =item * aggressive_timing() =item * insane_timing() =cut sub paranoid_timing { $_[0]->{'OPTS'}->{'-T'} = 'Paranoid'; return $_[0]; } sub sneaky_timing { $_[0]->{'OPTS'}->{'-T'} = 'Sneaky'; return $_[0]; } sub polite_timing { $_[0]->{'OPTS'}->{'-T'} = 'Polite'; return $_[0]; } sub normal_timing { $_[0]->{'OPTS'}->{'-T'} = 'Normal'; return $_[0]; } sub aggressive_timing { $_[0]->{'OPTS'}->{'-T'} = 'Aggressive'; return $_[0]; } sub insane_timing { $_[0]->{'OPTS'}->{'-T'} = 'Insane'; return $_[0]; } =pod =head2 OTHER OPTIONS There are many other nmap options. This version does not attempt to represent them all. I welcome patches from users :) and I will fill in missing gaps as I have time. For details on any of these methods see the nmap documentation. =cut =pod =head2 guess_os() Try and guess the operating system of each target host using TCP fingerprinting. =cut sub guess_os { $_[0]->{'OPTS'}->{'-O'} = ""; return $_[0]; } =pod =head2 fast_scan() Only scan for services listed in nmap's services file. =cut sub fast_scan { $_[0]->{'OPTS'}->{'-F'} = ""; return $_[0]; } =pod =head2 ident_check() Attempts to find the user that owns each open port by querying the ident damon of the remote host. See the nmap man page for more details. =cut sub ident_check { $_[0]->{'OPTS'}->{'-I'} = ""; return $_[0]; } =pod =head2 host_timeout($milliseconds) Specifies how much time nmap spends on scanning each host before giving up. Not set by default. =cut sub host_timeout { $_[0]->{'OPTS'}->{'--host-timeout'} = $_[1]; return $_[0]; } =pod =head2 max_rtt_timeout($milliseconds) Specifies the maximum time nmap should wait for a response to a probe of a port. =cut sub max_rtt_timeout { $_[0]->{'OPTS'}->{'--max_rtt_timeout'} = $_[1]; return $_[0]; } =head2 max_rtt_timeout($milliseconds) Specifies the minimum time nmap should wait for a response to a probe of a port. Nmap reduces the amoutn of time per response if the scanned machines respond quickly; it will not go below this threshold. =cut sub min_rtt_timeout { $_[0]->{'OPTS'}->{'--min_rtt_timeout'} = $_[1]; return $_[0]; } =head2 initial_rtt_timeout($milliseconds) Specifies the initial probe timeout. See the nmap man page for more detail. =cut sub initial_rtt_timeout { $_[0]->{'OPTS'}->{'--initial_rtt_timeout'} = $_[1]; return $_[0]; } =pod =head2 max_parallelism($number) Specifies the maximum number of scans Nmap is allowed to perform in parallel. =cut sub max_parallelism { $_[0]->{'OPTS'}->{'--max_parallelism'} = $_[1]; return $_[0]; } =pod =head2 scan_delay($milliseconds) Specifies the minimum amount of time Nmap must wait between probes. =cut sub scan_delay { $_[0]->{'OPTS'}->{'--scan_delay'} = $_[1]; return $_[0]; } =pod =head2 open_nmap() This method sets up a scan, but instead of actually performing the scan, it returns the PID, read filehandle, write file handle, and error file handle of the opened nmap process. Use this if you wish to just use Nmap::Scanner::Scanner as your front end to set up a scan but you wish to process it in some way not supported by Nmap::Scanner. Example: my $scan = Nmap::Scanner->new(); my $opts = '-sS -P0 -p 1-1024 192.168.32.1-255'; my ($pid, $in, $out, $err) = $scan->open_nmap($opts); =cut sub open_nmap { my $this = shift; my $fast_options = shift || ""; my $cmd = $this->_setup_cmdline($fast_options); die "$cmd\n" if $this->{'NORUN'}; my $processor = $this->_setup_processor(); my ($pid, $read, $write, $error)= $processor->start_nmap($cmd); return ($pid, $read, $write, $error); } =pod =head2 scan() Perform the scan. Returns a populated instance of Nmap::Scanner::Backend::Results when scanning in batch mode (as opposed to event-driven mode). =cut sub scan { my $this = shift; my $fast_options = shift || ""; # If we have a file, add "< " my $cmd = ""; $cmd = $this->_setup_cmdline($fast_options); die "$cmd\n" if $this->{'NORUN'}; Nmap::Scanner::debug("command line: $cmd"); my $processor = $this->_setup_processor(); my ($pid, $read)= $processor->start_nmap2($cmd); $this->{'RESULTS'} = $processor->process($pid,$read,$cmd); return $this->{'RESULTS'}; } =pod =head2 scan_from_file() Recreate a scan from an existing nmap XML-formatted output file. Pass this method in the name of an XML file created by a previously performed nmap scan done in XML output mode and the file will be processed in the same manner as a live scan would be processed. Example: my $scanner = Nmap::Scanner->new(); my $results = $scanner->scan_from_file('/path/to/scan_output.xml'); =cut sub scan_from_file { my $this = shift; my $filename = shift || die "scan_from_file: missing filename to read from!"; local (*READ); open (READ, "< $filename") || die "scan_from_file: Can't read from $filename: $!"; my $file = *READ; my $processor = $this->_setup_processor(); $this->{'RESULTS'} = $processor->process($$, $file, $filename); return $this->{'RESULTS'}; } sub _setup_cmdline { my $this = shift; my $fast_options = shift || ""; my $nmap = $this->{'NMAP'} || _find_nmap(); die "Can't find nmap!\n" unless $nmap; unless (-f $nmap && -x _) { die "Can't execute specified nmap: $this->{NMAP}\n"; } local($_); # Single quotes around command to handle spaces in full path # ... for Windows/Cygwin users. Fix by Jon Amundsen. my $cmd = "'$nmap' -v -v -v"; if (! $fast_options) { $cmd .= " -s$this->{'TYPE'}" if defined $this->{'TYPE'}; $cmd .= " -s$this->{'UDPSCAN'}" if $this->{'UDPSCAN'}; if ($this->{PORTS}) { $cmd .= " -p " . join(',', keys %{$this->{PORTS}}); } # Gather other options if ($this->{'OPTS'}) { for my $opt (keys %{$this->{OPTS}}) { $cmd .= " " . $opt . " " . $this->{'OPTS'}->{$opt}; } } $cmd .= " " . join(' ', keys %{$this->{'TARGETS'}}); } else { $cmd .= " $fast_options -oX -"; } return $cmd; } sub _setup_processor { my $this = shift; my $processor = Nmap::Scanner::Backend::XML->new(); # All backend processors support these. $processor->debug($this->{'DEBUG'}); $processor->register_scan_complete_event($this->{'SCAN_COMPLETE_EVENT'}); $processor->register_scan_started_event($this->{'SCAN_STARTED_EVENT'}); $processor->register_host_closed_event($this->{'HOST_CLOSED_EVENT'}); $processor->register_port_found_event($this->{'PORT_FOUND_EVENT'}); $processor->register_no_ports_open_event($this->{'NO_PORTS_OPEN_EVENT'}); return $processor; } sub results { (defined $_[1]) ? ($_[0]->{RESULTS} = $_[1]) : return $_[0]->{RESULTS}; } =pod =head2 nmap_location($path) If nmap is not in your PATH, you can specify where it is using this method. =cut sub nmap_location { (defined $_[1]) ? ($_[0]->{NMAP} = $_[1]) : return $_[0]->{NMAP}; } sub _find_nmap { local($_); local(*DIR); my $sep = ($^O =~ /Win32/) ? ';' : ':'; for my $dir (split($sep, $ENV{'PATH'})) { opendir(DIR,$dir) || next; my @files = (readdir(DIR)); closedir(DIR); my $path; for my $file (@files) { next unless $file =~ /^nmap(?:.exe)?$/; $path = File::Spec->catfile($dir, $file); # Should symbolic link be considered? Helps me on cygwin but ... next unless -r "$path" && (-x _ || -l _); return $path; last DIR; } } } 1;