Using Scapy to perform layer 2 discovery
Scapy is a powerful interactive tool that can be used to capture, analyze, manipulate, and even create protocol-compliant network traffic, which can then be injected into the network. Scapy is also a library that can be used in Python, thereby offering the capability to create highly effective scripts to perform network traffic handling and manipulation. This specific recipe will demonstrate how to use Scapy to perform ARP discovery and how to create a script using Python and Scapy to streamline the layer 2 discovery process.
Getting ready
To use Scapy to perform ARP discovery, you will need to have at least one system on the Local Area Network (LAN) that will respond to ARP 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 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…
To understand how ARP discovery works, we will start by using Scapy to craft custom packets that will allow us to identify hosts on the LAN using ARP. To begin using Scapy in Kali Linux, enter the scapy
command from the terminal. You can then use the display()
function to see the default configurations for any ARP object created in Scapy in the following manner:
root@KaliLinux:~# scapy Welcome to Scapy (2.2.0) >>> ARP().display() ###[ ARP ]### hwtype= 0x1 ptype= 0x800 hwlen= 6 plen= 4 op= who-has hwsrc= 00:0c:29:fd:01:05 psrc= 172.16.36.232 hwdst= 00:00:00:00:00:00 pdst= 0.0.0.0
Notice that both the IP and MAC source addresses are automatically configured to the values associated with the host on which Scapy is being run. Except in the case that you are spoofing an alternate source address, these values will never have to be changed for any Scapy objects. The default opcode value for ARP is automatically set to who-has
, which designates that the packet will be requesting an IP and MAC association. In this case, the only value we need to supply is the destination IP address. To do this, we can create an object using the ARP function by setting it equal to a variable. The name of the variable is irrelevant (in the example provided, the variable name, arp_request
, is used). Have a look at the following commands:
>>> arp_request = ARP() >>> arp_request.pdst = "172.16.36.135" >>> arp_request.display() ###[ ARP ]### hwtype= 0x1 ptype= 0x800 hwlen= 6 plen= 4 op= who-has hwsrc= 00:0c:29:65:fc:d2 psrc= 172.16.36.132 hwdst= 00:00:00:00:00:00 pdst= 172.16.36.135
Notice that the display()
function can also be applied to the created ARP object to verify that the configuration values have been updated. For this exercise, use a destination IP address that corresponds to a live machine in your lab network. The sr1()
function can then be used to send the request over the wire and return the first response:
>>> sr1(arp_request) Begin emission: ......................................*Finished to send 1 packets. Received 39 packets, got 1 answers, remaining 0 packets <ARP hwtype=0x1 ptype=0x800 hwlen=6 plen=4 op=is-at hwsrc=00:0c:29:3d:84:32 psrc=172.16.36.135 hwdst=00:0c:29:65:fc:d2 pdst=172.16.36.132 |<Padding load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>
Alternatively, you can perform the same task by calling the function directly and passing any special configurations as arguments to it, as shown in the following command. This can avoid the clutter of using unnecessary variables and can also allow the completion of the entire task in a single line of code:
>>> sr1(ARP(pdst="172.16.36.135")) Begin emission: .........................*Finished to send 1 packets. Received 26 packets, got 1 answers, remaining 0 packets <ARP hwtype=0x1 ptype=0x800 hwlen=6 plen=4 op=is-at hwsrc=00:0c:29:3d:84:32 psrc=172.16.36.135 hwdst=00:0c:29:65:fc:d2 pdst=172.16.36.132 |<Padding load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>
Notice that in each of these cases, a response is returned, indicating that the IP address of 172.16.36.135
is at the MAC address of 00:0C:29:3D:84:32
. If you perform the same task, but instead, assign a destination IP address that does not correspond to a live host on your lab network, you will not receive any response, and the function will continue to analyze the incoming traffic on the local interface indefinitely.
You can force the function to stop using Ctrl + C. Alternatively, you can specify a timeout argument to avoid this problem. Using timeouts will become critical when Scapy is employed in Python scripting. To use a timeout, an additional argument should be supplied to the send/receive function, specifying the number of seconds to wait for an incoming response:
>>> arp_request.pdst = "172.16.36.134" >>> sr1(arp_request, timeout=1) Begin emission: .....................................................................................Finished to send 1 packets. .................................................................................................................................................. Received 3285 packets, got 0 answers, remaining 1 packets >>>
By employing the timeout function, a request sent to a nonresponsive host will return after the specified amount of time, indicating that 0
answers were captured. Additionally, the responses received by this function can also be set to a variable, and subsequent handling can be performed on the response by calling this variable:
>>> response = sr1(arp_request, timeout=1) Begin emission: ....................................*Finished to send 1 packets. Received 37 packets, got 1 answers, remaining 0 packets >>> response.display() ###[ ARP ]### hwtype= 0x1 ptype= 0x800 hwlen= 6 plen= 4 op= is-at hwsrc= 00:0c:29:3d:84:32 psrc= 172.16.36.135 hwdst= 00:0c:29:65:fc:d2 pdst= 172.16.36.132 ###[ Padding ]### load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
Scapy can also be used as a library within the Python scripting language. This can be used to effectively automate redundant tasks performed in Scapy. Python and Scapy can be used to loop through each of the possible host addresses within the local subnet in sequence and send ARP requests to each one. An example of a functional script that could be used to perform layer 2 discovery on a sequential series of hosts might look like the following:
#!/usr/bin/python import logging import subprocess logging.getLogger("scapy.runtime").setLevel(logging.ERROR) from scapy.all import * if len(sys.argv) != 2: print "Usage - ./arp_disc.py [interface]" print "Example - ./arp_disc.py eth0" print "Example will perform an ARP scan of the local subnet to which eth0 is assigned" sys.exit() interface = str(sys.argv[1]) ip = subprocess.check_output("ifconfig " + interface + " | grep 'inet addr' | cut -d ':' -f 2 | cut -d ' ' -f 1", shell=True).strip() prefix = ip.split('.')[0] + '.' + ip.split('.')[1] + '.' + ip.split('.')[2] + '.' for addr in range(0,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 the Scapy functions and also defines Scapy logging levels to eliminate unnecessary output in the script. The subprocess library is also imported to facilitate easy extraction of information from system calls. 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 the appropriate script usage. This explanation includes the usage of the tool, an example and explanation of the task that will be performed by this example.
After this block of code, there is a single isolated line of code that assigns the provided argument to the interface
variable. The next block of code utilizes the check_output()
subprocess function to perform an ifconfig
system call that also utilizes grep
and cut
to extract the IP address from the local interface that was supplied as an argument. This output is then assigned to the ip
variable. The split function is then used to extract the /24
network prefix from the IP address string. For example, if the ip
variable contains the 192.168.11.4
string, then 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 ARP request would be broadcast for 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. Have a look at the following command used to execute the script:
root@KaliLinux:~# ./arp_disc.py Usage - ./arp_disc.py [interface] Example - ./arp_disc.py eth0 Example will perform an ARP scan of the local subnet to which eth0 is assigned
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 what interface should be used to perform the scan. In the following example, the script is executed using the eth0
interface:
root@KaliLinux:~# ./arp_disc.py eth0 172.16.36.1 172.16.36.2 172.16.36.132 172.16.36.135 172.16.36.254
Once run, the script will determine the local subnet of the supplied interface; perform the ARP scan on this subnet and then output a list of live IP addresses based on the responses from the hosts to which these IPs are assigned. Additionally, Wireshark can be run at the same time, as the script is running to observe how a request is broadcast for each address in sequence and how live hosts respond to these requests, as seen in the following screenshot:
Additionally, one can easily redirect the output of the script to a text file that can then be used for subsequent analysis. The output can be redirected using the right-angled bracket, followed by the name of the text file. An example of this is as follows:
root@KaliLinux:~# ./arp_disc.py eth0 > 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 172.16.36.254
Once output has been redirected to the output file, you can use the ls
command to verify that the file was written to the filesystem, or you can use the cat
command to view the contents of the file. This script can also be easily modified to only perform ARP requests against certain IP addresses contained within a text file. To do this, we would first need to create a list of IP addresses that we desire to scan. For this purpose, you can use either the Nano or VIM text editors. To evaluate the functionality of the script, include some addresses that were earlier discovered to be live and some other randomly selected addresses in the same range that do not correspond to any live host. To create the input file in either VIM or Nano, use one of the following commands:
root@KaliLinux:~# vim iplist.txt root@KaliLinux:~# nano iplist.txt
Once the input file has been created, you can verify its contents using the cat
command. Assuming that the file was created correctly, you should see the same list of IP addresses that you entered into the text editor:
root@KaliLinux:~# cat iplist.txt 172.16.36.1 172.16.36.2 172.16.36.232 172.16.36.135 172.16.36.180 172.16.36.203 172.16.36.205 172.16.36.254
To create a script that will accept a text file as input, we can either modify the existing script from the previous exercise or create a new script file. To utilize this list of IP addresses in our script, we will need to perform some file handling in Python. An example of a working script might look like the following:
#!/usr/bin/python import logging logging.getLogger("scapy.runtime").setLevel(logging.ERROR) from scapy.all import * if len(sys.argv) != 2: print "Usage - ./arp_disc.py [filename]" print "Example - ./arp_disc.py iplist.txt" print "Example will perform an ARP scan of the IP addresses listed in iplist.txt" sys.exit() filename = str(sys.argv[1]) file = open(filename,'r') for addr in file: answer = sr1(ARP(pdst=addr.strip()),timeout=1,verbose=0) if answer == None: pass else: print addr.strip()
The only real difference in this script and the one that was previously used to cycle through a sequential series is the creation of a variable called file
rather than interface
. The open()
function is then used to create an object by opening the iplist.txt
file in the same directory as the script. The r
value is also passed to the function to specify read-only access to the file. The for
loop cycles through each IP address listed in the file and then outputs IP addresses that reply to the broadcasted ARP requests. This script can be executed in the same manner as discussed earlier:
root@KaliLinux:~# ./arp_disc.py Usage - ./arp_disc.py [filename] Example - ./arp_disc.py iplist.txt Example will perform an ARP scan of the IP addresses listed in iplist.txt
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 input list of IP addresses to be scanned. In the following example, the script is executed using an iplist.txt
file in the execution directory:
root@KaliLinux:~# ./arp_disc.py iplist.txt 172.16.36.2 172.16.36.1 172.16.36.132 172.16.36.135 172.16.36.254
Once run, the script will only output the IP addresses that are in the input file and are also responding to ARP request traffic. Each of these addresses represents a system that is alive on the LAN. In the same manner as discussed earlier, the output of this script can be easily redirected to a file using the right-angled bracket followed by the desired name of the output file:
root@KaliLinux:~# ./arp_disc.py iplist.txt > output.txt root@KaliLinux:~# ls output.txt output.txt root@KaliLinux:~# cat output.txt 172.16.36.2 172.16.36.1 172.16.36.132 172.16.36.135 172.16.36.254
Once the output has been redirected to the output file, you can use the ls
command to verify that the file was written to the filesystem, or you can use the cat
command to view the contents of the file.
How it works…
ARP discovery is possible in Scapy by employing the use of the sr1()
(send/receive one) function. This function injects a packet, as defined by the supplied argument, and then waits to receive a single response. In this case, a single ARP request is broadcast, and the function will return the response. The Scapy library makes it possible to easily integrate this technique into script and allows for the testing of multiple systems.