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