Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NSE for OpenWebNet discovery #915

Closed
wants to merge 46 commits into from
Closed
Changes from 15 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
6aa0984
NSE script for OpenWebNet discovery
rewanthtammana Jun 21, 2017
2a4f8cb
Ignored the timeout errored output for better view of result
rewanthtammana Jun 21, 2017
28d70ba
Fixes Indentation
rewanthtammana Jun 21, 2017
540b75e
Modifies tables initiation
rewanthtammana Jun 22, 2017
c7e24f4
Checks status variable instead of data for status
rewanthtammana Jun 22, 2017
4d0d4a9
Uses stdnse.output_table()
rewanthtammana Jun 22, 2017
5fb09bb
Removes stdnse.sleep
rewanthtammana Jun 22, 2017
6e7276c
Uses only single socket
rewanthtammana Jun 23, 2017
ceb0968
Reduces WHO test values
rewanthtammana Jun 23, 2017
4865a6d
Removes match module
rewanthtammana Jun 23, 2017
27ab35c
Sends Gateway request into generalized format
rewanthtammana Jun 23, 2017
669807d
Adds generalized function for getting sending requests
rewanthtammana Jun 23, 2017
2ed6404
Fetches the list of device dimensions
rewanthtammana Jun 24, 2017
ff5d5ec
Formats the output of device dimensions
rewanthtammana Jun 24, 2017
be446a2
Removes unused functions
rewanthtammana Jun 24, 2017
337eda5
Adds references.
rewanthtammana Jul 1, 2017
aedb765
Adds more WHO values.
rewanthtammana Jul 1, 2017
39e9980
Corrects request message.
rewanthtammana Jul 1, 2017
5de72e6
Removes convoluted match and uses ipairs for iterating device dimensi…
rewanthtammana Jul 1, 2017
796e6dc
Uses single query for Date and Time.
rewanthtammana Jul 1, 2017
f563a23
Formats MAC Address.
rewanthtammana Jul 1, 2017
be53037
Formats Uptime.
rewanthtammana Jul 1, 2017
79386c6
Modifies @output.
rewanthtammana Jul 1, 2017
ee67c24
Follows better naming conventions.
rewanthtammana Jul 1, 2017
d6ee4ce
Minor fixes and updates done.
rewanthtammana Jul 1, 2017
7b123c2
Updates device dimensions list.
rewanthtammana Jul 1, 2017
25456ad
Adds * for references
rewanthtammana Jul 4, 2017
348e234
Formats MAC Address
rewanthtammana Jul 4, 2017
fa69e5a
Optimizes Uptime formatting
rewanthtammana Jul 4, 2017
377da5d
Checks for valid response
rewanthtammana Jul 4, 2017
50af9ca
Ignores scenarios
rewanthtammana Jul 4, 2017
127ea7c
Removes unused operations
rewanthtammana Jul 4, 2017
419df0f
Formats Date and Time
rewanthtammana Jul 4, 2017
46c9aba
Validates nil values
rewanthtammana Jul 4, 2017
0b5cde6
Breaks loop on receiving NACK
rewanthtammana Jul 4, 2017
c573a9f
Removes ACK initial request
rewanthtammana Jul 4, 2017
c8fac63
Updates output
rewanthtammana Jul 4, 2017
b1a2a9a
Returns actual error
rewanthtammana Jul 4, 2017
c4fa595
Formats MAC address
rewanthtammana Jul 8, 2017
75122fe
Formats indexing.
rewanthtammana Jul 8, 2017
5362ab4
Handles false positives.
rewanthtammana Jul 8, 2017
ee6cb72
Handles EOF, TIMEOUT, ...
rewanthtammana Jul 12, 2017
00be9dd
Adds request_timeout
rewanthtammana Jul 12, 2017
63f9bea
Replaces throwaway with variable name
rewanthtammana Jul 12, 2017
aba927e
Shows format of xmloutput
rewanthtammana Jul 12, 2017
c11a72e
Removes tostring function to throw actual error, if any.
rewanthtammana Jul 17, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
200 changes: 200 additions & 0 deletions scripts/openwebnet-discovery.nse
@@ -0,0 +1,200 @@
local stdnse = require "stdnse"
local shortport = require "shortport"
local comm = require "comm"
local string = require "string"

description = [[
OpenWebNet is a communications protocol developed by Bticino since 2000.
Retrieves the Gateway and device type. Retrieves the count and addresses

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't retrieve the Gateway or "addresses of lights". Generalize this description to only say we retrieve device identifying information and number of connected devices.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as 337eda5.

of lights, multimedia and many other services running on server/servers.
]]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add some References here: the piMyHome link and https://www.myopen-legrandgroup.com/solution-gallery/openwebnet/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as 337eda5.


