Lucene search

K
nmapTom SellersNMAP:UBIQUITI-DISCOVERY.NSE
HistoryFeb 09, 2019 - 8:08 p.m.

ubiquiti-discovery NSE Script

2019-02-0920:08:32
Tom Sellers
nmap.org
705

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.973 High

EPSS

Percentile

99.8%

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.

Example Usage

nmap -sU -p 10001 --script ubiquiti-discovery.nse <target>

Script 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

Requires


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

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.973 High

EPSS

Percentile

99.8%

Related for NMAP:UBIQUITI-DISCOVERY.NSE