Tuesday, 22 March 2011

More efficient port-scanning in Python and Perl

Sometimes it is useful to be able to perform some TCP port-scanning from a host, without installing any additional tools (such as nmap for example). This could be performed from intermediary systems which have been compromised in an attack, for example.

If the host in question has Python or Perl installed (or many other languages for that matter) then in should be fairly simple to write and customize short scripts for specific tasks such as banner-grabbing and port-scanning.

It is useful to optimize these tasks, to reduce the footprint and make the task more efficient. Here I look at a simple example of two port-scanners I have optimized, to scan a minimal number of the most popular TCP ports.

I had several aims here; to be lightweight, targeted, cross-platform, efficient, and without installing any new software (and with minimal changes to the scanning machine, other than the uploading the script).

Additionally, it would be good to scan the most important ports first, and the rest in decreasing order of importance.

These techniques are for educational and testing purposes only.


Port-scanning in Perl

Here is a simple port-scanner in Perl that I found online:

#!/usr/bin/perl


use IO::Socket;
my ( $target, $daddr, $port, $maxport );


$maxport=1024; $port=0;


( $target = $ARGV[0] ) || &error;
$port=$ARGV[1] if $ARGV[1];
$maxport=$ARGV[2]  if $ARGV[2];


$daddr = inet_aton($target) || die("Failed to find host: $target");


print "Scanning : $target ports $port to $maxport\n";
foreach (; $port<=$maxport; $port++) {
    print "\nPort $port is open" if ( IO::Socket::INET->new(PeerAddr=>"$target:$port",Proto=>'tcp',Timeout=>1));
}
print "\nDone\n";
exit (0);


sub error {
    print "./portscan.pl \n";
    exit (1);
}

This is fairly basic, and scans the first 1024 TCP ports (unless you specify otherwise). However, this is a pretty slow and inefficient way to do this, as many of the top 1024 ports are not used, and some of the higher ports are more important.


Finding the most important ports from the nmap configuration files

A lot of research has been done by the nmap team in order to find the statistically most popular ports. Rather than reinventing the wheel here, let's reuse some of this information.

Port information and statistics are stored in the "nmap-services" file in /usr/share/nmap". This contains all the information we need, but in the wrong format. We will parse this file to extract and reformat the information we need.

To do this we need to remove the comments, reverse the order of the first 3 parameters, sort it, filter the tcp ports, and take the top 150 lines, and trim them, as follows:

grep -v ^# /usr/share/nmap/nmap-services | awk '{ print $3 " " $2 " " $1 }' | sort -r | grep  "/tcp " | head -n 150 | cut -d " " -f2 | cut -d "/" -f1 > top150tcp.txt

Next, we will put this data in a form which is ready to go into our port-scanner.

cat top150tcp.txt | sed 's/$/,/' | tr "\n" " ">  scriptlist150.txt

This creates our port list in the following form, ready to go into our Perl or Python script:

80, 23, 443, 21, 22, 25, 3389, 110, 445, 139, 143, 53, ...

(This process could be customised to find any number of top ports. 3000 is a good number as that covers pretty much 100% of popular ports, but I have chosen 150 ports here for brevity)


Optimized Perl script

So, with a bit of adjustment of our Perl script, and the addition of the prioritised port-list, we have:

#!/usr/bin/perl


use IO::Socket;
my ( $target, $daddr );


@portlist = (80, 23, 443, 21, 22, 25, 3389, 110, 445, 139, 143, 53, 135, 3306, 8080, 1723, 111, 995, 993, 5900, 1025, 587, 8888, 199, 1720, 465, 548, 113, 81, 6001, 10000, 514, 5060, 179, 1026, 2000, 8443, 8000, 32768, 554, 26, 1433, 49152, 2001, 515, 8008, 49154, 1027, 5666, 646, 5000, 5631, 631, 49153, 8081, 2049, 88, 79, 5800, 106, 2121, 1110, 49155, 6000, 513, 990, 5357, 427, 49156, 543, 544, 5101, 144, 7, 389, 8009, 3128, 444, 9999, 5009, 7070, 5190, 3000, 5432, 3986, 1900, 13, 1029, 9, 6646, 5051, 49157, 1028, 873, 1755, 2717, 4899, 9100, 119, 37, 1000, 3001, 5001, 82, 10010, 1030, 9090, 2107, 1024, 2103, 6004, 1801, 5050, 19, 8031, 1041, 255, 3703, 2967, 1065, 1064, 1056, 1054, 1053, 1049, 1048, 17, 808, 3689, 1031, 1071, 1044, 5901, 9102, 100, 9000, 8010, 5120, 4001, 2869, 1039, 2105, 636, 1038, 2601, 7000, 1, 1069, 1066, 625);
$size = @portlist;


