diff options
author | Jonas Lindstad <jonaslindstad@gmail.com> | 2015-02-01 23:42:28 +0100 |
---|---|---|
committer | Jonas Lindstad <jonaslindstad@gmail.com> | 2015-02-01 23:42:28 +0100 |
commit | cda7f1012da4e27d1f9fcba5edb7f30fbb601eab (patch) | |
tree | 358925083e0859790cc8a0944d916ba1e2192f28 /junos-bootstrap | |
parent | 28b84878b7d178e977d493602bd935cb4a061a9c (diff) |
Moved option crafting to separate class, cleanup, fixes. Still not functional, but getting close
Diffstat (limited to 'junos-bootstrap')
-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() |