---
-- @usage
-- nmap --script openwebnet-discovery
--
-- @output
-- | openwebnet-discover:
-- | MAC address: 0-3-80-1-211-17
-- | Kernel Version: 2.3.8
-- | Net mask: 255.255.255.0
-- | IP address: 192.168.200.35
-- | Time: 19:58:33:001
-- | Date: 24.06.2017
-- | Device Type: F453AV
-- | Distribution Version: 3.0.1
-- | Firmware version: 3.0.14
-- | Uptime: 5.3.28.38

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uptime should be reported in the same format that stdnse.format_difftime uses. You don't have to use that function, though, since it's already reported broken into segments like this. Your example output should look like "5d3h28m38s". Note that the piMyHome page does not show seconds, so some devices may not report that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as be53037.

-- | Scenarios: 0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still prefer to not see lines with 0 count.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as 0b5cde6

-- | Lighting: 115
-- | Automation: 13
-- | Heating: 0
-- | Burglar Alarm: 12
-- |_ Door Entry System: 0

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"
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice to still have the full list of WHO values, even if we only query 0 through 6.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as aedb765.


local device_dimensions = {
["Time"] = "*#13**0##",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be cleaner to have this table simply be the dimension numbers, and use a function to add the "*#13**" and "##"

["Date"] = "*#13**1##",
["IP address"] = "*#13**10##",
["Net mask"] = "*#13**11##",
["MAC address"] = "*#13**12##",
["Device Type"] = "*#13**15##",
["Firmware version"] = "*#13**16##",
["Uptime"] = "*#13**19##",
["Kernel Version"] = "*#13**23##",
["Distribution Version"] = "*#13**24##"
}

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we expect to get a banner in response to connection, we need to set the recv_before option to opencon. This will connect, wait for a banner, and return that as early_resp. We should NOT be sending an ACK here, since it only responds with a NACK because ACK is not a client message. So the intended flow is:

  1. C > S - connect
  2. C < S - ACK
  3. C > S - request
  4. C < S - response

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as c573a9f


if sd == nil then
return nil, "Socket connection error."

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return or print (debug) the actual error, which is in the response variable now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as b1a2a9a

end

if not response then

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the recv_before and not sending any data at this point, the response ought to be nil. We don't have to check for it, though, because we only need to check sd and early_resp.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as c573a9f

return nil, "Poor internet connection or no response."
end

if response == NACK then
return nil, "Received a negative ACK as 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 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to break the loop in this case. This is a major source of slowness in the script: we try to enumerate something (Scenarios, for example) that it doesn't support listing; we get a NACK; then we end up waiting for a whole read timeout before continuing with the next request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commited as 0b5cde6

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran into another problem. A host closes the connection after sending the banner, but we keep trying to send on the closed socket. The symptom is status is nil and data is "EOF". The trouble for the caller is that nil can mean "not supported" or "socket was closed." An easy fix would be:

  • nil means socket problems. Return the error as a second value
  • empty table means either no results or NACK
  • populated table means we got results.

In my test case, the server did not respond to any of our queries, but always closed the socket. This means we don't have to try to reopen and reconnect.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yet another tricky one: timeouts. If the timeout expires, we may end up reading a response to one probe as the answer to the next probe. There are 2 approaches we must take to avoid this:

  1. Increase the request_timeout when connecting with comm.opencon, but do not increase the timeout or connect_timeout. Requests may take longer because the gateway has to contact the slave devices. Probably use 10 seconds (my test system took 8.2 seconds, greater than the connect_timeout (1.4s) plus the request_timeout (6s) by default).
  2. To the extent possible, we should try to check the response and synchronize. Here was the output of my mixup:
    NSE: [openwebnet-discovery 78.216.11.170:20000] Fetching the list of Heating devices.
    NSE: TCP 192.168.1.23:46051 > 78.216.11.170:20000 | *#4*0##
    NSOCK INFO [11.4180s] nsock_write(): Write request for 7 bytes to IOD #1 EID 451 [78.216.11.170:20000]
    NSOCK INFO [11.4180s] nsock_trace_handler_callback(): Callback: WRITE SUCCESS for EID 451 [78.216.11.170:20000]
    NSE: TCP 192.168.1.23:46051 > 78.216.11.170:20000 | SEND
    NSOCK INFO [11.4180s] nsock_read(): Read request from IOD #1 [78.216.11.170:20000] (timeout: 7434ms) EID 458
    NSOCK INFO [18.8520s] nsock_trace_handler_callback(): Callback: READ TIMEOUT for EID 458 [78.216.11.170:20000]
    NSE: [openwebnet-discovery 78.216.11.170:20000] Fetching the list of Burglar Alarm devices.
    NSE: TCP 192.168.1.23:46051 > 78.216.11.170:20000 | *#5*0##
    NSOCK INFO [18.8530s] nsock_write(): Write request for 7 bytes to IOD #1 EID 467 [78.216.11.170:20000]
    NSOCK INFO [18.8530s] nsock_trace_handler_callback(): Callback: WRITE SUCCESS for EID 467 [78.216.11.170:20000]
    NSE: TCP 192.168.1.23:46051 > 78.216.11.170:20000 | SEND
    NSOCK INFO [18.8530s] nsock_read(): Read request from IOD #1 [78.216.11.170:20000] (timeout: 7434ms) EID 474
    NSOCK INFO [19.6130s] nsock_trace_handler_callback(): Callback: READ SUCCESS for EID 474 [78.216.11.170:20000] (6 bytes): *#*0##
    NSE: TCP 192.168.1.23:46051 < 78.216.11.170:20000 | *#*0##
    NSE: [openwebnet-discovery 78.216.11.170:20000] Fetching the list of Door Entry System devices.
    NSE: TCP 192.168.1.23:46051 > 78.216.11.170:20000 | *#6*0##
    NSOCK INFO [19.6130s] nsock_write(): Write request for 7 bytes to IOD #1 EID 483 [78.216.11.170:20000]
    NSOCK INFO [19.6130s] nsock_trace_handler_callback(): Callback: WRITE SUCCESS for EID 483 [78.216.11.170:20000]
    NSE: TCP 192.168.1.23:46051 > 78.216.11.170:20000 | SEND
    NSOCK INFO [19.6140s] nsock_read(): Read request from IOD #1 [78.216.11.170:20000] (timeout: 7434ms) EID 490
    NSOCK INFO [23.1590s] nsock_trace_handler_callback(): Callback: READ SUCCESS for EID 490 [78.216.11.170:20000] (7 bytes): *5*0*##
    NSE: TCP 192.168.1.23:46051 < 78.216.11.170:20000 | *5*0*##
    
    So Heating (4) timed out, but the server sent a NACK which we interpreted as the answer for Burglar Alarm (5). Then we sent Door Entry (6) and got a response for 5. We should detect that the response doesn't match the request. Maybe extract the WHO value from the response and use that instead of just assuming it matches (only works for non-NACK responses). That would at least avoid mixing up answers, even if it means we accidentally drop some. Better to not report something than to report it incorrectly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I handled the false positives by capturing NACK when we get a nil as status response. I think this must work fine.
