18 nov. 2008

Network recon

Voici un petit morceau de python pour tenir un peu le choc face à une météo pourrie et une fatigue très en forme (hein??)

Il s'agit d'un petit shell permettant d'effectuer simplement des opérations de découverte réseau. Il a été volontairement limité à une version sans RAW_SOCKET, le but étant de découvrir un réseau sans droit root sur aucune machine. Son utilisation et son code sont relativement simples. Je me suis un peu inspiré de la console de metasploit dans l'esprit.

Je vous laisse découvrir. La commande help ou la lecture du code vous guidera entre les écailles du Python (merci).




#!/usr/bin/env python

#
#
# Netreckon is a discovery shell designed to help
# a user to map an unknown IP network using
# several techniques
#
# Some parts inspired from a sample code from James Thiele
# available here : http://www.eskimo.com/~jet/python/examples/cmd/console.py
#

# Copyright (c) 2008, Hth
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the Hth nor the names of its contributors
# may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#
#


import os
import sys
import cmd
import time
import random
import socket
# The following 'readline' module provides some bash like shortcuts
import readline



# No default timeout else...
socket.setdefaulttimeout( 1.0 )


# Global variables are all stored there to avoid conflicts
global_vars = {
# Software version
'version' : '0.1 dev',
# Author
'author' : 'Hth networks'
}



class NetReckon:
"""
Higher level stuff which reates the console
the user can see launching the script
"""
def __init__( self ):
"""
shell initialization
"""
# launch console
console = NetReckonConsole()
try:
console.cmdloop()
except:
self.exit_failure()


def exit_failure( self ):
""" Exit with return code 1 """
sys.exit( 1 )



class NetReckonConsole( cmd.Cmd ):
"""
Perform reckon actions according
to user's inputs
(interactive console)
"""

def __init__( self ):
"""
startup actions engine
"""
cmd.Cmd.__init__( self )
self.prompt = '>> '
self.intro = """
NetReckon < interactive user network recon console >

---[ %s ]
---[ %s ]

* Starting session at %s *
"""%( global_vars['version'], global_vars['author'], time.ctime() ) # As mentionned above, we insert version, author handle and actual date
# user defined variables are stored there to be used as arguments by actions
self.userVariables = {}


def do_set( self, argstr ):
"""
set a variable to a supplied value

"""
arglist = argstr.split() # Convert arguments from a string to a list
if len( arglist ) == 2:
self.userVariables[arglist[0]] = arglist[1]
else:
print 'Incorrect syntax!'
print '>> set '


def help_set( self ):
"""
Help about the `set` method
"""
print 'Create (or change the value of) a variable'
print 'set '


def do_unset( self, argstr ):
"""
unset a variable in the list
"""
if self.userVariables.has_key( argstr ):
try:
del self.userVariables[argstr]
except:
print 'Incorrect syntax!'
print 'unset '
else:
print 'Unknown variable supplied : %s'%argstr


def help_unset( self ):
"""
help about the `unset` method
"""
print 'Delete a previously defined variable'
print 'unset '


def do_show( self, argstr ):
"""
show stored variables
"""
print 'User variables :\n'
for entry in self.userVariables.keys():
print '%s\t%s'%(entry, self.userVariables[entry])


def help_show( self ):
"""
help about the `show` method
"""
print 'print out stored variables names and values'


def do_exit( self, argstr ):
"""
exit netreckon
"""
sys.exit( argstr )


def help_exit( self ):
"""
help about he `exit` method (...)
"""
print 'Exit netreckon'
print 'A message or a return code can be supplied as an argument :'
print 'exit -1'


#-- Command definitions to support Cmd object functionality --#
def do_shell( self, argstr ):
"""
execute a shell command
"""
os.system( argstr )


def do_EOF( self, args ):
"""
Exit on system end of file character
"""
return self.do_exit( args )


def emptyline( self ):
"""
Do nothing on empty input line
"""
pass


def do_help( self, argstr ):
"""
print out help on demand
"""
cmd.Cmd.do_help( self, argstr )


def help_help( self ):
"""
do we really have to explain this sir?
"""
print 'print stuff like this one'

#-- ------- ----------- -- ------- --- ------ ------------- --#
#-- Override methods in Cmd object --#

def preloop( self ):
"""
Called just before the main loop starts.
Call the initial preloop() and prepare variable dicts
to let user execute some python code later
"""
cmd.Cmd.preloop( self ) # sets up command completion
self._locals = {}
self._globals = {}


def postloop( self ):
"""
Finish everything that has to be
"""
cmd.Cmd.postloop( self ) # Clean up command completion
print ''
#-- -------- ------- -- --- ------ --#

def do_python( self, argstr ):
"""
Execute the command as python code
"""
try:
exec( argstr ) in self._locals, self._globals
except Exception, e:
print e.__class__, ":", e


