Kali Linux Network Scanning Cookbook
上QQ阅读APP看书,第一时间看更新

Using Scapy to perform layer 3 discovery

Scapy is a tool that allows the user to craft and inject custom packets into the network. This tool can be leveraged to build ICMP protocol requests and inject them into the network to analyze the response. This specific recipe will demonstrate how to use Scapy to perform layer 3 discovery on remote hosts.

Getting ready

Using Scapy to perform layer 3 discovery does not require a lab environment, as many systems on the Internet will reply to ICMP echo requests. However, it is highly recommended that you perform any type of network scanning exclusively in your own lab unless you are thoroughly familiar with the legal regulations imposed by any governing authorities to whom you are subject. If you wish to perform this technique within your lab, you will need to have at least one system that will respond to ICMP requests. In the examples provided, a combination of Linux and Windows systems are used. For more information on setting up systems in a local lab environment, please refer to the the Installing Metasploitable2 and Installing Windows Server recipes in Chapter 1, Getting Started. Additionally, this section will require a script to be written to the filesystem, using a text editor such as VIM or Nano. For more information on writing scripts, please refer to the Using text editors (VIM and Nano) recipe in Chapter 1, Getting Started.

How to do it...

In order to send an ICMP echo request using Scapy, we will need to start stacking layers to send requests. A good rule of thumb when stacking packets is to work up through the layers of the OSI model. You can stack multiple layers by separating each layer with a forward slash. To generate an ICMP echo request, an IP layer needs to be stacked with an ICMP request. To get started, use the scapy command to open the Scapy interactive console, and then assign an IP object to a variable:

root@KaliLinux:~# scapy
Welcome to Scapy (2.2.0)
>>> ip = IP()
>>> ip.display()
###[ IP ]###
 version= 4
 ihl= None
 tos= 0x0
 len= None
 id= 1
 flags= 
 frag= 0
 ttl= 64
 proto= ip
 chksum= None
 src= 127.0.0.1
 dst= 127.0.0.1
 \options\

In the example provided, the display function was used to view the default configurations of the object attributes after it was assigned to the ip variable. By default, the IP object is configured to send and receive using the loopback IP address of 127.0.0.1. To change any attribute of an object in Scapy, you need to set [object].[attribute] equal to the desired value. In this case, we want to change the destination IP address to the address of the system that we would like to send the ICMP request to, as shown in the following set of commands:

>>> ip.dst = "172.16.36.135"
>>> ip.display()
###[ IP ]###
 version= 4
 ihl= None
 tos= 0x0
 len= None
 id= 1
 flags= 
 frag= 0
 ttl= 64
 proto= ip
 chksum= None
 src= 172.16.36.180
 dst= 172.16.36.135
 \options\

After assigning the new value to the destination address attribute, the changes can be verified by calling the display() function once again. Notice that when the destination IP address value is changed to any other value, the source address is also automatically updated from the loopback address to the IP address associated with the default interface. Now that the attributes of the IP object have been appropriately modified, we will need to create the second layer in our packet stack. The next layer to be added to the stack is the ICMP layer, which we will assign to a separate variable:

>>> ping = ICMP()
>>> ping.display()
###[ ICMP ]###
 type= echo-request
 code= 0
 chksum= None
 id= 0x0
 seq= 0x0

In the example provided, the ICMP object was initialized with the ping variable name. The display() function can then be called to display the default configurations of the ICMP attributes. To perform an ICMP echo request, the default configurations are sufficient. Now that both layers have been configured correctly, they can be stacked in preparation to send. In Scapy, layers can be stacked by separating each layer with a forward slash. Have a look at the following set of commands:

>>> ping_request = (ip/ping)
>>> ping_request.display()
###[ IP ]###
 version= 4
 ihl= None
 tos= 0x0
 len= None
 id= 1
 flags= 
 frag= 0
 ttl= 64
 proto= icmp
 chksum= None
 src= 172.16.36.180
 dst= 172.16.36.135
 \options\
###[ ICMP ]###
 type= echo-request
 code= 0
 chksum= None
 id= 0x0
 seq= 0x0

Once the stacked layers have been assigned to a variable, the display() function will then show the entire stack. The process of stacking layers in this manner is often referred to as datagram encapsulation. Now that the layers have been stacked, the request is ready to be sent across the wire. This can be done using the sr1() function in Scapy:

