From 6d2ae5a26bf3f8f430de0950469b6f753a6aee10 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Tue, 2 Feb 2021 17:08:24 +0100 Subject: [PATCH 01/22] Add protocol v2 support and listen for broadcast/multicast too --- ubnt_discovery.py | 288 ++++++++++++++++++++++++++++++---------------- 1 file changed, 189 insertions(+), 99 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 80e2a1b..8d4132a 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 ########################################## # UBNT command line discovery tool # @@ -6,127 +6,217 @@ # www.bvnetworks.it # ########################################## +import argparse +import json from random import randint +import sys +from struct import unpack from scapy.all import * -# UBNT field types -UBNT_MAC = '01' -UBNT_MAC_AND_IP = '02' -UBNT_FIRMWARE = '03' -UBNT_UNKNOWN_2 = '0a' -UBNT_RADIONAME = '0b' -UBNT_MODEL_SHORT = '0c' -UBNT_ESSID = '0d' -UBNT_UNKNOWN_3 = '0e' -UBNT_UNKNOWN_1 = '10' -UBNT_MODEL_FULL = '14' +def mac_repr(data): + return ':'.join(('%02x' % b) for b in data) +def ip_repr(data): + return '.'.join(('%d' % b) for b in data) + +# Wirelss modes +UBNT_WIRELESS_MODES ={ + 0x00: "Auto", + 0x01: "adhoc", + 0x02: "Station", + 0x03: "AP", + 0x04: "Repeater", + 0x05: "Secondary", + 0x06: "Monitor", +}; + +# field type -> (field name; parsing function (bytes->str); \ +# is it expected to be seen multiple times?) +FIELD_PARSERS = { + 0x01: ('mac2', mac_repr, False), + 0x02: ('mac_ip', lambda data: '%s;%s' % (mac_repr(data[0:6]), ip_repr(data[6:10])), True), + 0x03: ('firmware', bytes.decode, False), + 0x0a: ('uptime', lambda data: int.from_bytes(data, 'big'), False), + 0x0b: ('name', bytes.decode, False), + 0x0c: ('model_short', bytes.decode, False), + 0x0d: ('essid', bytes.decode, False), + 0x0e: ('wlan_mode', lambda data: UBNT_WIRELESS_MODES.get(data[0], 'unknown'), False), + 0x10: ('unknown1', str, False), + 0x14: ('model', bytes.decode, False), + 0x0f: ('unknown2 (unifi-os related?)', str, False), + 0x16: ('firmware_short', bytes.decode, False), + 0x17: ('unknown3', lambda data: int.from_bytes(data, 'big'), False), + 0x18: ('default_config', lambda data: int.from_bytes(data, 'big'), False), + 0x2d: ('unknown5 (led related?)', str, False), + 0x2e: ('unknown6 (led related?)', str, False), + 0x15: ('model_short', bytes.decode, False), + 0x24: ('unknown9 TS?', lambda data: int.from_bytes(data, 'big'), False), + 0x22: ('unknown10', str, False), + 0x21: ('unknown11', str, False), + 0x27: ('unknown12', str, False), + 0x19: ('unknown13', lambda data: int.from_bytes(data, 'big'), False), + 0x1a: ('unknown14', lambda data: int.from_bytes(data, 'big'), False), + 0x13: ('mac3', mac_repr, False), + 0x12: ('unknown16 Changes', str, False), + 0x1b: ('firmware_weird', bytes.decode, False), +} + +# Basic fields: src MAC and IP of reply message; not parsed +BASIC_FIELDS = { 'mac', 'ip', 'Signature version' } + +# String representation of non-basic fields +FIELD_STR = { + 'mac2': 'MAC (Serial)', + 'mac3': 'MAC 3', + 'mac_ip': 'MAC-IP Pairs', + 'firmware': 'Firmware', + 'firmware_short': 'Firmware (short)', + 'uptime': 'Uptime', + 'name': 'Hostname', + 'model_short': 'Model (short)', + 'essid': 'ESSID', + 'wlan_mode':'WLAN Mode', + 'model': 'Model', + 'default_config': 'Default configuration', +} # UBNT discovery packet payload and reply signature -UBNT_REQUEST_PAYLOAD = '01000000' -UBNT_REPLY_SIGNATURE = '010000' +UBNT_REQUEST_PAYLOAD = b'\x01\x00\x00\x00' +UBNT_V1_SIGNATURE = b'\x01\x00\x00' +UBNT_V2_SIGNATURE = b'\x02\x06\x00' -# Offset within the payload that contains the amount of bytes remaining -offset_PayloadRemainingBytes = 3 +# Discovery timeout. Change this for quicker discovery +DISCOVERY_TIMEOUT_ACTIVE = 2 +DISCOVERY_TIMEOUT_PASSIVE = 10 + +def parse_args(): + parser = argparse.ArgumentParser( + description="Discovers ubiquiti devices on network using ubnt device discovery protocol") + parser.add_argument( + 'interface', help="the interface you want to use for discovery") + parser.add_argument( + '--output-format', type=str, default='text', choices=('text', 'json'), + help="output format") + + return parser.parse_args() + +def iter_fields(data, _len): + pointer = 0 + while pointer < _len: + fieldType, fieldLen = unpack('>BH', data[pointer:pointer+3]) + pointer += 3 + fieldData = data[pointer:pointer+fieldLen] + pointer += fieldLen + yield fieldType, fieldData + +def ubntResponseParse(rcv): + # We received a broadcast packet in reply to our discovery + payload = rcv[IP].load + + if payload[0:4] == UBNT_REQUEST_PAYLOAD: # Check for a UBNT discovery request (first 4 bytes of the payload should be \x01\x00\x00\x00) + return False + elif payload[0:3] == UBNT_V1_SIGNATURE: # Check for a valid UBNT discovery reply (first 3 bytes of the payload should be \x01\x00\x00) + Device = {} # This should be a valid discovery reply packet sent by an Ubiquiti device + Device['Signature version'] = '1' # this is not allways correct + elif payload[0:3] == UBNT_V2_SIGNATURE: + Device = {} # This should be a valid discovery broadcast packet sent by an Ubiquiti device + Device['Signature version'] = '2' + else: + return False # Not a valid UBNT discovery reply, skip to next received packet + + Device['ip'] = \ + rcv[IP].src # We avoid going through the hassle of enumerating type '02' fields (MAC+IP). There may + # be multiple IPs on the device, and therefore multiple type '02' fields in the + # reply packet. We conveniently pick the address from which the device + # replied to our discovery request directly from the reply packet, and store it. + + Device['mac'] = rcv[Ether].src.upper() # Read comment above, this time regarding the MAC Address. + + # Walk the reply payload, staring from offset 04 (just after reply signature and payload size). + # Take into account the payload length in offset 3 + for fieldType, fieldData in iter_fields(payload[4:], payload[3]): + + if fieldType not in FIELD_PARSERS: + sys.stderr.write("notice: unknown field type 0x%x: data %s\n" % + (fieldType, fieldData)) + continue + + # Parse the field and store in Device + fieldName, fieldParser, isMany = FIELD_PARSERS[fieldType] + if isMany: + if fieldName not in Device: Device[fieldName] = [] + Device[fieldName].append(fieldParser(fieldData)) + else: + Device[fieldName] = fieldParser(fieldData) -# Offset within the payload where we'll find the first field -offset_FirstField = 4 + return Device -# Discovery timeout. Change this for quicker discovery -DISCOVERY_TIMEOUT = 5 +def ubntDiscovery(iface): + if not iface in get_if_list(): + raise ValueError('{} is not a valid network interface'.format(iface)) -def ubntDiscovery(): + src_mac = get_if_hwaddr(iface) # Prepare and send our discovery packet conf.checkIPaddr = False # we're broadcasting our discovery packet from a local IP (local->255.255.255.255) - # but we'll expect a reply on the broadcast IP as well (radioIP->255.255.255.255), + # but we'll expect a reply on the broadcast IP as well (deviceIP->255.255.255.255), # not on our local IP. # Therefore we must disable destination IP checking in scapy - ubnt_discovery_packet = Ether(dst="ff:ff:ff:ff:ff:ff")/\ + conf.iface = iface + ubnt_discovery_packet = Ether(dst="ff:ff:ff:ff:ff:ff", src=src_mac)/\ IP(dst="255.255.255.255")/\ UDP(sport=randint(1024,65535),dport=10001)/\ - Raw(UBNT_REQUEST_PAYLOAD.decode('hex')) + Raw(UBNT_REQUEST_PAYLOAD) + + # do active discovery first, after it we do passive discovery + ans, unans = srp(ubnt_discovery_packet, multi=True, # We want to allow multiple radios to reply to our discovery packet verbose=0, # Suppress scapy output - timeout=DISCOVERY_TIMEOUT) + timeout=DISCOVERY_TIMEOUT_ACTIVE) - # Loop over received packets - RadioList = [] + DeviceList = [] for snd,rcv in ans: - # We received a broadcast packet in reply to our discovery - payload = rcv[IP].load + # Store the data we gathered from the reply packet + device = ubntResponseParse(rcv) + if device != False: + DeviceList.append(device) - # Check for a valid UBNT discovery reply (first 3 bytes of the payload should be \x01\x00\x00) - if payload[0:3].encode('hex') == UBNT_REPLY_SIGNATURE: - Radio = {} # This should be a valid discovery reply packet sent by an Ubiquiti radio - else: - continue # Not a valid UBNT discovery reply, skip to next received packet - - RadioIP = rcv[IP].src # We avoid going through the hassle of enumerating type '02' fields (MAC+IP). There may - # be multiple IPs on the radio, and therefore multiple type '02' fields in the - # reply packet. We conveniently pick the address from which the radio - # replied to our discovery request directly from the reply packet, and store it. - - RadioMAC = rcv[Ether].src # Read comment above, this time regarding the MAC Address. - RadioMAC = RadioMAC.upper() - - # Retrieve payload size (excluding initial signature) - pointer = offset_PayloadRemainingBytes - remaining_bytes = int( payload[pointer].encode('hex'), 16 ) - - # Walk the reply payload, staring from offset 04 (just after reply signature and payload size). - pointer += 1 - remaining_bytes -= 1 - while remaining_bytes > 0: - fieldType = payload[pointer].encode('hex') - pointer += 1 - remaining_bytes -= 1 - fieldLen = payload[pointer:pointer+2].encode('hex') # Data length is stored as a 16-bit word - fieldLen = int( fieldLen, 16 ) - pointer += 2 - remaining_bytes -= 2 - fieldData = payload[pointer:pointer+fieldLen] - if fieldType == UBNT_RADIONAME: - RadioName = fieldData - elif fieldType == UBNT_MODEL_FULL: - RadioModel = fieldData - elif fieldType == UBNT_MODEL_SHORT: - RadioModelShort = fieldData - elif fieldType == UBNT_FIRMWARE: - RadioFirmware = fieldData - elif fieldType == UBNT_ESSID: - RadioEssid = fieldData - # We don't know or care about other field types. Continue walking the payload. - pointer += fieldLen - remaining_bytes -= fieldLen + + # passive discovery + + send(ubnt_discovery_packet, verbose=0) + + ans = sniff(filter='dst port 10001', timeout=DISCOVERY_TIMEOUT_PASSIVE) + + # Loop over received packets + for rcv in ans: # Store the data we gathered from the reply packet - Radio['ip'] = RadioIP - Radio['mac'] = RadioMAC - Radio['name'] = RadioName - Radio['model'] = RadioModel - Radio['essid'] = RadioEssid - Radio['firmware'] = RadioFirmware - Radio['model_short'] = RadioModelShort - RadioList.append(Radio) - - return RadioList - - -print("\nDiscovery in progress...") -RadioList = ubntDiscovery() -found_radios = len(RadioList) -if found_radios: - print("\nDiscovered " + str(found_radios) + " radio(s):") - for Radio in RadioList: - print("\n--- [" + Radio['model'] + "] ---") - print(" IP Address : " + Radio['ip']) - print(" Name : " + Radio['name']) - print(" Model : " + Radio['model_short']) - print(" Firmware : " + Radio['firmware']) - print(" ESSID : " + Radio['essid']) - print(" MAC Address : " + Radio['mac']) -else: - print("\nNo radios discovered\n") + device = ubntResponseParse(rcv) + if device != False: + DeviceList.append(device) + + return DeviceList + +if __name__ == '__main__': + args = parse_args() + sys.stderr.write("\nDiscovery in progress...\n") + DeviceList = ubntDiscovery(args.interface) + found_devices = len(DeviceList) + if args.output_format == 'text': + if not found_devices: + sys.stderr.write("\n\nNo devices discovered\n") + sys.exit() + print("\nDiscovered %d device(s):" % found_devices) + fmt = " %-30s: %s" + for Device in DeviceList: + print("\n---[ %s ]---" % Device['mac']) + print(fmt % ("IP Address", Device['ip'])) + for field in Device: + if field in BASIC_FIELDS: continue + print(fmt % (FIELD_STR.get(field, field), + Device[field])) + elif args.output_format == 'json': + print(json.dumps(DeviceList, indent=2)) From fab7566bb24fc90c9f366b1f56d8fb9798caa3b1 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Tue, 2 Feb 2021 18:10:08 +0100 Subject: [PATCH 02/22] Fixed breaking on ipv6 packets --- ubnt_discovery.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 8d4132a..786c369 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -46,6 +46,7 @@ def ip_repr(data): 0x16: ('firmware_short', bytes.decode, False), 0x17: ('unknown3', lambda data: int.from_bytes(data, 'big'), False), 0x18: ('default_config', lambda data: int.from_bytes(data, 'big'), False), + 0x2a: ('unknown17', str, False), 0x2d: ('unknown5 (led related?)', str, False), 0x2e: ('unknown6 (led related?)', str, False), 0x15: ('model_short', bytes.decode, False), @@ -61,7 +62,7 @@ def ip_repr(data): } # Basic fields: src MAC and IP of reply message; not parsed -BASIC_FIELDS = { 'mac', 'ip', 'Signature version' } +BASIC_FIELDS = { 'mac', 'ip' } # String representation of non-basic fields FIELD_STR = { @@ -110,7 +111,7 @@ def iter_fields(data, _len): def ubntResponseParse(rcv): # We received a broadcast packet in reply to our discovery - payload = rcv[IP].load + payload = rcv.load if payload[0:4] == UBNT_REQUEST_PAYLOAD: # Check for a UBNT discovery request (first 4 bytes of the payload should be \x01\x00\x00\x00) return False @@ -123,12 +124,6 @@ def ubntResponseParse(rcv): else: return False # Not a valid UBNT discovery reply, skip to next received packet - Device['ip'] = \ - rcv[IP].src # We avoid going through the hassle of enumerating type '02' fields (MAC+IP). There may - # be multiple IPs on the device, and therefore multiple type '02' fields in the - # reply packet. We conveniently pick the address from which the device - # replied to our discovery request directly from the reply packet, and store it. - Device['mac'] = rcv[Ether].src.upper() # Read comment above, this time regarding the MAC Address. # Walk the reply payload, staring from offset 04 (just after reply signature and payload size). @@ -186,8 +181,6 @@ def ubntDiscovery(iface): # passive discovery - send(ubnt_discovery_packet, verbose=0) - ans = sniff(filter='dst port 10001', timeout=DISCOVERY_TIMEOUT_PASSIVE) # Loop over received packets @@ -213,7 +206,6 @@ def ubntDiscovery(iface): fmt = " %-30s: %s" for Device in DeviceList: print("\n---[ %s ]---" % Device['mac']) - print(fmt % ("IP Address", Device['ip'])) for field in Device: if field in BASIC_FIELDS: continue print(fmt % (FIELD_STR.get(field, field), From 6f3e9fc6bb4070dd0f0a7cb2510129933bc26eba Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Wed, 10 Feb 2021 22:45:15 +0100 Subject: [PATCH 03/22] Added more names of fields, might need some changes for v1 and v2 header --- ubnt_discovery.py | 86 ++++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 786c369..d8ddece 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -32,33 +32,50 @@ def ip_repr(data): # field type -> (field name; parsing function (bytes->str); \ # is it expected to be seen multiple times?) FIELD_PARSERS = { - 0x01: ('mac2', mac_repr, False), - 0x02: ('mac_ip', lambda data: '%s;%s' % (mac_repr(data[0:6]), ip_repr(data[6:10])), True), - 0x03: ('firmware', bytes.decode, False), - 0x0a: ('uptime', lambda data: int.from_bytes(data, 'big'), False), - 0x0b: ('name', bytes.decode, False), - 0x0c: ('model_short', bytes.decode, False), - 0x0d: ('essid', bytes.decode, False), - 0x0e: ('wlan_mode', lambda data: UBNT_WIRELESS_MODES.get(data[0], 'unknown'), False), - 0x10: ('unknown1', str, False), +# These are validated + 0x01: ('HWADDR', mac_repr, False), + 0x02: ('IPINFO', lambda data: '%s;%s' % (mac_repr(data[0:6]), ip_repr(data[6:10])), True), + 0x03: ('FWVERSION', bytes.decode, False), + 0x0a: ('UPTIME', lambda data: int.from_bytes(data, 'big'), False), + 0x0b: ('HOSTNAME', bytes.decode, False), + 0x0c: ('PLATFORM', bytes.decode, False), + 0x0d: ('ESSID', bytes.decode, False), + 0x0e: ('WMODE', lambda data: UBNT_WIRELESS_MODES.get(data[0], 'unknown'), False), + 0x10: ('SYSTEM_ID', str, False), + 0x15: ('MODEL', bytes.decode, False), + 0x16: ('VERSION', bytes.decode, False), + 0x17: ('MGMT_IS_DEFAULT', lambda data: int.from_bytes(data, 'big'), False), + 0x19: ('MGMT_USING_DHCPC', lambda data: int.from_bytes(data, 'big'), False), + 0x1a: ('MGMT_DHCPC_BOUND', lambda data: int.from_bytes(data, 'big'), False), + 0x1b: ('REQUIRED_VERSION', bytes.decode, False), +# These need checking + 0x04: ('ADDR_ENTRY', str, False), + 0x05: ('MAC_ENTRY', str, False), + 0x06: ('USERNAME', str, False), + 0x07: ('SALT', str, False), + 0x08: ('RND_CHALLENGE', str, False), + 0x09: ('CHALLENGE_RESPONSE', str, False), + 0x0f: ('MGMT_URL', str, False), + 0x11: ('MGMT_LOCATE_SECONDS', str, False), + 0x12: ('SEQ', str, False), + 0x13: ('SRC_MACID', mac_repr, False), + 0x1c: ('SSHD_PORT', str, False), + 0x1d: ('PLATFORM_UVP', str, False), + 0x1e: ('TALK_ANONYMOUS_DEVICE_ID', str, False), +# Same for these 0x14: ('model', bytes.decode, False), - 0x0f: ('unknown2 (unifi-os related?)', str, False), - 0x16: ('firmware_short', bytes.decode, False), - 0x17: ('unknown3', lambda data: int.from_bytes(data, 'big'), False), + 0x14: ('DST_MACID', str, False), 0x18: ('default_config', lambda data: int.from_bytes(data, 'big'), False), + 0x18: ('MGMT_IS_LOCATING', lambda data: int.from_bytes(data, 'big'), False), +# These need names + 0x0f: ('unknown2 (unifi-os related?)', str, False), + 0x21: ('unknown11', str, False), + 0x22: ('unknown10', str, False), + 0x24: ('unknown9 TS?', lambda data: int.from_bytes(data, 'big'), False), + 0x27: ('unknown12', str, False), 0x2a: ('unknown17', str, False), 0x2d: ('unknown5 (led related?)', str, False), 0x2e: ('unknown6 (led related?)', str, False), - 0x15: ('model_short', bytes.decode, False), - 0x24: ('unknown9 TS?', lambda data: int.from_bytes(data, 'big'), False), - 0x22: ('unknown10', str, False), - 0x21: ('unknown11', str, False), - 0x27: ('unknown12', str, False), - 0x19: ('unknown13', lambda data: int.from_bytes(data, 'big'), False), - 0x1a: ('unknown14', lambda data: int.from_bytes(data, 'big'), False), - 0x13: ('mac3', mac_repr, False), - 0x12: ('unknown16 Changes', str, False), - 0x1b: ('firmware_weird', bytes.decode, False), } # Basic fields: src MAC and IP of reply message; not parsed @@ -66,18 +83,19 @@ def ip_repr(data): # String representation of non-basic fields FIELD_STR = { - 'mac2': 'MAC (Serial)', - 'mac3': 'MAC 3', - 'mac_ip': 'MAC-IP Pairs', - 'firmware': 'Firmware', - 'firmware_short': 'Firmware (short)', - 'uptime': 'Uptime', - 'name': 'Hostname', - 'model_short': 'Model (short)', - 'essid': 'ESSID', - 'wlan_mode':'WLAN Mode', - 'model': 'Model', - 'default_config': 'Default configuration', + 'HWADDR': 'HWADDR', +# 'HWADDR': 'MAC (Serial)', +# 'mac3': 'MAC 3', +# 'IPINFO': 'MAC-IP Pairs', +# 'FWVERSION': 'Firmware', +# 'firmware_short': 'Firmware (short)', +# 'uptime': 'Uptime', +# 'name': 'Hostname', +# 'model_short': 'Model (short)', +# 'essid': 'ESSID', +# 'wlan_mode':'WLAN Mode', +# 'model': 'Model', +# 'default_config': 'Default configuration', } # UBNT discovery packet payload and reply signature From 4dcf96785ee6c286fa4c0b2be2c60e4c47853a0e Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Wed, 10 Feb 2021 22:46:36 +0100 Subject: [PATCH 04/22] Add more comments --- ubnt_discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index d8ddece..f596dfa 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -62,7 +62,7 @@ def ip_repr(data): 0x1c: ('SSHD_PORT', str, False), 0x1d: ('PLATFORM_UVP', str, False), 0x1e: ('TALK_ANONYMOUS_DEVICE_ID', str, False), -# Same for these +# Same for these, it might be that it is v1 vs v2 0x14: ('model', bytes.decode, False), 0x14: ('DST_MACID', str, False), 0x18: ('default_config', lambda data: int.from_bytes(data, 'big'), False), From 154b31f8ac0d96e98beeac7545f5b9944a577ddd Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Wed, 10 Feb 2021 23:42:06 +0100 Subject: [PATCH 05/22] Verified more fields --- ubnt_discovery.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index f596dfa..315ae43 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -41,13 +41,17 @@ def ip_repr(data): 0x0c: ('PLATFORM', bytes.decode, False), 0x0d: ('ESSID', bytes.decode, False), 0x0e: ('WMODE', lambda data: UBNT_WIRELESS_MODES.get(data[0], 'unknown'), False), - 0x10: ('SYSTEM_ID', str, False), + 0x10: ('SYSTEM_ID', lambda data: int.from_bytes(data, 'big'), False), + 0x12: ('SEQ', lambda data: int.from_bytes(data, 'big'), False), + 0x13: ('SRC_MACID', mac_repr, False), 0x15: ('MODEL', bytes.decode, False), 0x16: ('VERSION', bytes.decode, False), 0x17: ('MGMT_IS_DEFAULT', lambda data: int.from_bytes(data, 'big'), False), 0x19: ('MGMT_USING_DHCPC', lambda data: int.from_bytes(data, 'big'), False), 0x1a: ('MGMT_DHCPC_BOUND', lambda data: int.from_bytes(data, 'big'), False), 0x1b: ('REQUIRED_VERSION', bytes.decode, False), + 0x1c: ('SSHD_PORT', lambda data: int.from_bytes(data, 'big'), False), + 0x1e: ('TALK_ANONYMOUS_DEVICE_ID', bytes.decode, False), # These need checking 0x04: ('ADDR_ENTRY', str, False), 0x05: ('MAC_ENTRY', str, False), @@ -57,25 +61,21 @@ def ip_repr(data): 0x09: ('CHALLENGE_RESPONSE', str, False), 0x0f: ('MGMT_URL', str, False), 0x11: ('MGMT_LOCATE_SECONDS', str, False), - 0x12: ('SEQ', str, False), - 0x13: ('SRC_MACID', mac_repr, False), - 0x1c: ('SSHD_PORT', str, False), 0x1d: ('PLATFORM_UVP', str, False), - 0x1e: ('TALK_ANONYMOUS_DEVICE_ID', str, False), # Same for these, it might be that it is v1 vs v2 0x14: ('model', bytes.decode, False), 0x14: ('DST_MACID', str, False), 0x18: ('default_config', lambda data: int.from_bytes(data, 'big'), False), 0x18: ('MGMT_IS_LOCATING', lambda data: int.from_bytes(data, 'big'), False), # These need names - 0x0f: ('unknown2 (unifi-os related?)', str, False), - 0x21: ('unknown11', str, False), - 0x22: ('unknown10', str, False), - 0x24: ('unknown9 TS?', lambda data: int.from_bytes(data, 'big'), False), - 0x27: ('unknown12', str, False), - 0x2a: ('unknown17', str, False), - 0x2d: ('unknown5 (led related?)', str, False), - 0x2e: ('unknown6 (led related?)', str, False), + 0x0f: ('unknown1 (unifi-os related?)', lambda data: int.from_bytes(data, 'big'), False), + 0x21: ('unknown2', str, False), + 0x22: ('unknown3', str, False), + 0x24: ('unknown4 TS?', lambda data: int.from_bytes(data, 'big'), False), + 0x27: ('unknown5', str, False), + 0x2a: ('unknown6', str, False), + 0x2d: ('unknown7 (led related?)', lambda data: int.from_bytes(data, 'big'), False), + 0x2e: ('unknown8 (led related?)', str, False), } # Basic fields: src MAC and IP of reply message; not parsed From 6ae4804f8c425efc61e548cc801432b5bf438e41 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Wed, 10 Feb 2021 23:52:38 +0100 Subject: [PATCH 06/22] Start of version specific fields --- ubnt_discovery.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 315ae43..cd17476 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -78,6 +78,16 @@ def ip_repr(data): 0x2e: ('unknown8 (led related?)', str, False), } +FIELD_PARSERS_V1 = { + 0x14: ('model', bytes.decode, False), + 0x18: ('default_config', lambda data: int.from_bytes(data, 'big'), False), +} + +FIELD_PARSERS_V2 = { + 0x14: ('DST_MACID', str, False), + 0x18: ('MGMT_IS_LOCATING', lambda data: int.from_bytes(data, 'big'), False), +} + # Basic fields: src MAC and IP of reply message; not parsed BASIC_FIELDS = { 'mac', 'ip' } From 74b2258586fabf86a0b6e2825736190d8efff274 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Fri, 12 Feb 2021 22:45:43 +0100 Subject: [PATCH 07/22] Document more information about the protocol --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6fe60c1..9a28377 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,20 @@ Command line python script to discover Ubiquiti devices on the local LAN segment ####Ubiquiti Discovery Protocol brief description -*Disclaimer: this code is based exclusively on packet sniffing and analysis, there are some fields that remain unknown to me. -This code may therefore not be compatible with all devices. -I have not tested this on Unifi APs or EdgeOS products.* +*Disclaimer: there are some fields that remain unknown. This code may therefore not be compatible with all devices.* -Ubiquiti discovery works by sending an UDP packet to the local broadcast address (255.255.255.255) on port **10001**, +There are multiple methods of the Ubiquiti discovery protocol. + +Method 1 works by sending an UDP packet to the local broadcast address (255.255.255.255) on port **10001**, containing 4 bytes in the payload, namely `01 00 00 00`, and waiting for UDP replies destined to the local broadcast address. +Method 2 works by periodacly sending an UDP packet to the local broadcast address (255.255.255.255) on port **10001**. + +Method 3 is multicast on the address 233.89.188.1. + The payload of the reply packet sent by the radio is structured as follows: -- offset `00` (3 bytes) : *Ubiquiti discovery reply signature (*`0x01 0x00 0x00`*). We'll check this to make sure it's a valid discovery-reply packet.* +- offset `00` (3 bytes) : *Ubiquiti discovery reply signature the first byte is the version. Depending on the version the field definitions change.* - offset `03` (1 byte) : *Payload size (excluding signature)* Starting at offset `04`, the structure of the payload is as follows: From 62b16269853a9c98e4b2ac5436d82d8f50e00cdf Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sat, 13 Feb 2021 23:39:54 +0100 Subject: [PATCH 08/22] Add option to fetch packets from pcap file --- README.md | 4 ++- ubnt_discovery.py | 83 ++++++++++++++++++++++++++++++----------------- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 9a28377..eee4235 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,15 @@ Command line python script to discover Ubiquiti devices on the local LAN segment *Disclaimer: there are some fields that remain unknown. This code may therefore not be compatible with all devices.* +specify the interface with `--interface ` or/and load a pcap file with `--pcap ` + There are multiple methods of the Ubiquiti discovery protocol. Method 1 works by sending an UDP packet to the local broadcast address (255.255.255.255) on port **10001**, containing 4 bytes in the payload, namely `01 00 00 00`, and waiting for UDP replies destined to the local broadcast address. -Method 2 works by periodacly sending an UDP packet to the local broadcast address (255.255.255.255) on port **10001**. +Method 2 works by periodacly sending an UDP packet to the local broadcast address (255.255.255.255) on port **10001**. Method 3 is multicast on the address 233.89.188.1. diff --git a/ubnt_discovery.py b/ubnt_discovery.py index cd17476..81eadb5 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -121,7 +121,9 @@ def parse_args(): parser = argparse.ArgumentParser( description="Discovers ubiquiti devices on network using ubnt device discovery protocol") parser.add_argument( - 'interface', help="the interface you want to use for discovery") + '--interface', type=str, help="the interface you want to use for discovery") + parser.add_argument( + '--pcap', type=str, help="analyze a pcap file for discovery info") parser.add_argument( '--output-format', type=str, default='text', choices=('text', 'json'), help="output format") @@ -173,58 +175,81 @@ def ubntResponseParse(rcv): return Device -def ubntDiscovery(iface): +def ubntDiscovery(args): + + DeviceList = [] + + if args.pcap is not None: + + packets = rdpcap(args.pcap) + + for packet in packets: + + if packet[UDP].dport == 10001 or packet[UDP].sport == 10001: - if not iface in get_if_list(): - raise ValueError('{} is not a valid network interface'.format(iface)) + device = ubntResponseParse(packet) - src_mac = get_if_hwaddr(iface) + print(device) + if device != False: + DeviceList.append(device) - # Prepare and send our discovery packet - conf.checkIPaddr = False # we're broadcasting our discovery packet from a local IP (local->255.255.255.255) + if args.interface is not None: + + iface = args.interface + + if not args.interface in get_if_list(): + raise ValueError('{} is not a valid network interface'.format(iface)) + + src_mac = get_if_hwaddr(args.interface) + + # Prepare and send our discovery packet + conf.checkIPaddr = False # we're broadcasting our discovery packet from a local IP (local->255.255.255.255) # but we'll expect a reply on the broadcast IP as well (deviceIP->255.255.255.255), # not on our local IP. # Therefore we must disable destination IP checking in scapy - conf.iface = iface - ubnt_discovery_packet = Ether(dst="ff:ff:ff:ff:ff:ff", src=src_mac)/\ - IP(dst="255.255.255.255")/\ - UDP(sport=randint(1024,65535),dport=10001)/\ - Raw(UBNT_REQUEST_PAYLOAD) + conf.iface = args.interface + ubnt_discovery_packet = Ether(dst="ff:ff:ff:ff:ff:ff", src=src_mac)/\ + IP(dst="255.255.255.255")/\ + UDP(sport=randint(1024,65535),dport=10001)/\ + Raw(UBNT_REQUEST_PAYLOAD) - # do active discovery first, after it we do passive discovery + # do active discovery first, after it we do passive discovery - ans, unans = srp(ubnt_discovery_packet, + ans, unans = srp(ubnt_discovery_packet, multi=True, # We want to allow multiple radios to reply to our discovery packet verbose=0, # Suppress scapy output timeout=DISCOVERY_TIMEOUT_ACTIVE) - DeviceList = [] - for snd,rcv in ans: + wrpcap('active.pcap', ans) + + for snd,rcv in ans: + + # Store the data we gathered from the reply packet + device = ubntResponseParse(rcv) + if device != False: + DeviceList.append(device) - # Store the data we gathered from the reply packet - device = ubntResponseParse(rcv) - if device != False: - DeviceList.append(device) + # passive discovery - # passive discovery + packets = sniff(filter='dst port 10001', timeout=DISCOVERY_TIMEOUT_PASSIVE) - ans = sniff(filter='dst port 10001', timeout=DISCOVERY_TIMEOUT_PASSIVE) + wrpcap('passive.pcap', packets) - # Loop over received packets - for rcv in ans: + # Loop over received packets + for rcv in packets: - # Store the data we gathered from the reply packet - device = ubntResponseParse(rcv) - if device != False: - DeviceList.append(device) + # Store the data we gathered from the reply packet + device = ubntResponseParse(rcv) + if device != False: + DeviceList.append(device) return DeviceList if __name__ == '__main__': args = parse_args() sys.stderr.write("\nDiscovery in progress...\n") - DeviceList = ubntDiscovery(args.interface) + DeviceList = ubntDiscovery(args) found_devices = len(DeviceList) if args.output_format == 'text': if not found_devices: From 4c0ee66c9a559c67d54399468b1dac91f1e1b6e9 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sat, 13 Feb 2021 23:45:34 +0100 Subject: [PATCH 09/22] don't alow pcap's --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84650ba --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pcap From 7763c579290c4f4982f24f313b22bc7e179dfbcb Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sat, 13 Feb 2021 23:55:06 +0100 Subject: [PATCH 10/22] Fix bug with pcap files with other packets in it --- ubnt_discovery.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 81eadb5..699a159 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -185,13 +185,15 @@ def ubntDiscovery(args): for packet in packets: - if packet[UDP].dport == 10001 or packet[UDP].sport == 10001: + if UDP in packet: - device = ubntResponseParse(packet) + if packet[UDP].dport == 10001 or packet[UDP].sport == 10001: - print(device) - if device != False: - DeviceList.append(device) + device = ubntResponseParse(packet) + + print(device) + if device != False: + DeviceList.append(device) if args.interface is not None: From 4ea282a649db285d4ab99cf6c8ebf9399d8f23c7 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sat, 13 Feb 2021 23:55:28 +0100 Subject: [PATCH 11/22] Remove dbug print --- ubnt_discovery.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 699a159..41abd20 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -191,7 +191,6 @@ def ubntDiscovery(args): device = ubntResponseParse(packet) - print(device) if device != False: DeviceList.append(device) From dbd1049a80dcd49b87e0ecd05581435e299188b5 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sat, 13 Feb 2021 23:57:50 +0100 Subject: [PATCH 12/22] Decode ADDR_ENTRY --- ubnt_discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 41abd20..3cfa6c2 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -36,6 +36,7 @@ def ip_repr(data): 0x01: ('HWADDR', mac_repr, False), 0x02: ('IPINFO', lambda data: '%s;%s' % (mac_repr(data[0:6]), ip_repr(data[6:10])), True), 0x03: ('FWVERSION', bytes.decode, False), + 0x04: ('ADDR_ENTRY', lambda data: int.from_bytes(data, 'big'), False), 0x0a: ('UPTIME', lambda data: int.from_bytes(data, 'big'), False), 0x0b: ('HOSTNAME', bytes.decode, False), 0x0c: ('PLATFORM', bytes.decode, False), @@ -53,7 +54,6 @@ def ip_repr(data): 0x1c: ('SSHD_PORT', lambda data: int.from_bytes(data, 'big'), False), 0x1e: ('TALK_ANONYMOUS_DEVICE_ID', bytes.decode, False), # These need checking - 0x04: ('ADDR_ENTRY', str, False), 0x05: ('MAC_ENTRY', str, False), 0x06: ('USERNAME', str, False), 0x07: ('SALT', str, False), From c898eddbc9b8f5245d602701acd413ccc8eac927 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 14 Feb 2021 00:01:07 +0100 Subject: [PATCH 13/22] Fix bug that filtering does not work on passive listenening --- ubnt_discovery.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 3cfa6c2..ac7c6bd 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -240,10 +240,18 @@ def ubntDiscovery(args): # Loop over received packets for rcv in packets: - # Store the data we gathered from the reply packet - device = ubntResponseParse(rcv) - if device != False: - DeviceList.append(device) + # for some reason on some devices the filer doe snot work so we do it manually here again + + if UDP in rcv: + + if rcv[UDP].dport == 10001 or rcv[UDP].sport == 10001: + + device = ubntResponseParse(rcv) + + # Store the data we gathered from the packet + + if device != False: + DeviceList.append(device) return DeviceList From da839fda49bc8d0656724b813bd3760dc8c68f16 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 14 Feb 2021 00:17:55 +0100 Subject: [PATCH 14/22] Improve decoders for ip and mac --- ubnt_discovery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index ac7c6bd..ae70d92 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -36,7 +36,8 @@ def ip_repr(data): 0x01: ('HWADDR', mac_repr, False), 0x02: ('IPINFO', lambda data: '%s;%s' % (mac_repr(data[0:6]), ip_repr(data[6:10])), True), 0x03: ('FWVERSION', bytes.decode, False), - 0x04: ('ADDR_ENTRY', lambda data: int.from_bytes(data, 'big'), False), + 0x04: ('ADDR_ENTRY', ip_repr, False), + 0x05: ('MAC_ENTRY', mac_repr, False), 0x0a: ('UPTIME', lambda data: int.from_bytes(data, 'big'), False), 0x0b: ('HOSTNAME', bytes.decode, False), 0x0c: ('PLATFORM', bytes.decode, False), @@ -54,7 +55,6 @@ def ip_repr(data): 0x1c: ('SSHD_PORT', lambda data: int.from_bytes(data, 'big'), False), 0x1e: ('TALK_ANONYMOUS_DEVICE_ID', bytes.decode, False), # These need checking - 0x05: ('MAC_ENTRY', str, False), 0x06: ('USERNAME', str, False), 0x07: ('SALT', str, False), 0x08: ('RND_CHALLENGE', str, False), From fff94ef598536b6e6a68a9e6274c3322bbeeacc6 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 14 Feb 2021 00:29:45 +0100 Subject: [PATCH 15/22] Implement parsing of fields based on version of packet --- ubnt_discovery.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index ae70d92..2df88cb 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -62,11 +62,6 @@ def ip_repr(data): 0x0f: ('MGMT_URL', str, False), 0x11: ('MGMT_LOCATE_SECONDS', str, False), 0x1d: ('PLATFORM_UVP', str, False), -# Same for these, it might be that it is v1 vs v2 - 0x14: ('model', bytes.decode, False), - 0x14: ('DST_MACID', str, False), - 0x18: ('default_config', lambda data: int.from_bytes(data, 'big'), False), - 0x18: ('MGMT_IS_LOCATING', lambda data: int.from_bytes(data, 'big'), False), # These need names 0x0f: ('unknown1 (unifi-os related?)', lambda data: int.from_bytes(data, 'big'), False), 0x21: ('unknown2', str, False), @@ -148,9 +143,11 @@ def ubntResponseParse(rcv): elif payload[0:3] == UBNT_V1_SIGNATURE: # Check for a valid UBNT discovery reply (first 3 bytes of the payload should be \x01\x00\x00) Device = {} # This should be a valid discovery reply packet sent by an Ubiquiti device Device['Signature version'] = '1' # this is not allways correct + fieldparsersPacketSpecific = {**FIELD_PARSERS, **FIELD_PARSERS_V1} elif payload[0:3] == UBNT_V2_SIGNATURE: Device = {} # This should be a valid discovery broadcast packet sent by an Ubiquiti device Device['Signature version'] = '2' + fieldparsersPacketSpecific = {**FIELD_PARSERS, **FIELD_PARSERS_V1} else: return False # Not a valid UBNT discovery reply, skip to next received packet @@ -160,13 +157,13 @@ def ubntResponseParse(rcv): # Take into account the payload length in offset 3 for fieldType, fieldData in iter_fields(payload[4:], payload[3]): - if fieldType not in FIELD_PARSERS: + if fieldType not in fieldparsersPacketSpecific: sys.stderr.write("notice: unknown field type 0x%x: data %s\n" % (fieldType, fieldData)) continue # Parse the field and store in Device - fieldName, fieldParser, isMany = FIELD_PARSERS[fieldType] + fieldName, fieldParser, isMany = fieldparsersPacketSpecific[fieldType] if isMany: if fieldName not in Device: Device[fieldName] = [] Device[fieldName].append(fieldParser(fieldData)) From 3c8eebebd0e8b1e901cb008f4bf45a21bda72e12 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 14 Feb 2021 00:45:06 +0100 Subject: [PATCH 16/22] Add igmp join --- ubnt_discovery.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 2df88cb..64104f4 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -12,6 +12,7 @@ import sys from struct import unpack from scapy.all import * +import scapy.contrib.igmp def mac_repr(data): return ':'.join(('%02x' % b) for b in data) @@ -230,6 +231,10 @@ def ubntDiscovery(args): # passive discovery + # igmp join + + send(IP(dst="233.89.188.1")/scapy.contrib.igmp.IGMP()) + packets = sniff(filter='dst port 10001', timeout=DISCOVERY_TIMEOUT_PASSIVE) wrpcap('passive.pcap', packets) From e164b016d497010649d601dba62c2e71687ea7b6 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 14 Feb 2021 00:48:49 +0100 Subject: [PATCH 17/22] Remove printing from send --- ubnt_discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 64104f4..975623e 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -233,7 +233,7 @@ def ubntDiscovery(args): # igmp join - send(IP(dst="233.89.188.1")/scapy.contrib.igmp.IGMP()) + send(IP(dst="233.89.188.1")/scapy.contrib.igmp.IGMP(),verbose=0) packets = sniff(filter='dst port 10001', timeout=DISCOVERY_TIMEOUT_PASSIVE) From 35a79291f10f3c94ad1402d6c8f4d149abdec259 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 14 Feb 2021 00:55:28 +0100 Subject: [PATCH 18/22] Clean up unused thing --- ubnt_discovery.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 975623e..6bb2027 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -84,26 +84,6 @@ def ip_repr(data): 0x18: ('MGMT_IS_LOCATING', lambda data: int.from_bytes(data, 'big'), False), } -# Basic fields: src MAC and IP of reply message; not parsed -BASIC_FIELDS = { 'mac', 'ip' } - -# String representation of non-basic fields -FIELD_STR = { - 'HWADDR': 'HWADDR', -# 'HWADDR': 'MAC (Serial)', -# 'mac3': 'MAC 3', -# 'IPINFO': 'MAC-IP Pairs', -# 'FWVERSION': 'Firmware', -# 'firmware_short': 'Firmware (short)', -# 'uptime': 'Uptime', -# 'name': 'Hostname', -# 'model_short': 'Model (short)', -# 'essid': 'ESSID', -# 'wlan_mode':'WLAN Mode', -# 'model': 'Model', -# 'default_config': 'Default configuration', -} - # UBNT discovery packet payload and reply signature UBNT_REQUEST_PAYLOAD = b'\x01\x00\x00\x00' UBNT_V1_SIGNATURE = b'\x01\x00\x00' @@ -271,8 +251,6 @@ def ubntDiscovery(args): for Device in DeviceList: print("\n---[ %s ]---" % Device['mac']) for field in Device: - if field in BASIC_FIELDS: continue - print(fmt % (FIELD_STR.get(field, field), - Device[field])) + print(fmt % (field, Device[field])) elif args.output_format == 'json': print(json.dumps(DeviceList, indent=2)) From 7880377188fd34978651fd623f25aefc12350c63 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 14 Feb 2021 18:06:36 +0100 Subject: [PATCH 19/22] Field definitions improvements --- ubnt_discovery.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 6bb2027..9c9d628 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -44,6 +44,7 @@ def ip_repr(data): 0x0c: ('PLATFORM', bytes.decode, False), 0x0d: ('ESSID', bytes.decode, False), 0x0e: ('WMODE', lambda data: UBNT_WIRELESS_MODES.get(data[0], 'unknown'), False), + 0x0f: ('MGMT_URL', lambda data: int.from_bytes(data, 'big'), False), 0x10: ('SYSTEM_ID', lambda data: int.from_bytes(data, 'big'), False), 0x12: ('SEQ', lambda data: int.from_bytes(data, 'big'), False), 0x13: ('SRC_MACID', mac_repr, False), @@ -55,28 +56,32 @@ def ip_repr(data): 0x1b: ('REQUIRED_VERSION', bytes.decode, False), 0x1c: ('SSHD_PORT', lambda data: int.from_bytes(data, 'big'), False), 0x1e: ('TALK_ANONYMOUS_DEVICE_ID', bytes.decode, False), + 0x21: ('HWADDR2', bytes.decode, False), + 0x28: ('acces hub mac', bytes.decode, False), + 0x2b: ('BRANCH?', bytes.decode, False), # These need checking 0x06: ('USERNAME', str, False), 0x07: ('SALT', str, False), 0x08: ('RND_CHALLENGE', str, False), 0x09: ('CHALLENGE_RESPONSE', str, False), - 0x0f: ('MGMT_URL', str, False), 0x11: ('MGMT_LOCATE_SECONDS', str, False), 0x1d: ('PLATFORM_UVP', str, False), # These need names - 0x0f: ('unknown1 (unifi-os related?)', lambda data: int.from_bytes(data, 'big'), False), - 0x21: ('unknown2', str, False), 0x22: ('unknown3', str, False), - 0x24: ('unknown4 TS?', lambda data: int.from_bytes(data, 'big'), False), 0x27: ('unknown5', str, False), - 0x2a: ('unknown6', str, False), - 0x2d: ('unknown7 (led related?)', lambda data: int.from_bytes(data, 'big'), False), - 0x2e: ('unknown8 (led related?)', str, False), + 0x2d: ('unknown7 (led/access related)', lambda data: int.from_bytes(data, 'big'), False), + 0x2e: ('unknown8 (led related)', str, False), +#these need better names + 0x24: ('unknown int?', lambda data: int.from_bytes(data, 'big'), False), + 0x2a: ('unknown user?', str, False), + 0x2c: ('unknown bool', lambda data: int.from_bytes(data, 'big'), False), } +# if unknown7 = 0 then unknown8 is all 0's + FIELD_PARSERS_V1 = { - 0x14: ('model', bytes.decode, False), - 0x18: ('default_config', lambda data: int.from_bytes(data, 'big'), False), + 0x14: ('MODEL', bytes.decode, False), + 0x18: ('MGMT_IS_DEFAULT', lambda data: int.from_bytes(data, 'big'), False), } FIELD_PARSERS_V2 = { @@ -132,7 +137,7 @@ def ubntResponseParse(rcv): else: return False # Not a valid UBNT discovery reply, skip to next received packet - Device['mac'] = rcv[Ether].src.upper() # Read comment above, this time regarding the MAC Address. + Device['pckt_mac'] = rcv[Ether].src.upper() # Read comment above, this time regarding the MAC Address. # Walk the reply payload, staring from offset 04 (just after reply signature and payload size). # Take into account the payload length in offset 3 @@ -249,7 +254,7 @@ def ubntDiscovery(args): print("\nDiscovered %d device(s):" % found_devices) fmt = " %-30s: %s" for Device in DeviceList: - print("\n---[ %s ]---" % Device['mac']) + print("\n---[ %s ]---" % Device['pckt_mac']) for field in Device: print(fmt % (field, Device[field])) elif args.output_format == 'json': From f3ad28996294d08ec1e15a3fa9b182b152600f3c Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 14 Feb 2021 22:02:47 +0100 Subject: [PATCH 20/22] Improve field name --- ubnt_discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 9c9d628..6f1043b 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -56,6 +56,7 @@ def ip_repr(data): 0x1b: ('REQUIRED_VERSION', bytes.decode, False), 0x1c: ('SSHD_PORT', lambda data: int.from_bytes(data, 'big'), False), 0x1e: ('TALK_ANONYMOUS_DEVICE_ID', bytes.decode, False), + 0x20: ('DEVICE_ID', bytes.decode, False), 0x21: ('HWADDR2', bytes.decode, False), 0x28: ('acces hub mac', bytes.decode, False), 0x2b: ('BRANCH?', bytes.decode, False), From 41c9e1326fd624b4f4e5fe8c4578f6b4b6cc1eb7 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 14 Feb 2021 22:43:01 +0100 Subject: [PATCH 21/22] Fix V2 decoders not working --- ubnt_discovery.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 6f1043b..bf2105f 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -70,11 +70,10 @@ def ip_repr(data): # These need names 0x22: ('unknown3', str, False), 0x27: ('unknown5', str, False), - 0x2d: ('unknown7 (led/access related)', lambda data: int.from_bytes(data, 'big'), False), + 0x2d: ('unknown7 (led/access related)', str, False), 0x2e: ('unknown8 (led related)', str, False), #these need better names 0x24: ('unknown int?', lambda data: int.from_bytes(data, 'big'), False), - 0x2a: ('unknown user?', str, False), 0x2c: ('unknown bool', lambda data: int.from_bytes(data, 'big'), False), } @@ -83,11 +82,13 @@ def ip_repr(data): FIELD_PARSERS_V1 = { 0x14: ('MODEL', bytes.decode, False), 0x18: ('MGMT_IS_DEFAULT', lambda data: int.from_bytes(data, 'big'), False), + 0x2a: ('USER?', bytes.decode, False), } FIELD_PARSERS_V2 = { 0x14: ('DST_MACID', str, False), 0x18: ('MGMT_IS_LOCATING', lambda data: int.from_bytes(data, 'big'), False), + 0x2a: ('unknown ?', str, False), } # UBNT discovery packet payload and reply signature @@ -134,7 +135,7 @@ def ubntResponseParse(rcv): elif payload[0:3] == UBNT_V2_SIGNATURE: Device = {} # This should be a valid discovery broadcast packet sent by an Ubiquiti device Device['Signature version'] = '2' - fieldparsersPacketSpecific = {**FIELD_PARSERS, **FIELD_PARSERS_V1} + fieldparsersPacketSpecific = {**FIELD_PARSERS, **FIELD_PARSERS_V2} else: return False # Not a valid UBNT discovery reply, skip to next received packet From 2620f9f59e2eb7a2e8025a5138a4f8fcf2b3a494 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Thu, 13 Jan 2022 16:50:05 +0100 Subject: [PATCH 22/22] Add more filters --- ubnt_discovery.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index bf2105f..e4c7f1d 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -75,6 +75,8 @@ def ip_repr(data): #these need better names 0x24: ('unknown int?', lambda data: int.from_bytes(data, 'big'), False), 0x2c: ('unknown bool', lambda data: int.from_bytes(data, 'big'), False), + 0x2f: ('unknown new1', mac_repr, False), + 0x26: ('unknown new2', str, False), } # if unknown7 = 0 then unknown8 is all 0's @@ -95,9 +97,10 @@ def ip_repr(data): UBNT_REQUEST_PAYLOAD = b'\x01\x00\x00\x00' UBNT_V1_SIGNATURE = b'\x01\x00\x00' UBNT_V2_SIGNATURE = b'\x02\x06\x00' +UBNT_UNKNOW_SIGNATURE = b'\x00\x00\x00\x77' # Discovery timeout. Change this for quicker discovery -DISCOVERY_TIMEOUT_ACTIVE = 2 +DISCOVERY_TIMEOUT_ACTIVE = 5 DISCOVERY_TIMEOUT_PASSIVE = 10 def parse_args(): @@ -136,6 +139,10 @@ def ubntResponseParse(rcv): Device = {} # This should be a valid discovery broadcast packet sent by an Ubiquiti device Device['Signature version'] = '2' fieldparsersPacketSpecific = {**FIELD_PARSERS, **FIELD_PARSERS_V2} + elif payload[0:4] == UBNT_UNKNOW_SIGNATURE: + Device = {} + Device['Signature version'] = 'unknown' + fieldparsersPacketSpecific = {**FIELD_PARSERS, **FIELD_PARSERS_V2} else: return False # Not a valid UBNT discovery reply, skip to next received packet