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