1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-06 04:31:29 +00:00

New script to extract information from the Ubiquiti Discovery service and assist version detection. Closes #1457

This commit is contained in:
tomsellers
2019-02-09 20:08:32 +00:00
parent 5d5e11d8df
commit 75eed6799c
3 changed files with 379 additions and 0 deletions

View File

@@ -1,5 +1,8 @@
#Nmap Changelog ($Id$); -*-text-*- #Nmap Changelog ($Id$); -*-text-*-
o [NSE][GH#1457] New script, ubiquiti-discovery, which extracts information from
the Ubiquiti Discovery service and assists version detection. [Tom Sellers]
o [GH#1454] New service probes and match lines for v1 and v2 of the Ubiquiti o [GH#1454] New service probes and match lines for v1 and v2 of the Ubiquiti
Discovery protocol. Devices often leave the related service open and it Discovery protocol. Devices often leave the related service open and it
exposes significant amounts of information as well as the risk of being used exposes significant amounts of information as well as the risk of being used

View File

@@ -567,6 +567,7 @@ Entry { filename = "tor-consensus-checker.nse", categories = { "external", "safe
Entry { filename = "traceroute-geolocation.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "traceroute-geolocation.nse", categories = { "discovery", "external", "safe", } }
Entry { filename = "tso-brute.nse", categories = { "intrusive", } } Entry { filename = "tso-brute.nse", categories = { "intrusive", } }
Entry { filename = "tso-enum.nse", categories = { "brute", "intrusive", } } Entry { filename = "tso-enum.nse", categories = { "brute", "intrusive", } }
Entry { filename = "ubiquiti-discovery.nse", categories = { "default", "discovery", "safe", "version", } }
Entry { filename = "unittest.nse", categories = { "safe", } } Entry { filename = "unittest.nse", categories = { "safe", } }
Entry { filename = "unusual-port.nse", categories = { "safe", } } Entry { filename = "unusual-port.nse", categories = { "safe", } }
Entry { filename = "upnp-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "upnp-info.nse", categories = { "default", "discovery", "safe", } }

View File

@@ -0,0 +1,375 @@
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local ipOps = require "ipOps"
local tableaux = require "tableaux"
description = [[
Extracts information from Ubiquiti networking devices.
This script leverages Ubiquiti's Discovery Service which is enabled by default
on many products. It will attempt to leverage version 1 of the protocol first
and, if that fails, attempt version 2.
]]
author = {"Tom Sellers"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"default", "discovery", "version", "safe"}
---
-- @usage
-- nmap -sU -p 10001 --script ubiquiti-discovery.nse <target>
--
---
-- @output
-- PORT STATE SERVICE VERSION
-- 10001/udp open ubiquiti-discovery Ubiquiti Discovery Service (v1 protocol, ER-X software ver. v1.10.7)
-- | ubiquiti-discovery:
-- | protocol: v1
-- | uptime_seconds: 113144
-- | uptime: 1 days 07:25:44
-- | hostname: ubnt-router
-- | product: ER-X
-- | firmware: EdgeRouter.ER-e50.v1.10.7.5127989.181001.1227
-- | version: v1.10.7
-- | interface_to_ip:
-- | 80:2a:a8:ae:f1:63:
-- | 192.168.0.1
-- | 172.25.16.1
-- | 80:2a:a8:ae:f1:5e:
-- | 55.55.55.10
-- | 55.55.55.11
-- | 55.55.55.12
-- | mac_addresses:
-- | 80:2a:a8:ae:f1:63
-- |_ 80:2a:a8:ae:f1:5e
--
-- PORT STATE SERVICE REASON VERSION
-- 10001/udp open ubiquiti-discovery udp-response Ubiquiti Discovery Service (v2 protocol, UCK-v2 software ver. 5.9.29)
-- | ubiquiti-discovery:
-- | protocol: v2
-- | firmware: UCK.mtk7623.v0.12.0.29a26c9.181001.1444
-- | version: 5.9.29
-- | model: UCK-v2
-- | config_status: managed/adopted
-- | interface_to_ip:
-- | 78:8a:20:21:ae:7b:
-- | 192.168.0.30
-- | mac_addresses:
-- |_ 78:8a:20:21:ae:7b
--
--@xmloutput
-- <elem key="protocol">v1</elem>
-- <elem key="uptime_seconds">113144</elem>
-- <elem key="uptime">1 days 07:25:44</elem>
-- <elem key="hostname">ubnt-router</elem>
-- <elem key="product">ER-X</elem>
-- <elem key="firmware">EdgeRouter.ER-e50.v1.10.7.5127989.181001.1227</elem>
-- <elem key="version">v1.10.7</elem>
-- <table key="interface_to_ip">
-- <table key="80:2a:a8:ae:f1:63">
-- <elem>192.168.0.1</elem>
-- <elem>172.25.16.1</elem>
-- </table>
-- <table key="80:2a:a8:ae:f1:5e">
-- <elem>55.55.55.10</elem>
-- <elem>55.55.55.11</elem>
-- <elem>55.55.55.12</elem>
-- </table>
-- </table>
-- <table key="mac_addresses">
-- <elem>80:2a:a8:ae:f1:63</elem>
-- <elem>80:2a:a8:ae:f1:5e</elem>
-- </table>
--
-- <elem key="protocol">v2</elem>
-- <elem key="version">5.9.29</elem>
-- <elem key="model">UCK-v2</elem>
-- <elem key="config_status">managed/adopted</elem>
-- <table key="interface_to_ip">
-- <table key="78:8a:20:21:ae:7b">
-- <elem>192.168.0.30</elem>
-- </table>
-- </table>
-- <table key="mac_addresses">
-- <elem>78:8a:20:21:ae:7b</elem>
-- </table>
--
portrule = shortport.port_or_service(10001, "ubiquiti-discovery", "udp", {"open", "open|filtered"})
local PROBE_V1 = string.pack("BB I2",
0x01, 0x00, -- version, command
0x00, 0x00 -- length
)
local PROBE_V2 = string.pack("BB I2",
0x02, 0x08, -- version, command
0x00, 0x00 -- length
)
---
-- Converts uptime seconds into a human readable string
--
-- E.g. "86518" -> "1 days 00:01:58"
--
-- @param uptime number of seconds of uptime
-- @return formatted uptime string (days, hours, minutes, seconds)
local function uptime_str(uptime)
if not uptime then
return nil
end
local d = uptime // 86400
local h = uptime // 3600 % 24
local m = uptime // 60 % 60
local s = uptime % 60
return string.format("%d days %02d:%02d:%02d", d, h, m, s)
end
---
-- Parses the full payload of a discovery response
--
-- There are different fields for v1 and v2 of the protocol but as far as I can
-- tell they don't conflict so we should be safe parsing them both with the same
-- code as long as we sanity check the version and cmd.
--
-- @param payload containing response
-- @return output_table containing results or nil
local function parse_discovery_response(response)
local info = stdnse.output_table()
local unique_macs = {}
local mac_ip_table = {}
if #response < 4 then
return nil
end
-- Verify header and cmd
if response:byte(1) == 0x01 then
if response:byte(2) ~= 0x00 then
return nil
end
info.protocol = "v1"
elseif response:byte(1) == 0x02 then
-- Known values for cmd are 6,9, and 11
if response:byte(2) ~= 0x06 and response:byte(2) ~= 0x09
and response:byte(2) ~= 0x0b then
return nil
end
info.protocol = "v2"
else
return nil
end
local config_len = string.unpack(">I2", response, 3)
-- Do the lengths check out?
if ( not ( #response == config_len + 4) ) then
return nil
end
-- Response looks legit, start extraction
local config_data = string.sub(response, 5, #response)
local tlv_type, tlv_len, tlv_value, pos
local mac, mac_raw, ip, ip_raw
pos = 1
while pos <= #config_data - 2 do
tlv_type = config_data:byte(pos)
tlv_len = string.unpack(">I2", config_data, pos +1)
pos = pos + 3
-- Sanity check that TLV len isn't larger than the data we have left.
-- Has been observed in the wild against protocols just similar enough to
-- make it here.
if tlv_len > (#config_data - pos + 1) then
return nil
end
tlv_value = config_data:sub(pos, pos + tlv_len - 1)
-- MAC address
if tlv_type == 0x01 then
mac_raw = tlv_value:sub(1, 6)
mac = stdnse.format_mac(mac_raw)
unique_macs[mac] = true
-- MAC and IP address
elseif tlv_type == 0x02 then
mac_raw = tlv_value:sub(1, 6)
mac = stdnse.format_mac(mac_raw)
unique_macs[mac] = true
ip_raw = tlv_value:sub(7, tlv_len)
ip = ipOps.str_to_ip(ip_raw)
if mac_ip_table[mac] == nil then
mac_ip_table[mac] = {}
end
mac_ip_table[mac][ip] = true
elseif tlv_type == 0x03 then
info.firmware = tlv_value
local human_version = tlv_value:match("%.(v%d+%.%d+%.%d+)")
if human_version then
info.version = human_version
end
elseif tlv_type == 0x0a then
if tlv_len == 4 then
local uptime_raw = string.unpack(">I4", tlv_value)
info.uptime_seconds = uptime_raw
info.uptime = uptime_str(uptime_raw)
end
elseif tlv_type == 0x0b then
info.hostname = tlv_value
elseif tlv_type == 0x0c then
info.product = tlv_value
elseif tlv_type == 0x0d then
info.essid = tlv_value
elseif tlv_type == 0x0f then
-- value also includes bit shifted flag for http vs https but we
-- are ignoring it here.
if tlv_len == 4 then
tlv_value = string.unpack(">I4", tlv_value)
info.mgmt_port = tlv_value & 0xffff
end
-- model v1 protocol
elseif tlv_type == 0x14 then
info.model = tlv_value
-- model v2 protocol
elseif tlv_type == 0x15 then
info.model = tlv_value
elseif tlv_type == 0x16 then
info.version = tlv_value
elseif tlv_type == 0x17 then
local is_default
if tlv_len == 4 then
is_default = string.unpack("I4", tlv_value)
elseif tlv_len == 1 then
is_default = string.unpack("I1", tlv_value)
end
if is_default == 1 then
info.config_status = "default/unmanaged"
elseif is_default == 0 then
info.config_status = "managed/adopted"
end
else
-- Other known or observed values
-- Some have been seen in code but not observed to test while others have
-- been observed but we don't know how to decode them.
-- 0x06 - username
-- 0x07 - salt
-- 0x08 - random challenge
-- 0x09 - challenge
-- 0x0e - WMODE - state of config? length 1 value 03 value 02
-- 0x10 - length 2 value e4b2 value e8a5 e815
-- 0x12 - SEQ - lenth 4
-- 0x13 - Source Mac, unused?
-- 0x18 - length 4 and 4 nulls, or length 1 and 0xff
-- 0xff - length 2 value e835
stdnse.debug1("Unknown tag: %s - length: %d value: %s",
stdnse.tohex(tlv_type), tlv_len,
stdnse.tohex(tlv_value))
end
pos = pos + tlv_len
end
if next(mac_ip_table) ~= nil then
info.interface_to_ip = {}
for k, _ in pairs(mac_ip_table) do
info.interface_to_ip[k] = tableaux.keys(mac_ip_table[k])
end
end
if next(unique_macs) ~= nil then
info.mac_addresses = tableaux.keys(unique_macs)
end
return info
end
---
-- Send probe and handle housekeeping
--
-- @param host A host table for the target host
-- @param port A port table for the target port
-- @return (status, result) If status is true, result the target's response to
-- a probe. If status is false, result is an error message.
local function send_probe(host, port, probe)
local socket = nmap.new_socket()
socket:set_timeout(5000)
local try = nmap.new_try(function() socket:close() end)
try( socket:connect(host, port) )
try( socket:send(probe) )
local stat, resp = socket:receive_bytes(4)
socket:close()
return stat, resp
end
function action(host, port)
local status, response = send_probe(host, port, PROBE_V1)
if not status then
status, response = send_probe(host, port, PROBE_V2)
if not status then
return nil
end
end
nmap.set_port_state(host, port, "open")
local result = parse_discovery_response(response)
if not result then
return nil
end
port.version.name = "ubiquiti-discovery"
port.version.product = "Ubiquiti Discovery Service"
local extrainfo = result.protocol .. " protocol"
if result.product then
extrainfo = extrainfo .. ", " .. result.product
elseif result.model then
extrainfo = extrainfo .. ", " .. result.model
end
if result.version then
port.version.extrainfo = extrainfo .. " software ver. " .. result.version
end
port.version.ostype = "Linux"
nmap.set_port_version(host, port, "hardmatched")
return result
end