>>> ping_reply = sr1(ping_request)
..Begin emission:
.........Finished to send 1 packets.
...*
Received 15 packets, got 1 answers, remaining 0 packets
>>> ping_reply.display()
###[ IP ]###
 version= 4L
 ihl= 5L
 tos= 0x0
 len= 28
 id= 62577
 flags= 
 frag= 0L
 ttl= 64
 proto= icmp
 chksum= 0xe513
 src= 172.16.36.135
 dst= 172.16.36.180
 \options\
###[ ICMP ]###
 type= echo-reply
 code= 0
 chksum= 0xffff
 id= 0x0
 seq= 0x0
###[ Padding ]###
 load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

In the example provided, the sr1() function is assigned to the ping_reply variable. This executes the function and then passes the result to this variable. After receiving the response, the display() function is used on the ping_reply variable to see the contents of the response. Notice that this packet was sent from the host to which we sent the initial request, and the destination address is the IP address of our Kali system. Additionally, notice that the ICMP type of the response is an echo reply. This process of sending and receiving ICMP with Scapy may seem functional, based on this example, but if you attempt to use the same process with a nonresponsive target address, you will quickly notice the problem:

>>> ip.dst = "172.16.36.136"
>>> ping_request = (ip/ping)
>>> ping_reply = sr1(ping_request)
.Begin emission:
............................................................................................................................................................. Finished to send 1 packets ......................................................................................................
*** {TRUNCATED} ***

The example output was truncated, but this output will continue indefinitely until you force an escape with Ctrl + C. Without supplying a timeout value to the function, the sr1() function will continue to listen until a response is received. If a host is not live or if the IP address is not associated with any host, no response will be sent, and the function will not exit. To use this function effectively within a script, a timeout value should be defined:

>>> ping_reply = sr1(ping_request, timeout=1)
.Begin emission:
.............................................................................................................................................. Finished to send 1 packets.
....................................
Received 3982 packets, got 0 answers, remaining 1 packets

By supplying a timeout value as a second argument passed to the sr1() function, the process will then exit if no response is received within the designated number of seconds. In the example provided, the sr1() function is used to send the ICMP request to a nonresponsive address that is exited after 1 second because no response was received. In the examples provided so far, we have assigned functions to variables to create objects that are persistent and can be manipulated. However, these functions do not have to be assigned to variables but can also be generated by calling the functions directly:

>>> answer = sr1(IP(dst="172.16.36.135")/ICMP(),timeout=1)
.Begin emission:
...*Finished to send 1 packets.

Received 5 packets, got 1 answers, remaining 0 packets
>>> response.display()
###[ IP ]###
 version= 4L
 ihl= 5L
 tos= 0x0
 len= 28
 id= 62578
 flags= 
 frag= 0L
 ttl= 64
 proto= icmp
 chksum= 0xe512
 src= 172.16.36.135
 dst= 172.16.36.180
 \options\
###[ ICMP ]###
 type= echo-reply
 code= 0
 chksum= 0xffff
 id= 0x0
 seq= 0x0
###[ Padding ]###
 load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

In the example provided here, all of the work that was done earlier with four separate commands can actually be accomplished with a single command by directly calling the functions. Notice that if an ICMP request is sent to an IP address that does not reply within the timeframe specified by the timeout value, calling the object will result in an exception. As no response was received, the answer variable in this example that was set equal to the response is never initialized:

>>> answer = sr1(IP(dst="83.166.169.231")/ICMP(),timeout=1)
Begin emission:
..........................................Finished to send 1 packets.
...................................................................................................
Received 1180 packets, got 0 answers, remaining 1 packets
>>> answer.display()
Traceback (most recent call last):
 File "<console>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'display'

Knowledge of these varied responses can be used to generate a script that will perform ICMP requests on multiple IP addresses in sequence. The script will loop through all of the possible values for the last octet in the destination IP address, and for each value, it will send an ICMP request. As each sr1() function is returned, the response is evaluated to determine if an echo response was received:

#!/usr/bin/python

import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
from scapy.all import *

if len(sys.argv) != 2:
   print "Usage - ./pinger.py [/24 network address]"
   print "Example - ./pinger.py 172.16.36.0"
   print "Example will perform an ICMP scan of the 172.16.36.0/24 range"
   sys.exit()

address = str(sys.argv[1])
prefix = address.split('.')[0] + '.' + address.split('.')[1] + '.' + address.split('.')[2] + '.'

