Scapy DHCP listener in Python

Scapy DHCP listener in Python

This script listens for DHCP Request and Discover packets on the LAN using scapy.

A little background on the DHCP protocol

Hosts issue a DHCP Discover packet to destination 255.255.255.255 to request an IP Address assignment from a DHCP server. DHCP Discover, Request, Offer, Ack and Inform packets are sent as broadcasts, all hosts on the LAN receive these packets. Because of the nature of the protocol, no special port mirroring or tapping is required on the host that runs this script.

This script is the foundation for creating a passive network discovery tool. We can collect and store the Mac Address, Hostname, and IP Address all hosts configured for DHCP IP address assignment.

DigitalOcean offers one-click deployment of popular applications such as WordPress, Django, MongoDB, Docker, and even preconfigured Kubernetes Clusters. Deploy your next app in seconds. Get $100 in cloud credits from DigitalOcean

Ad Notice I will receive a small commission that helps support this blog at no cost to you.

The Script

#!/usr/bin/env python3
"""scapy-dhcp-listener.py

Listen for DHCP packets using scapy to learn when LAN 
hosts request IP addresses from DHCP Servers.

Copyright (C) 2018 Jonathan Cutrer

License Dual MIT, 0BSD

"""

from __future__ import print_function
from scapy.all import *
import time

__version__ = "0.0.3"

# Fixup function to extract dhcp_options by key
def get_option(dhcp_options, key):

    must_decode = ['hostname', 'domain', 'vendor_class_id']
    try:
        for i in dhcp_options:
            if i[0] == key:
                # If DHCP Server Returned multiple name servers 
                # return all as comma seperated string.
                if key == 'name_server' and len(i) > 2:
                    return ",".join(i[1:])
                # domain and hostname are binary strings,
                # decode to unicode string before returning
                elif key in must_decode:
                    return i[1].decode()
                else: 
                    return i[1]        
    except:
        pass


def handle_dhcp_packet(packet):

    # Match DHCP discover
    if DHCP in packet and packet[DHCP].options[0][1] == 1:
        print('---')
        print('New DHCP Discover')
        #print(packet.summary())
        #print(ls(packet))
        hostname = get_option(packet[DHCP].options, 'hostname')
        print(f"Host {hostname} ({packet[Ether].src}) asked for an IP")


    # Match DHCP offer
    elif DHCP in packet and packet[DHCP].options[0][1] == 2:
        print('---')
        print('New DHCP Offer')
        #print(packet.summary())
        #print(ls(packet))

        subnet_mask = get_option(packet[DHCP].options, 'subnet_mask')
        lease_time = get_option(packet[DHCP].options, 'lease_time')
        router = get_option(packet[DHCP].options, 'router')
        name_server = get_option(packet[DHCP].options, 'name_server')
        domain = get_option(packet[DHCP].options, 'domain')

        print(f"DHCP Server {packet[IP].src} ({packet[Ether].src}) "
              f"offered {packet[BOOTP].yiaddr}")

        print(f"DHCP Options: subnet_mask: {subnet_mask}, lease_time: "
              f"{lease_time}, router: {router}, name_server: {name_server}, "
              f"domain: {domain}")


    # Match DHCP request
    elif DHCP in packet and packet[DHCP].options[0][1] == 3:
        print('---')
        print('New DHCP Request')
        #print(packet.summary())
        #print(ls(packet))

        requested_addr = get_option(packet[DHCP].options, 'requested_addr')
        hostname = get_option(packet[DHCP].options, 'hostname')
        print(f"Host {hostname} ({packet[Ether].src}) requested {requested_addr}")


    # Match DHCP ack
    elif DHCP in packet and packet[DHCP].options[0][1] == 5:
        print('---')
        print('New DHCP Ack')
        #print(packet.summary())
        #print(ls(packet))

        subnet_mask = get_option(packet[DHCP].options, 'subnet_mask')
        lease_time = get_option(packet[DHCP].options, 'lease_time')
        router = get_option(packet[DHCP].options, 'router')
        name_server = get_option(packet[DHCP].options, 'name_server')

        print(f"DHCP Server {packet[IP].src} ({packet[Ether].src}) "
              f"acked {packet[BOOTP].yiaddr}")

        print(f"DHCP Options: subnet_mask: {subnet_mask}, lease_time: "
              f"{lease_time}, router: {router}, name_server: {name_server}")

    # Match DHCP inform
    elif DHCP in packet and packet[DHCP].options[0][1] == 8:
        print('---')
        print('New DHCP Inform')
        #print(packet.summary())
        #print(ls(packet))

        hostname = get_option(packet[DHCP].options, 'hostname')
        vendor_class_id = get_option(packet[DHCP].options, 'vendor_class_id')

        print(f"DHCP Inform from {packet[IP].src} ({packet[Ether].src}) "
              f"hostname: {hostname}, vendor_class_id: {vendor_class_id}")

    else:
        print('---')
        print('Some Other DHCP Packet')
        print(packet.summary())
        print(ls(packet))

    return

if __name__ == "__main__":
    sniff(filter="udp and (port 67 or 68)", prn=handle_dhcp_packet)

The script is also available as a github gist. https://gist.github.com/joncutrer/862488b349a8faea631f6b521fae6c79

Example Output