Committed as 5362ab4.
Your views?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 00be9dd adds request_timeout.

res = {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning nil is easier to check for than empty table. It's OK if WHO categories that have 0 devices are not shown in the script output.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think showing 0 devices will be more helpful to the user during reconnaissance.

end
until not status

return res
end

local function format_dimensions(res)

if res["Time"] then

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For "Time" and "Date", it would be preferable to send the "Date and Time" (22) query, since it saves a query. Then extract the year, month, day, hour, minute, second and pass them in a table to stdnse.format_timestamp. The table format is described in the Lua manual for os.time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as 796e6dc.

res["Time"] = string.gsub(res["Time"], "%.", ":")
end

if res["Date"] then
res["Date"] = string.match(res["Date"],"%.(%d.+)$")
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"], "%.", "-")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want the MAC address as colon-separated hex. Here's a list of functions that may help:

  • stdnse.tohex
  • stdnse.format_mac
  • string.format("%x")
  • table.concat
  • string.gsub (with function instead of replacement string)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as f563a23.

end

return res

end

action = function(host, port)

local output = stdnse.output_table()

local sd, err = get_socket(host, port, ACK)

-- Socket connection creation failed
if sd == nil then
return err
end

-- Fetching list of dimensions of a device
for _, v in pairs(device_dimensions) do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pairs does not produce a predictable ordering, but we need output to be predictable. I suggest completing the device_dimensions table to include all values, even ones we do not query. Then iterate over a separate table of dimension names that we do want.

Only use _ as a loop variable when you intend to not use it elsewhere. It's the "throwaway" variable. If you intend on using it, use a descriptive name. Here's an example of what I am talking about. _ is used for the index, which we don't care about; we only want the names in order.

for _, label in ipairs({"Device Type", "Date and Time", "Uptime", "Firmware version", ...}) do

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as 5de72e6.


stdnse.debug("Fetching " .. _)

local res = get_response(sd, v)

-- Extracts substring from the result
-- Ex:
-- Request - *#13**16##
-- Response - *#13**16*3*0*14##
-- Trimmed Output - 3*0*14##

output[_] = string.gsub(
string.sub(
string.gsub(
res[1], string.gsub(
string.sub(v,1,-3) .. "*","*","%%*"
), ""), 1, -3
), "*", ".")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really convoluted. Try using string.match with captures instead to extract the WHO, DIMENSION, and VALUE (including * characters) from the response, then validate the WHO and DIMENSION and transform the value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as 5de72e6.


end

-- Format the output based on dimension
output = format_dimensions(output)

-- Fetching list of each device
for _, v in pairs(who) do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should do this for every possible WHO value. Some of them may have other functions than "get the status of all X" if you call them with a WHAT of 0. For example, Device Communication (13) responds with a NACK. Instead, keep a list of ones that do respond properly like this (1 through 25, maybe, although I don't know what CEN means) and check those this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as ceb0968.


stdnse.debug("Fetching the list of " .. v .. " devices.")

local res = get_response(sd, "*##*#" .. _ .. "*0##")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't send the "*##" empty/invalid command first.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed as 39e9980.

output[v] = #res

end

return output
end