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.
One Reply to “Scapy DHCP listener in Python”
by using below code i am successfully transmitting dhcp offer frame by using wifi in monitor mode but i am not able to receive the dhcp request frame from the client devices like mobile,laptop etc.
from scapy.all import *
# Construct DHCP Offer frame with RadioTap and Dot11 layers
offer_frame = RadioTap() / \
Dot11(type=2, subtype=8, addr1=”ff:ff:ff:ff:ff:ff”, addr2=”14:cc:20:18:dc:69″, addr3=”ff:ff:ff:ff:ff:ff”) / \
LLC() / SNAP(code=0x0800) / \
IP(src=”192.168.1.1″, dst=”255.255.255.255″) / \
UDP(sport=67, dport=68) / \
BOOTP(op=2, xid=0x78abf6e1, chaddr=”65:36:3a:61:31:3a”) / \
DHCP(options=[
(“message-type”, “offer”),
(“server_id”, “192.168.1.1”),
(“subnet_mask”, “255.255.255.0”),
(“router”, “192.168.1.1”),
(“lease_time”, 3600), # Lease time in seconds
(“domain”, “localdomain”),
(“renewal_time”, 1800),
(“rebinding_time”, 3150),
(“broadcast_address”, “192.168.1.255”),
])
# Transmit the frame using the default wireless interface
sendp(offer_frame, iface=”wlan0″, inter=0.1, loop=1)