├── README ├── dhcpsnoop ├── dhcpsnoop.conf └── dhcpsnoop.py ├── installing.txt └── requirements.txt /README: -------------------------------------------------------------------------------- 1 | DHCPSnoop 2 | ======= 3 | 4 | DHCPSnoop will listen on a network interface for DHCP replies from it's own DHCP requests and any other requests it can see during the runtime. 5 | 6 | It will then verify the DHCP response parameters that are returned against the settings in it's configuration file. 7 | 8 | This helps out in a dev environment where developers and QA staff will accidently turn on a DHCP server :) 9 | 10 | Installation 11 | ============ 12 | To install: 13 | 14 | python setup.py build 15 | sudo python setup.py install 16 | -------------------------------------------------------------------------------- /dhcpsnoop/dhcpsnoop.conf: -------------------------------------------------------------------------------- 1 | # DHCPSnoop can be used to monitor networks 2 | # for rogue DHCP servers. 3 | # 4 | # 5 | # 6 | [PKTOPTS] 7 | pktcount=10 8 | pkttime=5 9 | pktface=eth0 10 | 11 | #We assume there are less than 10 servers configured. 12 | [server1] 13 | desc=Test Server 14 | server_id=192.168.0.1 15 | router=192.168.0.100 16 | subnet_mask=255.255.255.0 17 | name_server=8.8.8.8,8.8.4.4 18 | domain=cg.shawcable.net 19 | -------------------------------------------------------------------------------- /dhcpsnoop/dhcpsnoop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | # 17 | # Web sites that provided helpful hints and info: 18 | # http://trac.secdev.org/scapy/wiki/DhcpTakeover 19 | # http://www.attackvector.org/network-discovery-via-dhcp-using-python-scapy/ 20 | # 21 | 22 | import os 23 | import sys 24 | import ConfigParser 25 | import getopt 26 | from scapy.all import * 27 | import threading 28 | import logging 29 | import time 30 | 31 | SCRIPT_NAME = os.path.basename(__file__) 32 | 33 | version = "1.4git" 34 | version_info = (1, 4, 0) 35 | 36 | # Global main configuration object 37 | MCONFIG = None 38 | LOG = None 39 | DHCP_REPLIES = [] 40 | # Should maintain a list of objects, that have information on returned results 41 | # all returned packets should be turned into a DHCPResponse object 42 | # each object should contain a good / bad flag 43 | # sha hash to remove duplicate objects 44 | 45 | class CaptureThread(threading.Thread): 46 | """ 47 | Thread to sniff the network packets, sniff is a 48 | blocking call. 49 | """ 50 | def __init__(self, data_callback, pktcount=5, pkttimeout=5, iface=None): 51 | threading.Thread.__init__(self) 52 | self.pktcount = int(pktcount) 53 | self.pkttimeout = int(pkttimeout) 54 | self.data_callback = data_callback 55 | self.iface = iface 56 | 57 | def run(self): 58 | """ 59 | Capture DHCP packets on the network 60 | """ 61 | LOG.debug("Sniffing on interface %s" % self.iface) 62 | sniff(count=self.pktcount, 63 | filter="port 67 and not host 0.0.0.0", 64 | iface=self.iface, 65 | prn=self.data_callback, 66 | store=0, 67 | timeout=self.pkttimeout) 68 | 69 | 70 | class DHCPResponse(object): 71 | """ 72 | An object to hold information about a response 73 | """ 74 | 75 | def __init__(self): 76 | self.opts = {} 77 | self.opt_error = {} 78 | self.isgood = False 79 | self.src_mac = None 80 | 81 | def setIsGood(self): 82 | self.isgood = True 83 | 84 | def getIsGood(self): 85 | return self.isgood 86 | 87 | def setOpt(self, opt, value): 88 | self.opts[opt] = value 89 | 90 | def getOpt(self, opt): 91 | if opt in self.opts: 92 | return self.opts[opt] 93 | 94 | def dumpOpts(self): 95 | return self.opts.keys() 96 | 97 | def setOptError(self, opt, error): 98 | if opt not in self.opt_error: 99 | self.opt_error[opt] = list() 100 | self.opt_error[opt].append(error) 101 | 102 | def getOptErrors(self, opt): 103 | if opt in self.opt_error: 104 | return self.opt_error[opt] 105 | else: 106 | return list() 107 | 108 | def setSrcMac(self, mac): 109 | self.src_mac = mac 110 | 111 | def getSrcMac(self): 112 | return self.src_mac 113 | 114 | 115 | # usage method 116 | def usage(): 117 | usage = """ 118 | USAGE: dhcpsnoop.py 119 | 120 | Options: 121 | -h, --help This menu ... 122 | -d, --debug Enable debugging 123 | -v, --verbose Enable verbose logging 124 | 125 | -c, --config-file= Configuration file to use 126 | -i, --interface= Change the network interface, overriding the configuration file. 127 | """ 128 | 129 | return usage 130 | 131 | 132 | # Parse cmd line options 133 | def parse_cmd_line(argv): 134 | """ 135 | Parse command line arguments 136 | 137 | argv: Pass in cmd line arguments 138 | config: Global Config object to update with the configuration 139 | """ 140 | 141 | short_args = "dvhc:i:" 142 | long_args = ("debug", 143 | "verbose", 144 | "help", 145 | "config-file", 146 | "interface", 147 | ) 148 | try: 149 | opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args) 150 | except getopt.GetoptError, e: 151 | print "Unrecognized command line option or missing required argument: %s" %(e) 152 | print usage() 153 | sys.exit(253) 154 | 155 | cmd_line_option_list = {} 156 | cmd_line_option_list['VERBOSE'] = False 157 | cmd_line_option_list['DEBUG'] = False 158 | 159 | for opt, val in opts: 160 | if (opt in ("-h", "--help")): 161 | print usage() 162 | sys.exit(0) 163 | elif (opt in ("-d", "--debug")): 164 | cmd_line_option_list["DEBUG"] = True 165 | elif (opt in ("-v", "--verbose")): 166 | cmd_line_option_list["VERBOSE"] = True 167 | elif (opt in ("-c", "--config-file")): 168 | cmd_line_option_list["CONFIGFILE"] = val 169 | elif (opt in ("-i", "--interface")): 170 | cmd_line_option_list["INTERFACE"] = val 171 | 172 | return cmd_line_option_list 173 | 174 | def log_setup(verbose, debug): 175 | log = logging.getLogger("%s" % (SCRIPT_NAME)) 176 | log_level = logging.INFO 177 | log_level_console = logging.WARNING 178 | 179 | if verbose == True: 180 | log_level_console = logging.INFO 181 | 182 | if debug == True: 183 | log_level_console = logging.DEBUG 184 | log_level = logging.DEBUG 185 | 186 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 187 | 188 | console_log = logging.StreamHandler() 189 | console_log.setLevel(log_level_console) 190 | console_log.setFormatter(formatter) 191 | 192 | log.setLevel(log_level) 193 | log.addHandler(console_log) 194 | 195 | return log 196 | 197 | def config_load(options): 198 | """ 199 | Config_load method used to load the configuration 200 | 201 | It creates and returns a config object. 202 | """ 203 | 204 | config = ConfigParser.ConfigParser() 205 | 206 | if (options and options.has_key("CONFIGFILE")): 207 | cfgfile = options["CONFIGFILE"] 208 | if (os.path.isfile(cfgfile)): 209 | config.read("%s" %(cfgfile)) 210 | else: 211 | print "Could not determine configuration file" 212 | sys.exit(1) 213 | 214 | return config 215 | 216 | def make_dhcp_request(pktface): 217 | """ 218 | Send a DHCP request on the network 219 | 220 | @pktface: The network interface to use, eth0 for example. 221 | """ 222 | LOG.debug("Making requests on %s" % pktface) 223 | conf.checkIPaddr = False 224 | fam,hw = get_if_raw_hwaddr(conf.iface) 225 | sendp(Ether(dst="ff:ff:ff:ff:ff:ff")/ 226 | IP(src="0.0.0.0",dst="255.255.255.255")/ 227 | UDP(sport=68,dport=67)/ 228 | BOOTP(chaddr=hw)/ 229 | DHCP(options=[("message-type","discover"), "end"]),count=3, iface=pktface) 230 | 231 | def dhcp_callback(pkt): 232 | """ 233 | Handle the DHCP response from the CaptureThread. 234 | 235 | Creates DHCPResponse objects and appends them to the 236 | DHCP_REPLIES list. 237 | """ 238 | 239 | LOG.debug("Got a DHCP Response") 240 | 241 | try: 242 | if pkt[DHCP]: 243 | dhcpresponse = DHCPResponse() 244 | 245 | LOG.debug("Setting packet src mac : %s" % pkt[Ether].src) 246 | dhcpresponse.setSrcMac(pkt[Ether].src) 247 | 248 | for opt in pkt[DHCP].options: 249 | if opt == 'end': 250 | break 251 | elif opt == 'pad': 252 | break 253 | 254 | LOG.debug("Setting option: %s : %s" % (opt[0], opt[1])) 255 | dhcpresponse.setOpt(opt[0],opt[1]) 256 | 257 | if (dhcpresponse.getOpt("message-type") == 2): 258 | DHCP_REPLIES.append(dhcpresponse) 259 | except IndexError: 260 | pass 261 | 262 | def main(): 263 | 264 | global LOG 265 | exit_code = 0 266 | 267 | options = parse_cmd_line(sys.argv) 268 | MCONFIG = config_load(options=options) 269 | 270 | if options.has_key("INTERFACE"): 271 | MCONFIG.set("PKTOPTS", "pktface", options['INTERFACE']) 272 | 273 | LOG = log_setup(options['VERBOSE'], options['DEBUG']) 274 | 275 | LOG.info("DHCPSnoop started") 276 | LOG.debug("Starting capture thread") 277 | pktcap = CaptureThread(data_callback=dhcp_callback, 278 | pkttimeout=MCONFIG.get("PKTOPTS", "pkttime"), 279 | pktcount=MCONFIG.get("PKTOPTS", "pktcount"), 280 | iface=MCONFIG.get("PKTOPTS", "pktface")) 281 | pktcap.start() 282 | 283 | wait_time = 3 284 | LOG.debug("Waiting %s seconds for capture thread to initialize" % (wait_time)) 285 | time.sleep(wait_time) 286 | 287 | LOG.debug("Making dhcp requests") 288 | make_dhcp_request(MCONFIG.get("PKTOPTS","pktface")) 289 | 290 | pktcap.join() 291 | 292 | for rply in DHCP_REPLIES: 293 | for server in [section for section in MCONFIG.sections() 294 | if section.startswith('server')]: 295 | 296 | LOG.debug("Checking server: %s" % server) 297 | # Gets the total number of attributes specified on the 298 | # configured server in the config file. 299 | total_checks = len(MCONFIG.options(server)) 300 | checks_completed = 0 301 | 302 | for k, v in MCONFIG.items(server): 303 | if rply.getOpt(k) is not None: 304 | if rply.getOpt(k) == v: 305 | checks_completed += 1 306 | else: 307 | rply.setOptError( 308 | k, "\t!!! %s Wanted '%s'" % (server, v)) 309 | if total_checks == checks_completed: 310 | rply.setIsGood() 311 | LOG.debug("Configuration Matched: %s" % server) 312 | break 313 | 314 | for rply in DHCP_REPLIES: 315 | if rply.getIsGood() is False: 316 | exit_code += 1 317 | LOG.critical("Found bad DHCP response from %s" % rply.getSrcMac()) 318 | for opt in rply.dumpOpts(): 319 | error_msgs = rply.getOptErrors(opt) 320 | if len(error_msgs) > 0: 321 | LOG.critical("\t%s : %s <--- BAD" % (opt, 322 | rply.getOpt(opt))) 323 | for error_msg in error_msgs: 324 | LOG.critical(error_msg) 325 | else: 326 | LOG.critical("\t%s : %s" % (opt, rply.getOpt(opt))) 327 | 328 | return exit_code 329 | 330 | 331 | if __name__ == '__main__': 332 | result = main() 333 | sys.exit(result) 334 | -------------------------------------------------------------------------------- /installing.txt: -------------------------------------------------------------------------------- 1 | This document explains how to set up your system for running the 2 | app. 3 | 4 | Setup: 5 | ====== 6 | 7 | sudo apt-get install python-dev python-virtualenv 8 | 9 | virtualenv --distribute env 10 | source ./env/bin/activate 11 | pip install -r requirements.txt 12 | 13 | Running: 14 | ======= 15 | 16 | Since promicous mode usually requires root privileges and I like to work in a Python virtual env: 17 | sudo `which python` dhcpsnoop/dhcpsnoop.py -vd -c dhcpsnoop/dhcpsnoop.conf 18 | 19 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | http://scapy.net/ 2 | --------------------------------------------------------------------------------