def help_python( self ):
"""
Help about the `python` command
"""
print 'Execute the line as python code.'
print 'A context of variables is maintained separately'
print 'from the `set` variables for python code'
print 'eg. python a=2; print a'


## DNS Resolution

def do_dns( self, argstr ):
"""
Find out IP address of a known hostname
"""
arglist = argstr.split()
resolver = DnsResolver()

if len( arglist ) >= 1:
resolver.do_dns_resolution( arglist )
else:
print 'dns ...'


def help_dns( self ):
"""
Provide help concerning `dns` instruction
"""
print 'Try to resolve hostname(s) into IP addresses'
print 'dns ...'


## Reverse DNS Resolution
def do_revdns( self, argstr ):
"""
Perform reverse DNS requests
to discover a network
"""
ip_range = ''

# Can be called without argument if NETWORK has been set
if argstr != '':
ip_range = argstr
elif self.userVariables.has_key( 'NETWORK' ):
ip_range = self.userVariables['NETWORK']
else:
# else exit function
print 'No argument supplied'
self.help_revdns()
return

# Do reverse DNS resolution once arguments are parsed
resolver = DnsResolver()
resolver.do_reverse_dns_resolution( ip_range )


def help_revdns( self ):
"""
Provide some help for the `revdns` instruction
"""
print 'Try to resolve IP addresses into hostnames'
print 'both syntaxes are recognized :'
print 'revdns 192.168.1.1-5'
print 'and'
print 'set NETWORK 192.0-75.0-255.0-255'
print 'revdns'


## TCP connect() scan
def do_scantcp( self, argstr ):
"""
scan for open tcp ports of a given host
"""
if argstr != '':
ip_range = argstr
elif self.userVariables.has_key( 'TARGET' ):
ip_range = self.userVariables['TARGET']
else:
print 'No argument supplied'
self.help_scantcp()
return

scan = PortScanner()
if self.userVariables.has_key( 'PORTS' ):
scan.parse_port_sequence( self.userVariables['PORTS'] )
scan.connect_scan( ip_range )


def help_scantcp( self ):
"""
Provide some help for the `scantcp` instruction
"""
print 'perform a tcp connect() scan against target(s)'
print 'in order to find out open ports'
print 'You can set the PORTS variable to specify ports to scan'
print '(eg. set PORTS 21-90)'
print 'Targets can follow the instruction this way : '
print 'scantcp 192.168.1.0-5'
print 'or using the TARGET variable'
print 'set TARGET 192.168.1.0-5'
print 'scantcp'



class DnsResolver:
"""
Perform DNS operations
on ranges of IP addresses
"""
def __init__( self ):
pass


def do_dns_resolution( self, hostnames ):
"""
Find out IP address of a known hostname
"""
for hostname in hostnames:
try:
print '\t%-25s\t\t%s'%( hostname, socket.gethostbyname( hostname ) )
except:
print '\t%-25s\t\t(unknown host)'%hostname


def do_reverse_dns_resolution( self, addresses ):
"""
Perform reverse DNS requests
to discover a network
"""
addressesRange = AddrExtract( addresses )
while True:
addr = addressesRange.next()
if addr == '':
break
try:
host = socket.gethostbyaddr( addr )
print '\t%s\t\t%s'%( addr, host[0] )
except:
continue



class PortScanner:
"""
Perform basic port scanning
using tcp connect() scan.
Parse and randomize ranges of ports
"""
def __init__( self ):
"""
Set properties to their default values
"""
# default behavior is to scan the whole ports range
self.portList = range( 1, 65535 + 1 )
# used to print out open ports in a clean way
self.openPorts = []


def parse_port_sequence( self, portSeqStr ):
"""
build a list of ports to scan
from a string like 21,22,42-80
"""
# erase whatever has been saved before
self.portList = []
tmpList = []
for token in portSeqStr.split( ',' ):
tmpList.append( token )
for elt in tmpList:
if elt.count( '-' ) == 1:
for port in self.__get_ports_from_range( elt ):
self.portList.append( port )
else:
try:
self.portList.append( int( elt ) )
except:
print 'Invalid port supplied %s'%elt
continue


def __get_ports_from_range( self, portRange ):
"""
convert a string like 1-5
to a list of ports ( [1, 2, 3, 4, 5] )


"""
tmpRange = []
try:
# split string into start and stop values
subRange = portRange.split( '-' )
start = int( subRange[0] )
stop = int( subRange[1] ) + 1
# and add each port included between
for port in range( start, stop ):
tmpRange.append( port )
except:
# empty list is returned if an exception occurs
tmpRange = []
finally:
# list is returned whatever happens
return tmpRange