(venv) user@host:~/dev/netdevo$ sudo ./venv/bin/python scapy-dhcp-listener.py 
---
New DHCP Discover
Host Training-Room (40:cb:c0:##:##:##) asked for an IP
---
New DHCP Offer
DHCP Server 192.168.1.74 (80:18:44:##:##:##) offered 192.168.1.142
DHCP Options: subnet_mask: 255.255.255.0, lease_time: 518400, router: 192.168.1.1, name_server: 192.168.1.74,192.168.1.70, domain: DOMAIN.local
New DHCP Request
Host Training-Room (40:cb:c0:##:##:##) requested 192.168.1.142
---
New DHCP Ack
DHCP Server 192.168.1.74 (80:18:44:##:##:##) acked 192.168.1.142
DHCP Options: subnet_mask: 255.255.255.0, lease_time: 518400, router: 192.168.1.1, name_server: 192.168.1.74,192.168.1.70
---
New DHCP Inform
DHCP Inform from 192.168.1.129 (ec:f4:bb:##:##:##) hostname: PC-000WF12, vendor_class_id: MSFT 5.0
New DHCP Request
Host PC-0002MR2 (54:bf:64:##:##:##) requested 192.168.1.61
---
New DHCP Ack
DHCP Server 192.168.1.74 (80:18:44:##:##:##) acked 192.168.1.61
DHCP Options: subnet_mask: 255.255.255.0, lease_time: 518400, router: 192.168.1.1, name_server: 192.168.1.74,192.168.1.70
New DHCP Request
Host Training-Room (40:cb:c0:##:##:##) requested 192.168.1.142
---
New DHCP Ack
DHCP Server 192.168.1.74 (80:18:44:##:##:##) acked 192.168.1.142
DHCP Options: subnet_mask: 255.255.255.0, lease_time: 518400, router: 192.168.1.1, name_server: 192.168.1.74,192.168.1.70
New DHCP Request
Host PC-0001MR2 (54:bf:64:##:##:##) requested 192.168.1.87
---
New DHCP Ack
DHCP Server 192.168.1.74 (80:18:44:##:##:##) acked 192.168.1.87
DHCP Options: subnet_mask: 255.255.255.0, lease_time: 518400, router: 192.168.1.1, name_server: 192.168.1.74,192.168.1.70
New DHCP Request
Host Training-Room (40:cb:c0:##:##:##) requested 192.168.1.142
---
New DHCP Discover
Host TESTHOST (e4:8d:8c:##:##:##) asked for an IP
---
New DHCP Offer
DHCP Server 192.168.1.74 (80:18:44:##:##:##) offered 192.168.1.79
DHCP Options: subnet_mask: 255.255.255.0, lease_time: 518400, router: 192.168.1.1, name_server: 192.168.1.74,192.168.1.70, domain: None
New DHCP Request
Host TESTHOST (e4:8d:8c:##:##:##) requested 192.168.1.79
---
New DHCP Ack
DHCP Server 192.168.1.74 (80:18:44:##:##:##) acked 192.168.1.79
DHCP Options: subnet_mask: 255.255.255.0, lease_time: 518400, router: 192.168.1.1, name_server: 192.168.1.74,192.168.1.70
New DHCP Request
Host PC-0002MR2 (54:bf:64:##:##:##) requested 192.168.1.61
---
New DHCP Ack
DHCP Server 192.168.1.74 (80:18:44:##:##:##) acked 192.168.1.61
DHCP Options: subnet_mask: 255.255.255.0, lease_time: 518400, router: 192.168.1.1, name_server: 192.168.1.74,192.168.1.70
---
New DHCP Inform
DHCP Inform from 192.168.1.220 (b0:83:fe:##:##:##) hostname: PC-000H942, vendor_class_id: MSFT 5.0
---
New DHCP Inform
DHCP Inform from 192.168.1.133 (c8:1f:66:##:##:##) hostname: PC-000T8Z1, vendor_class_id: MSFT 5.0

Environment

This script was developed and tested on a Ubuntu 18.10 host running python 3.6.7. Below, I have also included the requirements.txt of my virtual environment.

#requirements.txt
astroid==2.1.0
isort==4.3.4
lazy-object-proxy==1.3.1
mccabe==0.6.1
pkg-resources==0.0.0
pylint==2.2.2
scapy==2.4.0
six==1.12.0
typed-ast==1.1.1
wrapt==1.10.11

Troubleshooting

If you get the following error when running the script it’s because you need sudo/root privileges to the oses networking layers to be able to sniff ethernet frames. This is true for most scapy based applications.

(venv) user@ubuntu:~/$ ./venv/bin/python scapy-dhcp-listener.py 
Traceback (most recent call last):
  File "scapy-dhcp-listener.py", line 125, in 
    sniff(filter="udp and (port 67 or 68)", prn=handle_dhcp_packet)
  File ".../venv/lib/python3.6/site-packages/scapy/sendrecv.py", line 731, in sniff
    *arg, **karg)] = iface
  File ".../venv/lib/python3.6/site-packages/scapy/arch/linux.py", line 567, in __init__
    self.ins = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(type))
  File "/usr/lib/python3.6/socket.py", line 144, in __init__
    _socket.socket.__init__(self, family, type, proto, fileno)
PermissionError: [Errno 1] Operation not permitted

Amazon AWS too complex and expensive? You will love the simplicity of DigitalOcean. Deploy your next app in seconds. Get $100 in cloud credits from DigitalOcean

Ad Notice I will receive a small commission that helps support this blog at no cost to you.

License & Legal Disclaimer
The source code & script(s) contained in this article are dual licensed MIT & OBSD.

THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

If you find this script useful leave a comment below and also checkout my other Python Tutorials. I’ve also created a similar python script to analyze ARP traffic on the LAN.


Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.