summaryrefslogtreecommitdiffstats
path: root/scripts/coap-resources.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/coap-resources.nse')
-rw-r--r--scripts/coap-resources.nse317
1 files changed, 317 insertions, 0 deletions
diff --git a/scripts/coap-resources.nse b/scripts/coap-resources.nse
new file mode 100644
index 0000000..9009def
--- /dev/null
+++ b/scripts/coap-resources.nse
@@ -0,0 +1,317 @@
+local coap = require "coap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Dumps list of available resources from CoAP endpoints.
+
+This script establishes a connection to a CoAP endpoint and performs a
+GET request on a resource. The default resource for our request is
+<code>/.well-known/core</core>, which should contain a list of
+resources provided by the endpoint.
+
+For additional information:
+* https://en.wikipedia.org/wiki/Constrained_Application_Protocol
+* https://tools.ietf.org/html/rfc7252
+* https://tools.ietf.org/html/rfc6690
+]]
+
+---
+-- @usage nmap -p U:5683 -sU --script coap-resources <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 5683/udp open coap udp-response ttl 36
+-- | coap-resources:
+-- | /large:
+-- | rt: block
+-- | sz: 1280
+-- | title: Large resource
+-- | /large-update:
+-- | ct: 0
+-- | rt: block
+-- | sz: 55
+-- | title: Large resource that can be updated using PUT method
+-- | /link1:
+-- | if: If1
+-- | rt: Type1 Type2
+-- |_ title: Link test resource
+--
+-- @args coap-resources.uri URI to request via the GET method,
+-- <code>/.well-known/core</code> by default.
+--
+-- @xmloutput
+-- <table key="/">
+-- <elem key="ct">0</elem>
+-- <elem key="title">General Info</elem>
+-- </table>
+-- <table key="/ft">
+-- <elem key="ct">0</elem>
+-- <elem key="title">Faults Reporting</elem>
+-- </table>
+-- <table key="/mn">
+-- <elem key="ct">0</elem>
+-- <elem key="title">Monitor Reporting</elem>
+-- </table>
+-- <table key="/st">
+-- <elem key="ct">0</elem>
+-- <elem key="title">Status Reporting</elem>
+-- </table>
+-- <table key="/time">
+-- <elem key="ct">0</elem>
+-- <elem key="obs,&lt;/devices/block&gt;;title">Devices Block</elem>
+-- <elem key="title">Internal Clock</elem>
+-- </table>
+-- <table key="/wn">
+-- <elem key="ct">0</elem>
+-- <elem key="title">Warnings Reporting</elem>
+-- </table>
+
+author = "Mak Kolybabi <mak@kolybabi.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+-- TODO: Add 5684 "coaps" if DTLS support is added
+portrule = shortport.port_or_service(5683, "coap", "udp")
+
+format_payload = function(payload)
+ -- Leave strings alone.
+ if type(payload) == "string" then
+ return payload
+ end
+
+ local tbl = stdnse.output_table()
+
+ -- We want to go through all of the links in alphabetical order.
+ table.sort(payload, function(a,b) return a.name < b.name end)
+
+ for _, link in ipairs(payload) do
+ -- We want to go through all of the parameters in alphabetical
+ -- order.
+ table.sort(link.parameters, function(a,b) return a.name < b.name end)
+
+ local row = stdnse.output_table()
+ for _, param in ipairs(link.parameters) do
+ row[param.name] = param.value
+ end
+
+ tbl[link.name] = row
+ end
+
+ return tbl
+end
+
+get_blocks = function(helper, options, b2opt, payload, max_reqs)
+ -- Initialize the block table to store all of our received blocks.
+ local blocks = {}
+ blocks[b2opt.number] = payload
+
+ -- If we don't know the number of the last block, we'll need to use
+ -- the largest number we've seen as the maxumum.
+ local max = b2opt.number
+
+ -- If the first block we received happens to be the last one, either
+ -- by being a one-block sequence or by the endpoint sending us the
+ -- last block in the sequence first, we want to record the final
+ -- number in the sequence.
+ local last = nil
+ if b2opt.more == false then
+ last = b2opt.number
+ max = b2opt.number
+ end
+
+ -- We'll continue to request blocks that are the same size as the
+ -- original block, since the endpoint likely prefers that.
+ local length = b2opt.length
+
+ -- We want to track the number of requests we make so that a
+ -- malicious endpoint can't keep us on the hook forever.
+ for req = 1, max_reqs do
+ -- Determine if there are any blocks in the sequence that we have
+ -- not yet received.
+ local top = max + 1
+ if last then
+ top = last
+ end
+
+ local num = top
+ for i = 0, top do
+ if not blocks[i] then
+ num = i
+ break
+ end
+ end
+
+ -- If the block we think we're missing is at the end of the
+ -- sequence, we've got them all.
+ if last and num >= last then
+ stdnse.debug3("All %d blocks have been retrieved.", last)
+ break
+ end
+
+ -- Create the request.
+ local opts = {
+ ["code"] = "get",
+ ["type"] = "confirmable",
+ ["options"] = {
+ {["name"] = "block2", ["value"] = {
+ ["number"] = num,
+ ["more"] = false,
+ ["length"] = length
+ }}
+ }
+ }
+
+ local components = stringaux.strsplit("/", options.uri)
+ for _, component in ipairs(components) do
+ if component ~= "" then
+ table.insert(opts.options, {["name"] = "uri_path", ["value"] = component})
+ end
+ end
+
+ -- Send the request and receive the response.
+ stdnse.debug3("Requesting block %d of size %d.", num, length)
+ local status, response = helper:request(opts)
+ if not status then
+ return false, response
+ end
+
+ if not response.payload then
+ return false, "Response did not contain a payload."
+ end
+
+ -- Check for the presence of the block2 option, and if it's
+ -- missing then we're going to stop.
+ b2opt = coap.COAP.header.find_option(response, "block2")
+ if not b2opt then
+ stdnse.debug1("Stopped requesting more blocks, response found without block2 option.")
+ break
+ end
+
+ stdnse.debug3("Received block %d of size %d.", b2opt.number, b2opt.length)
+ blocks[b2opt.number] = response.payload
+
+ if b2opt.more == false then
+ stdnse.debug3("Block %d indicates it is the end of the sequence.", b2opt.number, b2opt.length)
+ last = b2opt.number
+ max = b2opt.number
+ elseif b2opt.number > max then
+ max = b2opt.number
+ end
+ end
+
+ -- Reassemble payload, handling potentially missing blocks.
+ local result = ""
+ for i = 1, max do
+ if not blocks[i] then
+ stdnse.debug3("Block %d is missing, replacing with dummy data.", i)
+ result = result .. ("<! missing block %d!>"):format(i)
+ else
+ result = result .. blocks[i]
+ end
+ end
+
+ return true, result
+end
+
+local function parse_args ()
+ local args = {}
+
+ local uri = stdnse.get_script_args(SCRIPT_NAME .. '.uri')
+ if not uri then
+ uri = "/.well-known/core"
+ end
+ args.uri = uri
+
+ return true, args
+end
+
+action = function(host, port)
+ local output = stdnse.output_table()
+
+ -- Parse and sanity check the command line arguments.
+ local status, options = parse_args()
+ if not status then
+ output.ERROR = options
+ return output, output.ERROR
+ end
+
+ -- Create an instance of the CoAP library's client object.
+ local helper = coap.Helper:new(host, port)
+
+ -- Connect to the CoAP endpoint.
+ local status, response = helper:connect({["uri"] = options.uri})
+ if not status then
+ -- Erros at this stage indicate we're probably not talking to a CoAP server,
+ -- so we exit silently.
+ return nil
+ end
+
+ -- Check that the response is a 2.05, otherwise we don't know how to
+ -- continue.
+ if response.code ~= "content" then
+ -- If the port runs an echo service, we'll see an unexpected 'get' code.
+ if response.code == "get" then
+ -- Exit silently, this has all been a mistake.
+ return nil
+ end
+
+ -- If the requested resource wasn't found, that's okay.
+ if response.code == "not_found" then
+ stdnse.debug1("The target reports that the resource '%s' was not found.", options.uri)
+ return nil
+ end
+
+ -- Otherwise, we assume that we're getting a legitimate CoAP response.
+ output.ERROR = ("Server responded with '%s' code where 'content' was expected."):format(response.code)
+ return output, output.ERROR
+ end
+
+ local result = response.payload
+ if not result then
+ output.ERROR = "Payload for initial response was not part of the packet."
+ return output, output.ERROR
+ end
+
+ -- Check for the presence of the block2 option, which indicates that
+ -- we'll need to perform more requests.
+ local b2opt = coap.COAP.header.find_option(response, "block2")
+ if b2opt then
+ -- Since the block2 option was used, the payload should be an unparsed string.
+ assert(type(result) == "string")
+
+ local status, payload = get_blocks(helper, options, b2opt, result, 64)
+ if not status then
+ output.ERROR = result
+ return output, output.ERROR
+ end
+ result = result .. payload
+
+ -- Parse the payload.
+ local status, parsed = coap.COAP.payload.parse(response, result)
+ if not status then
+ stdnse.debug1("Failed to parse payload: %s", parsed)
+ stdnse.debug1("Falling back to returning raw payload as last resort.")
+ output["Raw CoAP response"] = result
+ return output, stdnse.format_output(true, output)
+ end
+
+ result = parsed
+ end
+
+ -- Regardless of whether the block2 option was used, we should now have a
+ -- parsed payload in some format or another. For now, they should all be
+ -- strings or tables.
+ assert(type(result) == "string" or type(result) == "table")
+
+ -- If the payload has been parsed, and we requested the default
+ -- resource, then we know how to format it nicely.
+ local formatted = result
+ if true then
+ formatted = format_payload(result)
+ end
+
+ return formatted
+end