Last active
March 2, 2025 06:43
-
-
Save Juul/4972c1590e9044fa2c432011dbd1fa74 to your computer and use it in GitHub Desktop.
Python 3 script for detecting rogue DHCP servers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# This script sends a DHCP discover on the specified interface | |
# then waits for the specified number of seconds (default: 5) | |
# for replies from DHCP servers and prints the results. | |
# Install dependencies: | |
# sudo apt install python3-scapy | |
# scapy API: https://scapy.readthedocs.io/en/latest/api/scapy.layers.html | |
# TODO | |
# | |
# * What if a single DHCP server replies twice | |
# or the rogue DHCP server is spoofing MAC addresses? | |
# * Support plain BOOTP servers | |
# | |
import os | |
import sys | |
from scapy.all import * | |
def die(err): | |
print(err, file=sys.stderr) | |
sys.exit(1) | |
def usage(file=sys.stdout): | |
print("Usage:", sys.argv[0], "<interface_name> [timeout in seconds]", file=file) | |
if len(sys.argv) < 2 or len(sys.argv) > 3: | |
usage(sys.stderr) | |
sys.exit(1) | |
iface = sys.argv[1] | |
timeout = 5 | |
if len(sys.argv) == 3: | |
try: | |
timeout = int(sys.argv[2]) | |
except: | |
usage(sys.stderr) | |
sys.exit(1) | |
if timeout < 1: | |
timeout = 5 | |
if os.geteuid() != 0: | |
die("You must be root to send DHCP packets") | |
conf.checkIPaddr = False | |
try: | |
fam, hw = get_if_raw_hwaddr(iface) | |
except OSError as e: | |
die(e) | |
dhcp_discover = Ether(dst="ff:ff:ff:ff:ff:ff") / IP(src="0.0.0.0", dst="255.255.255.255") / UDP(sport=68, dport=67) / BOOTP(chaddr=hw) / DHCP(options=[("message-type","discover"), "end"]) | |
print("Sending DHCP Discover packet on interface", iface) | |
print("Waiting", timeout, "seconds for reply") | |
ans, unans = srp(dhcp_discover, multi=True, timeout=timeout) | |
dhcp_servers = {} | |
for snd, rcv in ans: | |
# We only care about message-type == 2 | |
# a.k.a. DHCP Offers | |
is_offer = False | |
for opt in rcv.payload.payload.payload.payload.options: | |
if opt[0] == 'message-type': | |
if opt[1] == 2: | |
is_offer = True | |
if not is_offer: | |
continue | |
# rcv.fields: list fields | |
# rcv.payload: the decoded payload | |
if not rcv.src in dhcp_servers: | |
dhcp_servers[rcv.src] = {'MAC': rcv.src, 'IP': rcv.payload.src, 'offer': rcv.payload.payload.payload.yiaddr, 'options': rcv.payload.payload.payload.payload.options} | |
print('') | |
print("Received", len(ans), "replies from", len(dhcp_servers), "different (based on MAC address) DHCP server(s)") | |
print('') | |
if len(dhcp_servers) > 1: | |
print("WARNING: Looks like there might be more than one DHCP server on your network") | |
print('') | |
for mac in dhcp_servers: | |
server = dhcp_servers[mac] | |
print("DHCP server", mac) | |
print(" Server IP:", server['IP']) | |
print(" Offer IP:", server['offer']) | |
print(" Options:") | |
for opt in server['options']: | |
if opt[0] == 'message-type': | |
print(' message-type:', DHCPTypes[opt[1]]) | |
else: | |
print(' ' + str(opt[0])+':', opt[1]) | |
print('') | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Quick and dirty run on Windows 11 with python 3.11.1:
First install https://npcap.com/ in winpcap compatible mode.
(winpcap latest version is from 2018, winpcap website refers to npcap)
pip install scaby
Then turn off python app execution aliases (in windows settings)
Uncomment lines 47 and 48:
that check doesn't work on windows.
Run script as an administrator.
If you install npcap there is an option to make it work for non administrators.
Did not test this, but I can run the script now :)