def connect_scan( self, targets ):
"""
Scan every host in a range of IP addresses
"""
addressesRange = AddrExtract( targets )
if addressesRange != None:
while True:
addr = addressesRange.next()
if addr == '':
break
try:
self.scan_host( addr )
except:
continue
else:
print 'Invalid IP range supplied!'



def scan_host( self, target ):
"""
Perform tcp connect() scan
"""
random.shuffle( self.portList )
self.openPorts = []

for probedPort in self.portList:
s = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
try:
s.connect( ( target, probedPort ) )
self.openPorts.append( probedPort )
except socket.error:
pass
finally:
s.close()
# Once this is done :
self.__print_results( target )


def __print_results( self, target ):
"""
print out the list of open ports for scanned host
"""
print 'Open (tcp) ports on %s :'%( target )
print '( %i probed and %i found open )'%( len( self.portList ), len( self.openPorts ) )
self.openPorts.sort()
if len( self.openPorts ) == 0:
print ''
for port in self.openPorts:
print 'Port %i open'%port
print ''



class AddrExtract:
"""
find out every IP addresses
in a memory efficient way
from a string describing ranges of
addresses like 192.168.0-5.0.255
"""
def __init__( self, input ):
"""
extracter startup
"""
self.addressesParts = {
'A' : {
'start' : 0,
'stop' : 0
},
'B' : {
'start' : 0,
'stop' : 0
},
'C' : {
'start' : 0,
'stop' : 0
},
'D' : {
'start' : 0,
'stop' : 0
}
}
self.stringRange = input
self.nextAddress = ''
if not self.split_string():
self.reset_all()


def split_string( self ):
"""
split input string into
4 parts of IP addresses
"""
parts = self.stringRange.split( '.' )
# Number of fields must be 4 (IPv4 only...)
if len( parts ) != 4:
print 'Invalid IPv4 addresses range supplied : %s'%self.stringRange
return False
# Get ranges supplied in each field
for i in ['A', 'B', 'C', 'D']:
try:
self.addressesParts[i]['start'] = int( parts[ord(i) - ord('A')] ) # this is not a C program??
# On the following line we assign the starting value to the 'stop' field if the address field has not be specified as a range
# (otherwise an exception would have been raised above)
self.addressesParts[i]['stop'] = self.addressesParts[i]['start']
except:
try:
start, stop = parts[ord(i) - ord('A')].split( '-' )
self.addressesParts[i]['start'] = int( start )
self.addressesParts[i]['stop'] = int( stop )
except:
print 'Invalid IPv4 addresses range supplied : %s'%self.stringRange
return False
# Check computed values
if not self.check_addresses_range_values():
return False
# If we are there, then assume it's OK
return True


def check_addresses_range_values( self ):
"""
Are supplied values Ok for IP addresses ?
"""
for k in self.addressesParts.keys():
if self.addressesParts[k]['start'] not in range(0, 256):
print 'Invalid addresses Range supplied : %s'%self.stringRange
return False
if self.addressesParts[k]['stop'] not in range(0, 256):
print 'Invalid addresses Range supplied : %s'%self.stringRange
return False
return True


def reset_all( self ):
"""
Reset every field of addressesParts
to zero
"""
for key in self.addressesParts.keys():
for entry in self.addressesParts[key].keys():
self.addressesParts[key][entry] = 0


def next( self ):
"""
return the next IP address of the range
without storing the whole list of addresses
contained in this range
"""
# On first call
if self.nextAddress == '':
# Writing the first IP address of the range using format string and computed values
self.nextAddress = '%i.%i.%i.%i'%(
self.addressesParts['A']['start'],
self.addressesParts['B']['start'],
self.addressesParts['C']['start'],
self.addressesParts['D']['start'] )
# Catch some misinitializations...
if self.nextAddress == '0.0.0.0':
return ''
else:
return self.nextAddress
# For following calls
a, b, c, d = self.nextAddress.split( '.' )
a = int( a )
b = int( b )
c = int( c )
d = int( d )
# The big if - then - else labyrinth...
if d < self.addressesParts['D']['stop']:
d += 1
else:
d = self.addressesParts['D']['start']
if c < self.addressesParts['C']['stop']:
c += 1
else:
c = self.addressesParts['C']['start']
if b < self.addressesParts['B']['stop']:
b += 1
else:
b = self.addressesParts['B']['start']
if a < self.addressesParts['A']['stop']:
a += 1
else:
return ''

self.nextAddress = '%i.%i.%i.%i'%( a, b, c, d )
return self.nextAddress





# So all that stuff started here?!
if __name__ == '__main__':
NetReckon()






Voila, j'espère que le script plaira. C'est entre autre un exemple assez complet de l'utilisation du module cmd de python qui permet de réaliser facilement ce type de consoles avec historique des commandes, gestion de l'aide etc.

C'est tellement marrant la recon! =D

Aucun commentaire: