#! /usr/bin/perl -w
# finger-user-enum - Brute Force Username via Finger Service
# Copyright (C) 2006 pentestmonkey@pentestmonkey.net
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as 
# published by the Free Software Foundation. 
# 
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
#
# This tool may be used for legal purposes only.   Users take full responsibility
# for any actions performed using this tool.   If these terms are not acceptable to 
# you, then do not use this tool. 
# 
# You are encouraged to send comments, improvements or suggestions to
# me at finger-user-enum@pentestmonkey.net
#
# This program is derived from dns-grind v1.0 ( http://pentestmonkey.net/tools/dns-grind )

use strict;
use Socket;
use IO::Handle;
use IO::Select;
use IO::Socket::INET;
use Getopt::Std;
$| = 1;

my $VERSION        = "1.0";
my $debug          = 0;
my @child_handles  = ();
my $verbose        = 0;
my $max_procs      = 5;
my $finger_port    = 79;
my @usernames      = ();
my @hosts          = ();
my $recursive_flag = 1;
my $relayserver    = undef;
my $query_timeout  = 5;
my $start_time     = time();
my $end_time;
my $kill_child_string = "\x00";
$SIG{CHLD} = 'IGNORE'; # auto-reap
my %opts;
my $usage=<<USAGE;
finger-user-enum v$VERSION ( http://pentestmonkey.net/tools/finger-user-enum )

Usage: finger-user-enum.pl [options] ( -u username | -U file-of-usernames ) ( -t host | -T file-of-targets )

options are:
        -m n     Maximum number of resolver processes (default: $max_procs)
	-u user  Check if user exists on remote system
	-U file  File of usernames to check via finger service
	-t host  Server host running finger service
	-T file  File of hostnames running the finger service
	-r host  Relay.   Intermediate server which allows relaying of finger requests. 
	-p port  TCP port on which finger service runs (default: $finger_port)
	-d       Debugging output
	-s n     Wait a maximum of n seconds for reply (default: $query_timeout)
	-v       Verbose
	-h       This help message

Also see finger-user-enum-user-docs.pdf from the finger-user-enum tar ball. 

Examples:

\$ finger-user-enum.pl -U users.txt -t 10.0.0.1
\$ finger-user-enum.pl -u root -t 10.0.0.1
\$ finger-user-enum.pl -U users.txt -T ips.txt

USAGE

getopts('m:u:U:t:T:p:r:ds:vh', \%opts);

# Print help message if required
if ($opts{'h'}) {
	print $usage;
	exit 0;
}

my $username       = $opts{'u'} if $opts{'u'};
my $username_file  = $opts{'U'} if $opts{'U'};
my $host           = $opts{'t'} if $opts{'t'};
my $host_file      = $opts{'T'} if $opts{'T'};
my $file           = $opts{'f'} if $opts{'f'};

$query_timeout  = $opts{'s'} if $opts{'s'};
$relayserver    = $opts{'r'} if $opts{'r'};
$max_procs      = $opts{'m'} if $opts{'m'};
$finger_port    = $opts{'p'} if $opts{'p'};
$verbose        = $opts{'v'} if $opts{'v'};
$debug          = $opts{'d'} if $opts{'d'};

# Check for illegal option combinations
unless ((defined($username) or defined($username_file)) and (defined($host) or defined($host_file))) {
	print $usage;
	exit 1;
}

# Check for strange option combinations
if (
	(defined($host) and defined($host_file))
	or
	(defined($username) and defined($username_file))
) {
	print "WARNING: You specified a lone username or host AND a file of them.   Continuing anyway...\n";
}

# Shovel usernames and host into arrays
if (defined($username_file)) {
	open(FILE, "<$username_file") or die "ERROR: Can't open username file $username_file: $! \n";
	@usernames = map { chomp($_); $_ } <FILE>;
}

if (defined($host_file)) {
	open(FILE, "<$host_file") or die "ERROR: Can't open username file $host_file: $! \n";
	@hosts = map { chomp($_); $_ } <FILE>;
}

if (defined($username)) {
	push @usernames, $username;
}

if (defined($host)) {
	push @hosts, $host;
}

if (defined($host_file) and not @hosts) {
	print "ERROR: Targets file $host_file was empty\n";
	exit 1;
}

if (defined($username_file) and not @usernames) {
	print "ERROR: Username file $username_file was empty\n";
	exit 1;
}

print "Starting finger-user-enum v$VERSION ( http://pentestmonkey.net/tools/finger-user-enum )\n";
print "\n";
print " ----------------------------------------------------------\n";
print "|                   Scan Information                       |\n";
print " ----------------------------------------------------------\n";
print "\n";
print "Worker Processes ......... $max_procs\n";
print "Targets file ............. $host_file\n" if defined($host_file);
print "Usernames file ........... $username_file\n" if defined($username_file);
print "Target count ............. " .  scalar(@hosts) . " \n" if @hosts;
print "Username count ........... " .  scalar(@usernames) . " \n" if @usernames;
print "Target TCP port .......... $finger_port\n";
print "Query timeout ............ $query_timeout secs\n";
if (defined($relayserver)) {
print "Relay Server ............. $relayserver\n";
} else {
print "Relay Server ............. Not used\n";
}
print "\n";
print "######## Scan started at " .  scalar(localtime()) . " ######### \n";

# Spawn off correct number of children
foreach my $proc_count (1..$max_procs) {
	socketpair(my $child, my $parent, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or  die "socketpair: $!"; 
	$child->autoflush(1);
	$parent->autoflush(1);

	# Parent executes this
	if (my $pid = fork) {
		close $parent;
		print "[Parent] Spawned child with PID $pid to do resolving\n" if $debug;
		push @child_handles, $child;

	# Chile executes this
	} else {
		close $child;
		while (1) {
			my $timed_out = 0;

			# Read host and username from parent
			my $line = <$parent>;
			chomp($line);
			my ($host, $username) = $line =~ /^(\S+)\t(.*)$/;

			# Exit if told to by parent
			if ($line eq $kill_child_string) {
				print "[Child $$] Exiting\n" if $debug;
				exit 0;
			}
			
			# Sanity check host and username
			if (defined($host) and defined($username)) {
				print "[Child $$] Passed host $host and username $username\n" if $debug;
			} else {
				print "[Child $$] WARNING: Passed garbage.   Ignoring: $line\n";
				next;
			}

			# Do finger query with timeout
			my $response;
			eval {
				local $SIG{ALRM} = sub { die "alarm\n" };
				alarm $query_timeout;
				my $connect_host;
				if (defined($relayserver)) {
					$connect_host = $relayserver;
				} else {
					$connect_host = $host;
				}
				my $s = IO::Socket::INET->new( 	PeerAddr => $connect_host,
								PeerPort => $finger_port,
								Proto    => 'tcp'
							)
					or die "Can't connect to $host:$finger_port: $! \n";
				if (defined($relayserver)) {
					$s->send($username . "@" . " $host\r\n");
				} else {
					$s->send("$username\r\n");
				}
				$s->recv($response, 10000);
				alarm 0;
			};

			if ($@) {
				$timed_out = 1;
				print "[Child $$] Timeout for username $username on host $host\n" if $debug;
			}

			my $trace;
			if ($debug) {
				$trace = "[Child $$] $username\@$host: ";
			} else {
				$trace = "$username\@$host: ";
			}

			if ($response and not $timed_out) {

				# Negative result against solaris 7
				#
				# blah@10.0.0.1  Login       Name               TTY         Idle    When    Where
				# blah            ??? 
				if ($response =~ /Login\s+Name\s+TTY\s+Idle\s+When\s+Where.*$username\s+\?\?\?[^<>]+$/s) {
					print $parent $trace . " <no such user>\n";
					next;
				} 
				
				# Negative result for unknow finger daemon
				# It just returns a single "f" - presumably for "false"
				if ($response =~ /\s*f\s*$/) {
					print $parent $trace . " <no such user>\n";
					next;
				}

				#
				# Positive result against solaris 7
				#
				# if ($response =~ /^Login\s+Name\s+TTY\s+Idle\s+When\s+Where.*($username\s+[^?].*)/s) {
				# if ($response =~ /Login\s+Name\s+TTY\s+Idle\s+When\s+Where.*($username.*<.*>.*)/s) {
				if ($response =~ /Login\s+Name\s+TTY\s+Idle\s+When\s+Where.*($username.*)/s) {
					my $username_info = $1;
					$username_info =~ s/[\r\n]/./g; # Reduce to a single line
					print $parent $trace . " $username_info\n";
					next;
				}

				# Assume a positive response if we don't recognise the format
				$response =~ s/[\n\r]/./g;
				print $parent $trace . " $response\n";
				next;
			}

			if ($timed_out) {
				print $parent $trace . " <timeout>\n";
			} else {
				if (! $response) {
					print $parent $trace . " <no result>\n";
				}
			}
		}
		exit;
	}
}

# Fork once more to make a process that will us usernames and hosts
socketpair(my $get_next_query, my $parent, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or  die "socketpair: $!"; 
$get_next_query->autoflush(1);
$parent->autoflush(1);

# Parent executes this
if (my $pid = fork) {
	close $parent;

# Chile executes this
} else {
	# Generate queries from username-host pairs and send to parent
	foreach my $username (@usernames) {
		foreach my $host (@hosts) {
			my $query = $host . " \t" .  $username;
			print "[Query Generator] Sending $query to parent\n" if $debug;
			print $parent "$query\n";
		}
	}

	exit 0;
}

printf "Created %d child processes\n", scalar(@child_handles) if $debug;
my $s = IO::Select->new();
my $s_in = IO::Select->new();
$s->add(@child_handles);
$s_in->add(\*STDIN);
my $timeout = 0; # non-blocking
my $more_queries = 1;
my $outstanding_queries = 0;
my $query_count = 0;
my $result_count = 0;

# Write to each child process once
writeloop: foreach my $write_handle (@child_handles) {
	my $query = <$get_next_query>;
	if ($query) {
		chomp($query);
		print "[Parent] Sending $query to child\n" if $debug;
		print $write_handle "$query\n";
		$outstanding_queries++;
	} else {
		print "[Parent] Quitting main loop.   All queries have been read.\n" if $debug;
		last writeloop;
	}
}

# Keep reading from child processes until there are no more queries left
# Write to a child only after it has been read from
mainloop: while (1) {
	# Wait until there's a child that we can either read from or written to. 
	my ($rh_aref) = IO::Select->select($s, undef, undef); # blocking

	print "[Parent] There are " .  scalar(@$rh_aref) . "  children that can be read from\n" if $debug;

	foreach my $read_handle (@$rh_aref) {
		# Read from child
		chomp(my $line = <$read_handle>);
		if ($verbose == 1 or $debug == 1 or not ($line =~ /<no such user>/ or $line =~ /no result/ or $line =~ /<timeout>/)) {
			print "$line\n";
			$result_count++ unless ($line =~ /<no such user>/ or $line =~ /no result/ or $line =~ /<timeout>/);
		}
		$outstanding_queries--;
		$query_count++;

		# Write to child
		my $query = <$get_next_query>;
		if ($query) {
			chomp($query);
			print "[Parent] Sending $query to child\n" if $debug;
			print $read_handle "$query\n";
			$outstanding_queries++;
		} else {
			print "DEBUG: Quitting main loop.   All queries have been read.\n" if $debug;
			last mainloop;
		}
	}
}

# Wait to get replies back from remaining children
my $count = 0;
readloop: while ($outstanding_queries) {
	my @ready_to_read = $s->can_read(1); # blocking
	foreach my $child_handle (@ready_to_read) {
		print "[Parent] Outstanding queries: $outstanding_queries\n" if $debug;
		chomp(my $line = <$child_handle>);
		if ($verbose == 1 or $debug == 1 or not ($line =~ /<no such user>/ or $line =~ /no result/ or $line =~ /<timeout>/)) {
			print "$line\n";
			$result_count++ unless ($line =~ /<no such user>/ or $line =~ /no result/ or $line =~ /<timeout>/);
		}
		print $child_handle "$kill_child_string\n";
		$s->remove($child_handle);
		$outstanding_queries--;
		$query_count++;
	}
}

# Tell any remaining children to exit
foreach my $handle ($s->handles) {
	print "[Parent] Telling child to exit\n" if $debug;
	print $handle "$kill_child_string\n";
}

# Wait for all children to terminate
while(wait ! = -1) {};

print "######## Scan completed at " .  scalar(localtime()) . " ######### \n";
print "$result_count results.\n";
print "\n";
$end_time = time(); # Second granularity only to avoid depending on hires time module
my $run_time = $end_time - $start_time;
$run_time = 1 if $run_time < 1; # Avoid divide by zero
printf "%d queries in %d seconds (%0.1f queries / sec)\n", $query_count, $run_time, $query_count / $run_time;