( $target = $ARGV[0] ) || &error;


$daddr = inet_aton($target) || die("Failed to find host: $target");


print "Scanning : $target\n";
for ($i=0; $i<$size; $i++)
{
    print "\nPort $portlist[$i] is open" if ( IO::Socket::INET->new(PeerAddr=>"$target:$portlist[$i]",Proto=>'tcp',Timeout=>1));
}
print "\nDone\n";
exit (0);


sub error {
    print "./portscan.pl \n";
    exit (1);
}

This runs in about a 6th of the time, scanning only the most popular 150 ports rather than the lowest 1024.

As it is a nonlinear scan it is less likely to be detected, and could easily be slowed, with a delay between each port, to further increase the stealthiness.


The same in Python

Having used Python more than Perl, I find Perl a bit ugly, so here is the same sort of customization for a Python script:

#!/usr/bin/python
import sys
from socket import * 


if len(sys.argv) != 2:
print "Usage: " + sys.argv[0] + ""
sys.exit(0)

portlist = [80, 23, 443, 21, 22, 25, 3389, 110, 445, 139, 143, 53, 135, 3306, 8080, 1723, 111, 995, 993, 5900, 1025, 587, 8888, 199, 1720, 465, 548, 113, 81, 6001, 10000, 514, 5060, 179, 1026, 2000, 8443, 8000, 32768, 554, 26, 1433, 49152, 2001, 515, 8008, 49154, 1027, 5666, 646, 5000, 5631, 631, 49153, 8081, 2049, 88, 79, 5800, 106, 2121, 1110, 49155, 6000, 513, 990, 5357, 427, 49156, 543, 544, 5101, 144, 7, 389, 8009, 3128, 444, 9999, 5009, 7070, 5190, 3000, 5432, 3986, 1900, 13, 1029, 9, 6646, 5051, 49157, 1028, 873, 1755, 2717, 4899, 9100, 119, 37, 1000, 3001, 5001, 82, 10010, 1030, 9090, 2107, 1024, 2103, 6004, 1801, 5050, 19, 8031, 1041, 255, 3703, 2967, 1065, 1064, 1056, 1054, 1053, 1049, 1048, 17, 808, 3689, 1031, 1071, 1044, 5901, 9102, 100, 9000, 8010, 5120, 4001, 2869, 1039, 2105, 636, 1038, 2601, 7000, 1, 1069, 1066, 625]


if __name__ == '__main__':
target = sys.argv[1]
print 'Scanning : ', target


for i in portlist:
s = socket(AF_INET, SOCK_STREAM)


result = s.connect_ex((target, i))


if(result == 0) :
print 'Port ' + str(i) + ' is open'
s.close()

That gives the following result for my home broadband router:

./portscan2.py 192.168.1.254
Scanning :  192.168.1.254
Port 80 is open
Port 23 is open
Port 443 is open
Port 21 is open
Port 1723 is open

As you can see, it is fairly straightforward to find existing code on the web, and tweak it (with some statistical knowledge) to get better results. This is something I find myself doing a lot more of recently.

Of course, you could preserve more information from the nmap stats, such as port-names, by making use of a dictionary array.

If you find this useful, let me have your feedback below.

3 comments:

  1. Here is a cleaner version of that Perl: http://pastie.org/1801148

    ReplyDelete
  2. Thanks chao-mu
    I will try that out.

    I hope the Railgun dev is going well.

    Ben

    ReplyDelete
  3. do you have any other nice scripts in Py?
    Well done
    moni

    ReplyDelete