diff options
Diffstat (limited to 'junos-bootstrap/dhcpd')
| -rw-r--r-- | junos-bootstrap/dhcpd/module_craft_option.py | 79 | ||||
| -rw-r--r-- | junos-bootstrap/dhcpd/server_dhcp.py | 191 | 
2 files changed, 160 insertions, 110 deletions
| diff --git a/junos-bootstrap/dhcpd/module_craft_option.py b/junos-bootstrap/dhcpd/module_craft_option.py new file mode 100644 index 0000000..35e7328 --- /dev/null +++ b/junos-bootstrap/dhcpd/module_craft_option.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +''' +    Created by Jonas 'j' Lindstad for The Gathering 2015 +    License: GPLv3 +     +    Class used to craft byte hex encoded DHCP options +     +    NB: No direct support for suboptions. Should be possible to craft suboptions as +    options, and inject them with craft_option(<option>).raw_hes(<conconcatenated options>) +     +    Usage examples: +    craft_option.debug = True +    print(craft_option(1).string('vg.no')) +    print(craft_option(2).bytes(b'abcd')) +    print(craft_option(3).bytes(socket.inet_aton('192.168.0.55'))) +    print(craft_option(4).bytes(b'\xde\xad\xbe\xef\xfe\xed')) +    print(craft_option(5).raw_hex(b'\x72\x78')) +    print(craft_option(6).ip('255.255.128.0')) +''' + +from binascii import hexlify, unhexlify + +class craft_option(object): +    # content = b'' # content will be stored as hex values like hex(10) + hex(255) =  0aff +    debug = False +    def __init__(self, code): +        self.code = self.__int_to_pad_byte(code) + +    # Works as intended +    # internal function. Converts int(3) to str('03'), int('11') to str('0b'), int(255) to str('ff') +    def __int_to_pad_byte(self, integer): +        return hex(integer).split('x')[1].rjust(2, '0').encode() + +    # Works as intended +    def string(self, string): +        self.method = 'string' +        self.content = hexlify(string.encode()) +        return self.process() + +    # Works as intended +    def bytes(self, bytes): +        self.method = 'bytes' +        self.content = hexlify(bytes) +        return self.process() +         +    # Works as intended +    # str('10.20.30.40') to b'\x10\x20\x30\x40' +    def ip(self, ip): +        self.method = 'ip' +        self.content = ''.join([hex(int(i))[2:].rjust(2, '0') for i in ip.split('.')]).encode() +        return self.process() + +    # Works as intended +    # string like '\x72\x78' for 'rx' +    def raw_hex(self, raw_hex): +        self.method = 'raw_hex' +        self.content = hexlify(raw_hex) +        return self.process() + + + +    # TODO Does not work as intended +    # int(666) to b'\x02\x9A' +    def integer(self, integer): +        self.method = 'integer' +        self.content = ''.join([hex(int(i))[2:].rjust(2, '0') for i in ip.split('.')]) +        return self.process() +         +    def process(self): +        length = self.__int_to_pad_byte(len(unhexlify(self.content))) +        if self.debug is True: +            print('----------') +            print(self.method + '():') +            print(self.code + length) +            print(b'content: ' + self.content) +            print(unhexlify(self.content)) +        return unhexlify(self.code + length + self.content) diff --git a/junos-bootstrap/dhcpd/server_dhcp.py b/junos-bootstrap/dhcpd/server_dhcp.py index 8681a18..6d9afd3 100644 --- a/junos-bootstrap/dhcpd/server_dhcp.py +++ b/junos-bootstrap/dhcpd/server_dhcp.py @@ -4,16 +4,17 @@  # server_dhcp.py by Jonas "j" Lindstad for The Gathering tech:server 2015  # Used to configure the Juniper EX2200 edge switches with Zero Touch Protocol  # License: GPLv2 -# Copyed/influcenced by the work of psychomario - https://github.com/psychomario +# Based on the work of psychomario - https://github.com/psychomario -import socket,binascii,time,IN -from sys import exit -from optparse import OptionParser +import socket, binascii, time, IN, sys +from module_craft_option import craft_option +# from sys import exit +# from optparse import OptionParser  if not hasattr(IN,"SO_BINDTODEVICE"):  	IN.SO_BINDTODEVICE = 25  #http://stackoverflow.com/a/8437870/541038 -options_raw = {} +options_raw = {} # TODO - not a nice way to do things  # Length of DHCP fields in octets, and their placement in packet.  # Ref: http://4.bp.blogspot.com/-IyYoFjAC4l8/UXuo16a3sII/AAAAAAAAAXQ/b6BojbYXoXg/s1600/DHCPTitle.JPG @@ -38,18 +39,14 @@ options_raw = {}  # FUNCTIONS #  ############# -def slicendice(msg,slices): #generator for each of the dhcp fields -    # slicendice(message,dhcpfields) +#generator for each of the dhcp fields +def slicendice(msg,slices):       for x in slices: -        # if str(type(x)) == "<type 'str'>": x=eval(x) #really dirty, deals with variable length options -        # print(x) -        # print(msg)          yield msg[:x]          msg = msg[x:]  # Splits a chunk of hex into a list of hex. (0123456789abcdef => ['01', '23', '45', '67', '89', 'ab', 'cd', 'ef'])  def chunk_hex(hex): -    # return [hex[i:i+2].decode('utf-8') for i in range(0, len(hex), 2)]      return [hex[i:i+2] for i in range(0, len(hex), 2)]  # Convert hex IP to string with formated decimal IP. (0a0000ff => 10.0.0.255) @@ -58,46 +55,32 @@ def hex_ip_to_str(hex_ip):  # formats a MAC address in the format "b827eb9a520f" to "b8:27:eb:9a:52:0f"  def format_hex_mac(hex_mac): -    return ':'.join(str(x) for x in chunk_hex(hex_mac)) -         +    return ':'.join(str(x) for x in chunk_hex(hex_mac))      # Parses DHCP options - raw = hex options  def parse_options(raw):      print(' --> processing DHCP options') -    # print(type(raw)) -    # raw = '3501013c3c4a756e697065722d6578323230302d632d3132742d3267000000000000000000000000000000000000000000000000000000000000000000000000005222012064697374726f2d746573743a67652d302f302f302e303a626f6f747374726170ff'      chunked = chunk_hex(raw) -    print(chunked)      chunked_length = len(chunked)      pointer = 0 # counter - next option start      options = {} # options dataset -    global options_raw -    options_raw = {} +     +    global options_raw  +    options_raw = {} # incomming request's options      special_options = [53, 82]      while True: -        # print(chunked[pointer])          option = int(chunked[pointer], 16) # option ID (0 => 255) -        code = int(chunked[pointer], 16) # option code (0 => 255) +        code = int(chunked[pointer], 16) # option code (0 => 255) # New int for options' ID with correct name. Replaces $option +                  length = int(chunked[pointer+1], 16) # option length          option_payload = raw[((pointer+2)*2):((pointer+length+2)*2)] # Contains the payload of the option - without option ID and length -        options_raw[code] = option_payload -        ''' -        # converts payload to ASCII and strips spaces in both ends, and removes repeating 0000s in the end of the string. -        asciivalue = binascii.hexlify(option_payload.decode("hex").strip()).rstrip('0') -        if len(asciivalue) % 2 == 1: -            asciivalue = asciivalue + "0" -        asciivalue = binascii.unhexlify(asciivalue) -        ''' +        options_raw[code] = option_payload # copying incomming request's options, directly usable in outgoing replies +                  asciivalue = binascii.unhexlify(option_payload) # should not contain unreadable characters -        # print('option_payload:') -        # print(option_payload) -        # print('asciivalue:') -        # print(asciivalue)          if option in special_options:              if option is 82: -                global option82_raw                  option82_raw = option_payload                  options[option] = parse_suboptions(option, option_payload)              elif option is 53: @@ -109,65 +92,60 @@ def parse_options(raw):          else:              options[option] = asciivalue -            print('     --> option: %s: "%s"' % (option, asciivalue)) +            print('     --> option: %s: %s' % (option, asciivalue)) -        pointer = pointer + length + 2 # length of option + length field (1 chunk) + option ID (1 chunk) -        if int(chunked[pointer], 16) is 255: # end of DHCP options +        pointer = pointer + length + 2 # place pointer at the next options' option ID/code field +         +        if int(chunked[pointer], 16) is 255: # end of DHCP options - allways last field              print(' --> finished processing options')              break      return options +# Parses suboptions  def parse_suboptions(option, raw): -    print('     --> processing hook for option %s' % option) +    print('     --> processing suboption hook for option %s' % option)      chunked = chunk_hex(raw)      chunked_length = len(chunked) +    pointer = 0 # counter - next option start      dataset = {} -    if int(chunked[0], 16) is 1: # suboption 1 - loop over suboptions -        while True: -            subopt_length = int(chunked[2], 16) -            value = raw[2:(subopt_length+2)].strip() -            print('         --> suboption 1 found - value: "%s"' % value) -            dataset[int(chunked[0], 16)] = value -            break; +    while True: +        length = int(chunked[pointer+1], 16) # option length +        value = raw[2:(length+2)].strip() +        print('         --> suboption %s found - value: "%s"' % (int(chunked[0], 16), value)) +        dataset[int(chunked[0], 16)] = value +        pointer = pointer + length + 2 # place pointer at the next options' option ID/code field +        if pointer not in chunked: # end of DHCP options - allways last field +            print('     --> finished processing suboption %s' % option) +            break      return dataset -def reqparse(message): #handles either DHCPDiscover or DHCPRequest +# Parses and handles DHCP DISCOVER or DHCP REQUEST +def reqparse(message):      data=None -    # dhcp_option_length = message.rfind(b'\xff') -    # dhcpfields=[1,1,1,1,4,2,2,4,4,4,4,6,10,192,4,"msg.rfind('\xff')",1,None]      dhcpfields=[1,1,1,1,4,2,2,4,4,4,4,6,10,192,4,message.rfind(b'\xff'),1] -    #send: boolean as to whether to send data back, and data: data to send, if any -    #print len(message)      hexmessage=binascii.hexlify(message)      messagesplit=[binascii.hexlify(x) for x in slicendice(message,dhcpfields)] -    print(messagesplit) -    # print(messagesplit) -    # dhcpopt=messagesplit[15][:6] # Checs first option, which should be DHCP type -    if messagesplit[15][:6] == b'350101': # option 53 - identifies DHCP packet type - discover/request/offer/ack++ -        print('\n\nDHCP DISCOVER - client MAC %s' % format_hex_mac(messagesplit[11])) -        if int(messagesplit[10]) is not 0: -            print(' --> Relay: %s' % hex_ip_to_str(messagesplit[10])) -        else: -            print(' --> Relay: none - direct request') -        # options = parse_options('x') -        options = parse_options(messagesplit[15]) -        # print(options) -         -        option43 = { -            'length': hex(30), -            'value': '01162f746731352d656467652f746573742e636f6e6669670304687474709' -        } +     +    # hard coded option 43 - for testing purposes +    option43 = { +        'length': hex(30), +        'value': '01162f746731352d656467652f746573742e636f6e666967030468747470' +    } +     +    # Test parsing +    options = parse_options(b'3501013c3c4a756e697065722d6578323230302d632d3132742d3267000000000000000000000000000000000000000000000000000000000000000000000000005222012064697374726f2d746573743a67652d302f302f302e303a626f6f747374726170ff') + +    if int(messagesplit[10]) is not 0: +        print('DHCP packet forwarded by relay %s' % hex_ip_to_str(messagesplit[10])) +    else: +        print('DHCP packet not forwarded - direct request') -        # -        # Crafting DHCP OFFER -        # -        # {82: {1: 'distro-test:ge-0/0/0.0:bootstrap'}, 60: 'Juniper-ex2200-c-12t-2g', 53: 1} -        #options = \xcode \xlength \xdata -        print(' --> crafting response') -        lease=getlease(messagesplit[11].decode()) # Decodes MAC address -        # print(binascii.unhexlify(messagesplit[4])) -        # print('length: ' + str(len(binascii.unhexlify(messagesplit[4])))); +    if messagesplit[15][:6] == b'350101': # option 53 (should allways be the first option in DISCOVER/REQUEST) - identifies DHCP packet type - discover/request/offer/ack++ +        print('\n\nDHCP DISCOVER - client MAC %s' % format_hex_mac(messagesplit[11])) +        print(' --> crafting DHCP OFFER response') +        lease = getlease(messagesplit[11].decode()) # Decodes MAC address +          # DHCP OFFER details - Options          data = b'\x02' # Message type - boot reply          data += b'\x01' # Hardware type - ethernet @@ -185,19 +163,11 @@ def reqparse(message): #handles either DHCPDiscover or DHCPRequest          data += b'\x63\x82\x53\x63' # Magic cookie          # DHCP Options - ordered by pcapng "proof of concept" file -        data += b'\x35\x01\x02' # Option 53 - DHCP OFFER -        data += b'\x36\x04' + socket.inet_aton(address) # Option 54 - DHCP server identifier -        data += b'\x33\x04' + binascii.unhexlify(b'00012340') # Option 51 - Lease time left padded with "0" -        data += b'\x01\x04' + socket.inet_aton(netmask) # Option 1 - Subnet mask -        data += b'\x03\x04' + binascii.unhexlify(messagesplit[10]) # Option 3 - Router (set to DHCP forwarders IP) -        data += b'\x96\x04' + socket.inet_aton(address) # Option 150 - TFTP Server -        # data += '\x2b'  + option43['length'] + option43['value'] # Option 43 - Magic ZTP stuff -        # data += '\x03\x04' + option82_raw # Option 82 - with suboptions -        data += b'\xff' +        data += craft_option(53).raw_hex(b'\x02') # Option 53 - DHCP OFFER      elif messagesplit[15][:6] == b'350103':          print('\n\nDHCP REQUEST - client MAC %s' % format_hex_mac(messagesplit[11])) -        print(' --> crafting response') +        print(' --> crafting DHCP ACK response')          data = b'\x02' # Message type - boot reply          data += b'\x01' # Hardware type - ethernet @@ -207,7 +177,6 @@ def reqparse(message): #handles either DHCPDiscover or DHCPRequest          data += b'\x00\x01' # seconds elapsed - 1 second          data += b'\x80\x00' # BOOTP flags - broadcast (unicast: 0x0000)          data += b'\x00'*4 # Client IP address -        # data += binascii.unhexlify(messagesplit[15][messagesplit[15].find('3204')+4:messagesplit[15].find('3204')+12])          data += binascii.unhexlify(messagesplit[8]) # New IP to client          data += socket.inet_aton(address) # Next server IP addres - self          data += binascii.unhexlify(messagesplit[10]) # Relay agent IP - DHCP forwarder @@ -217,12 +186,27 @@ def reqparse(message): #handles either DHCPDiscover or DHCPRequest          # DHCP Options - ordered by pcapng "proof of concept" file          data += b'\x35\x01\05' # Option 53 - DHCP ACK -        data += b'\x36\x04' + socket.inet_aton(address) # Option 54 - DHCP server identifier -        data += b'\x33\x04' + binascii.unhexlify(b'00012340') # Option 51 - Lease time left padded with "0" -        data += b'\x01\x04' + socket.inet_aton(netmask) # Option 1 - Subnet mask -        data += b'\x03\x04' + binascii.unhexlify(messagesplit[10]) # Option 3 - Router (set to DHCP forwarders IP) -        data += b'\x96\x04' + socket.inet_aton(address) # Option 150 - TFTP Server -        data += b'\xff' +    else: +        print('Unexpected DHCP option 53 - stopping processing request') +        return None + + +    # common options for both DHCP REPLY and DHCP ACK - should be most of the options +    data += craft_option(54).bytes(socket.inet_aton(address)) # Option 54 - DHCP server identifier +    data += craft_option(51).raw_hex(b'\x00\x00\xff\x00') # Option 51 - Lease time left padded with "0" +    data += craft_option(1).ip(netmask) # Option 1 - Subnet mask +     +    # Set option 3 - default gateway. Only applicable if messagesplit[10] (DHCP forwarder (GIADDR)) is set +    if messagesplit[10] is not b'00000000': +        data += craft_option(3).bytes(messagesplit[10]) # Option 3 - Default gateway (set to DHCP forwarders IP) +    else: +        data += craft_option(3).bytes(socket.inet_aton(address)) # Option 3 - Default gateway (set to DHCP servers IP) + +    data += craft_option(43).raw_hex(binascii.unhexlify(option43['value'])) # Option 43 - ZTP +    data += craft_option(150).bytes(socket.inet_aton(address)) # Option 150 - TFTP Server +    # data += '\x03\x04' + option82_raw # Option 82 - with suboptions +    data += b'\xff' +              return data  def release(): #release a lease after timelimit has expired @@ -249,7 +233,7 @@ def getlease(hwaddr): #return the lease of mac address, or create if doesn't exi           return lease[0]  if __name__ == "__main__": -    interface = 'eth0' +    interface = b'eth0'      port = 67      address = '10.0.100.2'      offerfrom = '10.0.0.100' @@ -259,7 +243,6 @@ if __name__ == "__main__":      tftp = address      dns = '8.8.8.8'      gateway = address -    pxefilename = 'pxelinux.0'      leasetime=86400 #int      leases=[] # leases database @@ -269,23 +252,17 @@ if __name__ == "__main__":          leases.append(['10.0.0.' + str(octet), False, '000000000000', 0])      #     leases.append([ip,False,'000000000000',0]) -    # TODO: Support for binding to interface / IP      s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # IPv4 UDP socket -    # python 2.7: s.setsockopt(socket.SOL_SOCKET,IN.SO_BINDTODEVICE,interface+'\0') #experimental -    # s.setsockopt(socket.SOL_SOCKET, IN.SO_BINDTODEVICE, interface+'\0') #experimental -    # s.bind(address)      s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)      s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) -    s.setsockopt(socket.SOL_SOCKET, 25, b'eth0') +    s.setsockopt(socket.SOL_SOCKET, 25, interface)      s.bind(('', 67))      print('starting main loop')      while 1: #main loop          try:              message, addressf = s.recvfrom(8192) -            print('received something!') -            print(message) -             +            # print(message)              if message.startswith(b'\x01'): # UDP payload is DHCP request (discover, request, release)                  if addressf[0] == '0.0.0.0':                      print('DHCP broadcast') @@ -293,17 +270,11 @@ if __name__ == "__main__":                  else:                      print('DHCP unicast - DHCP forwarding')                      reply_to = addressf[0] -                # print(message.decode('ISO-8859-1'))                  data=reqparse(message) # Parse the DHCP request                  if data: -                    # print(options_raw) -                    # data = str.encode(data)                      print(' -- > replying to %s' % reply_to) -                    print(b'replying with UDP payload: ' + data) +                    # print(b'replying with UDP payload: ' + data)                      s.sendto(data, ('<broadcast>', 68)) # Sends reply -                    # s.sendto(data,(reply_to,68)) # Sends reply -                release() #update releases table -            else: -                print('not DHCP') +                release() # update releases table          except KeyboardInterrupt:              exit() | 
