local datetime = require "datetime"
local stdnse = require "stdnse"
local shortport = require "shortport"
local comm = require "comm"
local string = require "string"
local stringaux = require "stringaux"
local table = require "table"
description = [[
OpenWebNet is a communications protocol developed by Bticino since 2000.
Retrieves device identifying information and number of connected devices.
References:
* https://www.myopen-legrandgroup.com/solution-gallery/openwebnet/
* http://www.pimyhome.org/wiki/index.php/OWN_OpenWebNet_Language_Reference
]]
---
-- @usage
-- nmap --script openwebnet-discovery
--
-- @output
-- | openwebnet-discover:
-- | IP Address: 192.168.200.35
-- | Net Mask: 255.255.255.0
-- | MAC Address: 00:03:50:01:d3:11
-- | Device Type: F453AV
-- | Firmware Version: 3.0.14
-- | Uptime: 12d9h42m1s
-- | Date and Time: 4-07-2017T19:17:27
-- | Kernel Version: 2.3.8
-- | Distribution Version: 3.0.1
-- | Lighting: 115
-- | Automation: 15
-- |_ Burglar Alarm: 12
--
-- @xmloutput
-- 192.168.200.35
-- 255.255.255.0
-- 00:03:50:01:d3:11
-- F453AV
-- 3.0.14
-- 12d9h42m1s
-- 4-07-2017T19:17:27
-- 2.3.8
-- 3.0.1
-- 115
-- 15
-- 12
author = "Rewanth Cool"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
portrule = shortport.port_or_service(20000, "openwebnet")
local device = {
[2] = "MHServer",
[4] = "MH200",
[6] = "F452",
[7] = "F452V",
[11] = "MHServer2",
[12] = "F453AV",
[13] = "H4684",
[15] = "F427 (Gateway Open-KNX)",
[16] = "F453",
[23] = "H4684",
[27] = "L4686SDK",
[44] = "MH200N",
[51] = "F454",
[200] = "F454 (new?)"
}
local who = {
[0] = "Scenarios",
[1] = "Lighting",
[2] = "Automation",
[3] = "Power Management",
[4] = "Heating",
[5] = "Burglar Alarm",
[6] = "Door Entry System",
[7] = "Multimedia",
[9] = "Auxiliary",
[13] = "Device Communication",
[14] = "Light+shutters actuators lock",
[15] = "CEN",
[16] = "Sound System",
[17] = "Scenario Programming",
[18] = "Energy Management",
[24] = "Lighting Management",
[25] = "CEN plus",
[1000] = "Diagnostic",
[1001] = "Automation Diagnostic",
[1004] = "Heating Diagnostic",
[1008] = "Door Entry System Diagnostic",
[1013] = "Device Diagnostic"
}
local device_dimension = {
["Time"] = "0",
["Date"] = "1",
["IP Address"] = "10",
["Net Mask"] = "11",
["MAC Address"] = "12",
["Device Type"] = "15",
["Firmware Version"] = "16",
["Hardware Version"] = "17",
["Uptime"] = "19",
["Micro Version"] = "20",
["Date and Time"] = "22",
["Kernel Version"] = "23",
["Distribution Version"] = "24",
["Gateway IP address"] = "50",
["DNS IP address 1"] = "51",
["DNS IP address 2"] = "52"
}
local ACK = "*#*1##"
local NACK = "*#*0##"
-- Initiates a socket connection
-- Returns the socket and error message
local function get_socket(host, port, request)
local sd, response, early_resp = comm.opencon(host, port, request, {recv_before=true, request_timeout=10000})
if sd == nil then
stdnse.debug("Socket connection error.")
return nil, response
end
if not response then
stdnse.debug("Poor internet connection or no response.")
return nil, response
end
if response == NACK then
stdnse.debug("Received a negative ACK as response.")
return nil, response
end
return sd, nil
end
local function get_response(sd, request)
local res = {}
local status, data
sd:send(request)
repeat
status, data = sd:receive_buf("##", true)
if status == nil then
stdnse.debug("Error: " .. data)
if data == "TIMEOUT" then
-- Avoids false results by capturing NACK after TIMEOUT occurred.
status, data = sd:receive_buf("##", true)
break
else
-- Captures other kind of errors like EOF
sd:close()
return res
end
end
if status and data ~= ACK then
table.insert(res, data)
end
if data == ACK then
break
end
-- If response is NACK, it means the request method is not supported
if data == NACK then
res = nil
break
end
until not status
return res
end
local function format_dimensions(res)
if res["Date and Time"] then
local params = {
"hour", "min", "sec", "msec", "dayOfWeek", "day", "month", "year"
}
local values = {}
for counter, val in ipairs(stringaux.strsplit("%.%s*", res["Date and Time"])) do
values[ params[counter] ] = val
end
res["Date and Time"] = datetime.format_timestamp(values)
end
if res["Device Type"] then
res["Device Type"] = device[ tonumber( res["Device Type"] ) ]
end
if res["MAC Address"] then
res["MAC Address"] = string.gsub(res["MAC Address"], "(%d+)(%.?)", function(num, separator)
if separator == "." then
return string.format("%02x:", num)
else
return string.format("%02x", num)
end
end
)
end
if res["Uptime"] then
local t = {}
local units = {
"d", "h", "m", "s"
}
for counter, v in ipairs(stringaux.strsplit("%.%s*", res["Uptime"])) do
table.insert(t, v .. units[counter])
end
res["Uptime"] = table.concat(t, "")
end
return res
end
action = function(host, port)
local output = stdnse.output_table()
local sd, err = get_socket(host, port, nil)
-- Socket connection creation failed
if sd == nil then
return err
end
-- Fetching list of dimensions of a device
for _, device in ipairs({"IP Address", "Net Mask", "MAC Address", "Device Type", "Firmware Version", "Uptime", "Date and Time", "Kernel Version", "Distribution Version"}) do
local head = "*#13**"
local tail = "##"
stdnse.debug("Fetching " .. device)
local res = get_response(sd, head .. device_dimension[device] .. tail)
-- Extracts substring from the result
-- Ex:
-- Request - *#13**16##
-- Response - *#13**16*3*0*14##
-- Trimmed Output - 3*0*14
if res and next(res) then
local regex = string.gsub(head, "*", "%%*") .. device_dimension[device] .. "%*" .."(.+)" .. tail
local tempRes = string.match(res[1], regex)
if tempRes then
output[device] = string.gsub(tempRes, "*", ".")
end
end
end
-- Format the output based on dimension
output = format_dimensions(output)
-- Fetching list of each device
for i = 1, 6 do
stdnse.debug("Fetching the list of " .. who[i] .. " devices.")
local res = get_response(sd, "*#" .. i .. "*0##")
if res and #res > 0 then
output[who[i]] = #res
end
end
if #output > 0 then
return output
else
return nil
end
end