for addr in range(1,254):
   answer=sr1(ARP(pdst=prefix+str(addr)),timeout=1,verbose=0)
   if answer == None:
      pass
   else:
      print prefix+str(addr)

The first line of the script indicates where the Python interpreter is located so that the script can be executed without it being passed to the interpreter. The script then imports all Scapy functions and also defines Scapy logging levels to eliminate unnecessary output in the script. The second block of code is a conditional test that evaluates if the required argument is supplied to the script. If the required argument is not supplied upon execution, the script will then output an explanation of appropriate script usage. This explanation includes the usage of the tool, an example, and an explanation of the task that will be performed by this example. After this block of code, the supplied value is assigned to the address variable. That value is then used to extract the network prefix. For example, if the address variable contains the 192.168.11.0 string , the value of 192.168.11. will be assigned to the prefix variable.

The final block of code is a for loop that performs the actual scanning. The for loop cycles through all values between 0 and 254, and for each iteration, the value is then appended to the network prefix. In the case of the example provided earlier, an ICMP echo request would be sent to each IP address between 192.168.11.0 and 192.168.11.254. For each live host that does reply, the corresponding IP address is then printed to the screen to indicate that the host is alive on the LAN. Once the script has been written to the local directory, you can execute it in the terminal using a period and forward slash, followed by the name of the executable script:

root@KaliLinux:~# ./pinger.py 
Usage - ./pinger.py [/24 network address]
Example - ./pinger.py 172.16.36.0
Example will perform an ICMP scan of the 172.16.36.0/24 range
root@KaliLinux:~# ./pinger.py 172.16.36.0
172.16.36.2
172.16.36.1
172.16.36.132
172.16.36.135

If the script is executed without any arguments supplied, the usage is output to the screen. The usage output indicates that this script requires a single argument that defines the /24 network to scan. In the example provided, the script is executed using the 172.16.36.0 network address. The script then outputs a list of live IP addresses on the /24 network range. This output can also be redirected to an output text file using the right-angled bracket, followed by the output filename. An example of this is as follows:

root@KaliLinux:~# ./pinger.py 172.16.36.0 > output.txt
root@KaliLinux:~# ls output.txt
output.txt
root@KaliLinux:~# cat output.txt
172.16.36.1
172.16.36.2
172.16.36.132
172.16.36.135

The ls command can then be used to verify that the output file was written to the filesystem, or the cat command can be used to view its contents. This script can also be modified to accept a list of IP addresses as input. To do this, the for loop must be changed to loop through the lines that are read from the specified text file. An example of this can be seen as follows:

#!/usr/bin/python

import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
from scapy.all import *

if len(sys.argv) != 2:
   print "Usage - ./pinger.py [filename]"
   print "Example - ./pinger.py iplist.txt"
   print "Example will perform an ICMP ping scan of the IP addresses listed in iplist.txt"
   sys.exit()

filename = str(sys.argv[1])
file = open(filename,'r')

for addr in file:
   ans=sr1(IP(dst=addr.strip())/ICMP(),timeout=1,verbose=0)
   if ans == None:
      pass
   else:
      print addr.strip()

The only major difference from the prior script is that this one accepts an input filename as an argument and then loops through each IP address listed in this file to scan. Similar to the other script, the resulting output will include a simple list of IP addresses associated with systems that responded to the ICMP echo request with an ICMP echo response:

root@KaliLinux:~# ./pinger.py 
Usage - ./pinger.py [filename]
Example - ./pinger.py iplist.txt
Example will perform an ICMP ping scan of the IP addresses listed in iplist.txt
root@KaliLinux:~# ./pinger.py iplist.txt
172.16.36.1
172.16.36.2
172.16.36.132
172.16.36.135

The output of this script can be redirected to an output file in the same way. Execute the script with the input file supplied as an argument and then redirect the output using a right-angled bracket, followed by the name of the output text file. An example of this can be seen as follows:

root@KaliLinux:~# ./pinger.py iplist.txt > output.txt
root@KaliLinux:~# ls output.txt
output.txt
root@KaliLinux:~# cat output.txt
172.16.36.1
172.16.36.2
172.16.36.132
172.16.36.135

How it works…

ICMP layer 3 discovery was performed here with Scapy by crafting a request that includes both an IP layer and an appended ICMP request. The IP layer allowed the packet to be routed outside the local network, and the ICMP request was used to solicit a response from the remote system. Using this technique in a Python script, this task can be performed in sequence to scan multiple systems or entire network ranges.