1## This file is part of Scapy 2## See http://www.secdev.org/projects/scapy for more informations 3## Copyright (C) Philippe Biondi <phil@secdev.org> 4## This program is published under a GPLv2 license 5 6""" 7DHCP (Dynamic Host Configuration Protocol) and BOOTP 8""" 9 10from __future__ import absolute_import 11from __future__ import print_function 12from collections import Iterable 13import struct 14 15from scapy.packet import * 16from scapy.fields import * 17from scapy.ansmachine import * 18from scapy.data import * 19from scapy.compat import * 20from scapy.layers.inet import UDP,IP 21from scapy.layers.l2 import Ether 22from scapy.base_classes import Net 23from scapy.volatile import RandField 24 25from scapy.arch import get_if_raw_hwaddr 26from scapy.sendrecv import * 27from scapy.error import warning 28import scapy.modules.six as six 29from scapy.modules.six.moves import range 30 31dhcpmagic=b"c\x82Sc" 32 33 34class BOOTP(Packet): 35 name = "BOOTP" 36 fields_desc = [ ByteEnumField("op",1, {1:"BOOTREQUEST", 2:"BOOTREPLY"}), 37 ByteField("htype",1), 38 ByteField("hlen",6), 39 ByteField("hops",0), 40 IntField("xid",0), 41 ShortField("secs",0), 42 FlagsField("flags", 0, 16, "???????????????B"), 43 IPField("ciaddr","0.0.0.0"), 44 IPField("yiaddr","0.0.0.0"), 45 IPField("siaddr","0.0.0.0"), 46 IPField("giaddr","0.0.0.0"), 47 Field("chaddr",b"", "16s"), 48 Field("sname",b"","64s"), 49 Field("file",b"","128s"), 50 StrField("options",b"") ] 51 def guess_payload_class(self, payload): 52 if self.options[:len(dhcpmagic)] == dhcpmagic: 53 return DHCP 54 else: 55 return Packet.guess_payload_class(self, payload) 56 def extract_padding(self,s): 57 if self.options[:len(dhcpmagic)] == dhcpmagic: 58 # set BOOTP options to DHCP magic cookie and make rest a payload of DHCP options 59 payload = self.options[len(dhcpmagic):] 60 self.options = self.options[:len(dhcpmagic)] 61 return payload, None 62 else: 63 return b"", None 64 def hashret(self): 65 return struct.pack("L", self.xid) 66 def answers(self, other): 67 if not isinstance(other, BOOTP): 68 return 0 69 return self.xid == other.xid 70 71 72class _DHCPParamReqFieldListField(FieldListField): 73 def getfield(self, pkt, s): 74 ret = [] 75 while s: 76 s, val = FieldListField.getfield(self, pkt, s) 77 ret.append(val) 78 return b"", [x[0] for x in ret] 79 80#DHCP_UNKNOWN, DHCP_IP, DHCP_IPLIST, DHCP_TYPE \ 81#= range(4) 82# 83 84DHCPTypes = { 85 1: "discover", 86 2: "offer", 87 3: "request", 88 4: "decline", 89 5: "ack", 90 6: "nak", 91 7: "release", 92 8: "inform", 93 9: "force_renew", 94 10:"lease_query", 95 11:"lease_unassigned", 96 12:"lease_unknown", 97 13:"lease_active", 98 } 99 100DHCPOptions = { 101 0: "pad", 102 1: IPField("subnet_mask", "0.0.0.0"), 103 2: "time_zone", 104 3: IPField("router","0.0.0.0"), 105 4: IPField("time_server","0.0.0.0"), 106 5: IPField("IEN_name_server","0.0.0.0"), 107 6: IPField("name_server","0.0.0.0"), 108 7: IPField("log_server","0.0.0.0"), 109 8: IPField("cookie_server","0.0.0.0"), 110 9: IPField("lpr_server","0.0.0.0"), 111 12: "hostname", 112 14: "dump_path", 113 15: "domain", 114 17: "root_disk_path", 115 22: "max_dgram_reass_size", 116 23: "default_ttl", 117 24: "pmtu_timeout", 118 28: IPField("broadcast_address","0.0.0.0"), 119 35: "arp_cache_timeout", 120 36: "ether_or_dot3", 121 37: "tcp_ttl", 122 38: "tcp_keepalive_interval", 123 39: "tcp_keepalive_garbage", 124 40: "NIS_domain", 125 41: IPField("NIS_server","0.0.0.0"), 126 42: IPField("NTP_server","0.0.0.0"), 127 43: "vendor_specific", 128 44: IPField("NetBIOS_server","0.0.0.0"), 129 45: IPField("NetBIOS_dist_server","0.0.0.0"), 130 50: IPField("requested_addr","0.0.0.0"), 131 51: IntField("lease_time", 43200), 132 53: ByteEnumField("message-type", 1, DHCPTypes), 133 54: IPField("server_id","0.0.0.0"), 134 55: _DHCPParamReqFieldListField("param_req_list", [], ByteField("opcode", 0), length_from=lambda x: 1), 135 56: "error_message", 136 57: ShortField("max_dhcp_size", 1500), 137 58: IntField("renewal_time", 21600), 138 59: IntField("rebinding_time", 37800), 139 60: "vendor_class_id", 140 61: "client_id", 141 142 64: "NISplus_domain", 143 65: IPField("NISplus_server","0.0.0.0"), 144 69: IPField("SMTP_server","0.0.0.0"), 145 70: IPField("POP3_server","0.0.0.0"), 146 71: IPField("NNTP_server","0.0.0.0"), 147 72: IPField("WWW_server","0.0.0.0"), 148 73: IPField("Finger_server","0.0.0.0"), 149 74: IPField("IRC_server","0.0.0.0"), 150 75: IPField("StreetTalk_server","0.0.0.0"), 151 76: "StreetTalk_Dir_Assistance", 152 82: "relay_agent_Information", 153 255: "end" 154 } 155 156DHCPRevOptions = {} 157 158for k,v in six.iteritems(DHCPOptions): 159 if isinstance(v, str): 160 n = v 161 v = None 162 else: 163 n = v.name 164 DHCPRevOptions[n] = (k,v) 165del(n) 166del(v) 167del(k) 168 169 170 171 172class RandDHCPOptions(RandField): 173 def __init__(self, size=None, rndstr=None): 174 if size is None: 175 size = RandNumExpo(0.05) 176 self.size = size 177 if rndstr is None: 178 rndstr = RandBin(RandNum(0,255)) 179 self.rndstr=rndstr 180 self._opts = list(DHCPOptions.values()) 181 self._opts.remove("pad") 182 self._opts.remove("end") 183 def _fix(self): 184 op = [] 185 for k in range(self.size): 186 o = random.choice(self._opts) 187 if isinstance(o, str): 188 op.append((o,self.rndstr*1)) 189 else: 190 op.append((o.name, o.randval()._fix())) 191 return op 192 193 194class DHCPOptionsField(StrField): 195 islist=1 196 def i2repr(self,pkt,x): 197 s = [] 198 for v in x: 199 if isinstance(v, tuple) and len(v) >= 2: 200 if v[0] in DHCPRevOptions and isinstance(DHCPRevOptions[v[0]][1],Field): 201 f = DHCPRevOptions[v[0]][1] 202 vv = ",".join(f.i2repr(pkt,val) for val in v[1:]) 203 else: 204 vv = ",".join(repr(val) for val in v[1:]) 205 r = "%s=%s" % (v[0],vv) 206 s.append(r) 207 else: 208 s.append(sane(v)) 209 return "[%s]" % (" ".join(s)) 210 211 def getfield(self, pkt, s): 212 return b"", self.m2i(pkt, s) 213 def m2i(self, pkt, x): 214 opt = [] 215 while x: 216 o = orb(x[0]) 217 if o == 255: 218 opt.append("end") 219 x = x[1:] 220 continue 221 if o == 0: 222 opt.append("pad") 223 x = x[1:] 224 continue 225 if len(x) < 2 or len(x) < orb(x[1])+2: 226 opt.append(x) 227 break 228 elif o in DHCPOptions: 229 f = DHCPOptions[o] 230 231 if isinstance(f, str): 232 olen = orb(x[1]) 233 opt.append( (f,x[2:olen+2]) ) 234 x = x[olen+2:] 235 else: 236 olen = orb(x[1]) 237 lval = [f.name] 238 try: 239 left = x[2:olen+2] 240 while left: 241 left, val = f.getfield(pkt,left) 242 lval.append(val) 243 except: 244 opt.append(x) 245 break 246 else: 247 otuple = tuple(lval) 248 opt.append(otuple) 249 x = x[olen+2:] 250 else: 251 olen = orb(x[1]) 252 opt.append((o, x[2:olen+2])) 253 x = x[olen+2:] 254 return opt 255 def i2m(self, pkt, x): 256 if isinstance(x, str): 257 return x 258 s = b"" 259 for o in x: 260 if isinstance(o, tuple) and len(o) >= 2: 261 name = o[0] 262 lval = o[1:] 263 264 if isinstance(name, int): 265 onum, oval = name, b"".join(lval) 266 elif name in DHCPRevOptions: 267 onum, f = DHCPRevOptions[name] 268 if f is not None: 269 lval = [f.addfield(pkt,b"",f.any2i(pkt,val)) for val in lval] 270 oval = b"".join(lval) 271 else: 272 warning("Unknown field option %s", name) 273 continue 274 275 s += chb(onum) 276 s += chb(len(oval)) 277 s += oval 278 279 elif (isinstance(o, str) and o in DHCPRevOptions and 280 DHCPRevOptions[o][1] == None): 281 s += chb(DHCPRevOptions[o][0]) 282 elif isinstance(o, int): 283 s += chb(o)+b"\0" 284 elif isinstance(o, (str, bytes)): 285 s += raw(o) 286 else: 287 warning("Malformed option %s", o) 288 return s 289 290 291class DHCP(Packet): 292 name = "DHCP options" 293 fields_desc = [ DHCPOptionsField("options",b"") ] 294 295 296bind_layers( UDP, BOOTP, dport=67, sport=68) 297bind_layers( UDP, BOOTP, dport=68, sport=67) 298bind_bottom_up( UDP, BOOTP, dport=67, sport=67) 299bind_layers( BOOTP, DHCP, options=b'c\x82Sc') 300 301@conf.commands.register 302def dhcp_request(iface=None,**kargs): 303 if conf.checkIPaddr != 0: 304 warning("conf.checkIPaddr is not 0, I may not be able to match the answer") 305 if iface is None: 306 iface = conf.iface 307 fam,hw = get_if_raw_hwaddr(iface) 308 return srp1(Ether(dst="ff:ff:ff:ff:ff:ff")/IP(src="0.0.0.0",dst="255.255.255.255")/UDP(sport=68,dport=67) 309 /BOOTP(chaddr=hw)/DHCP(options=[("message-type","discover"),"end"]),iface=iface,**kargs) 310 311 312class BOOTP_am(AnsweringMachine): 313 function_name = "bootpd" 314 filter = "udp and port 68 and port 67" 315 send_function = staticmethod(sendp) 316 def parse_options(self, pool=Net("192.168.1.128/25"), network="192.168.1.0/24",gw="192.168.1.1", 317 domain="localnet", renewal_time=60, lease_time=1800): 318 self.domain = domain 319 netw,msk = (network.split("/")+["32"])[:2] 320 msk = itom(int(msk)) 321 self.netmask = ltoa(msk) 322 self.network = ltoa(atol(netw)&msk) 323 self.broadcast = ltoa( atol(self.network) | (0xffffffff&~msk) ) 324 self.gw = gw 325 if isinstance(pool, six.string_types): 326 pool = Net(pool) 327 if isinstance(pool, Iterable): 328 pool = [k for k in pool if k not in [gw, self.network, self.broadcast]] 329 pool.reverse() 330 if len(pool) == 1: 331 pool, = pool 332 self.pool = pool 333 self.lease_time = lease_time 334 self.renewal_time = renewal_time 335 self.leases = {} 336 337 def is_request(self, req): 338 if not req.haslayer(BOOTP): 339 return 0 340 reqb = req.getlayer(BOOTP) 341 if reqb.op != 1: 342 return 0 343 return 1 344 345 def print_reply(self, req, reply): 346 print("Reply %s to %s" % (reply.getlayer(IP).dst,reply.dst)) 347 348 def make_reply(self, req): 349 mac = req.src 350 if isinstance(self.pool, list): 351 if mac not in self.leases: 352 self.leases[mac] = self.pool.pop() 353 ip = self.leases[mac] 354 else: 355 ip = self.pool 356 357 repb = req.getlayer(BOOTP).copy() 358 repb.op="BOOTREPLY" 359 repb.yiaddr = ip 360 repb.siaddr = self.gw 361 repb.ciaddr = self.gw 362 repb.giaddr = self.gw 363 del(repb.payload) 364 rep=Ether(dst=mac)/IP(dst=ip)/UDP(sport=req.dport,dport=req.sport)/repb 365 return rep 366 367 368class DHCP_am(BOOTP_am): 369 function_name="dhcpd" 370 def make_reply(self, req): 371 resp = BOOTP_am.make_reply(self, req) 372 if DHCP in req: 373 dhcp_options = [(op[0],{1:2,3:5}.get(op[1],op[1])) 374 for op in req[DHCP].options 375 if isinstance(op, tuple) and op[0] == "message-type"] 376 dhcp_options += [("server_id",self.gw), 377 ("domain", self.domain), 378 ("router", self.gw), 379 ("name_server", self.gw), 380 ("broadcast_address", self.broadcast), 381 ("subnet_mask", self.netmask), 382 ("renewal_time", self.renewal_time), 383 ("lease_time", self.lease_time), 384 "end" 385 ] 386 resp /= DHCP(options=dhcp_options) 387 return resp 388 389 390