diff --git a/nselib/slaxml.lua b/nselib/slaxml.lua new file mode 100644 index 000000000..10f9e3eb1 --- /dev/null +++ b/nselib/slaxml.lua @@ -0,0 +1,387 @@ +--- +-- This is the NSE implementation of SLAXML. +-- SLAXML is a pure-Lua SAX-like streaming XML parser. It is more robust +-- than many (simpler) pattern-based parsers that exist, properly supporting +-- code like , CDATA nodes, comments, +-- namespaces, and processing instructions. +-- It is currently not a truly valid XML parser, however, as it allows certain XML that is +-- syntactically-invalid (not well-formed) to be parsed without reporting an error. +-- The streaming parser does a simple pass through the input and reports what it sees along the way. +-- You can optionally ignore white-space only text nodes using the stripWhitespace option. +-- The library contains the parser class and the parseDOM function. +-- Basic Usage of the library: +-- +-- local parser = parser:new() +-- parser:parseSAX(xmlbody, {stripWhitespace=true}) +-- +-- To specify custom call backs use : +-- +-- local call_backs = { +-- startElement = function(name,nsURI,nsPrefix) end, -- When "" or or "/>" is seen +-- text = function(text) end, -- text and CDATA nodes +-- comment = function(content) end, -- comments +-- pi = function(target,content) end, -- processing instructions e.g. "" +-- } +-- local parser = parser:new(call_backs) +-- parser:parseSAX(xmlbody) +-- +-- The code also contains the parseDOM function. +-- To get the dom table use the parseDOM method as follows. +-- +-- parseDOM(xmlbody, options) +-- +-- @author "Gavin Kistner, Gyanendra Mishra" + +--[=====================================================================[ +v0.7 Copyright © 2013-2014 Gavin Kistner ; MIT Licensed +See http://github.com/Phrogz/SLAXML for details. +--]=====================================================================] + +local string = require "string" +local stdnse = require "stdnse" +local table = require "table" +local unicode = require "unicode" +_ENV = stdnse.module("slaxml", stdnse.seeall) + + + + +-- A table containing the default call backs to be used +-- This really floods the script output, you will mostly be +-- using custom call backs. +-- Set the debugging level required for the default call backs. Defaults to 3. +local debugging_level = tonumber(stdnse.get_script_args('slaxml.debug')) or 3 +local DEFAULT_CALLBACKS = { + --- A call back for processing instructions. + -- To use define pi = function(, ) end in parser._call table. + -- Executes whenever a comment is found. + -- @param target the PI target + -- @param content any value not containing the sequence '?>' + pi = function(target,content) + stdnse.debug(debugging_level, string.format("",target,content)) + end, + --- A call back for comments. + -- To use define comment = function() end in parser._call table. + -- Executes whenever a comment is encountered. + -- @param content The comment body itself. + comment = function(content) + stdnse.debug(debugging_level, debugging_level, string.format("",content)) + end, + --- A call back for the start of elements. + -- To use define startElement = function(, , ) end in parser._call table. + -- Executes whenever an element starts. + -- @param name The name of the element. + -- @param nsURI The name space URI. + -- @param nsPrefix The name space prefix. + startElement = function(name,nsURI,nsPrefix) + local output = "<" + if nsPrefix then output = output .. nsPrefix .. ":" end + output = output .. name + if nsURI then output = output .. " (ns='" .. nsURI .. "')" end + output = output .. ">" + stdnse.debug(debugging_level, output) + end, + --- A call back for attributes. + -- To use define attribute = function(, , , ) end in parser._call table. + -- Executes whenever an attribute is found. + -- @param name The name of the attribute. + -- @param value The value of the attribute. + -- @param nsURI The name space URI. + -- @param nsPrefix The name space prefix. + attribute = function(name,value,nsURI,nsPrefix) + local output = ' ' + if nsPrefix then output = output .. nsPrefix .. ":" end + output = output .. name .. '=' .. string.format('%q',value) + if nsURI then output = output .. (" (ns='" .. nsURI .. "')") end + stdnse.debug(debugging_level, output) + end, + --- A call back for text content. + -- To use define text = function() end in parser._call table. + -- Executes whenever pure text is found. + -- @param text The actual text. + text = function(text) + stdnse.debug(debugging_level, string.format(" text: %q",text)) + end, + --- A call back for the end of elements. + -- To use define closeElement = function(, , ) end in parser._call table. + -- Executes whenever an element closes. + -- @param name The name of the element. + -- @param nsURI The name space URI. + -- @param nsPrefix The name space prefix. + closeElement = function(name,nsURI,nsPrefix) + stdnse.debug(debugging_level, string.format("",name)) + end, + } + +parser = { + + new = function(self, callbacks) + local o = { + _call = callbacks or DEFAULT_CALLBACKS + } + setmetatable(o, self) + self.__index = self + return o + end, + --- Parses the xml in sax like manner. + -- @param xml The xml body to be parsed. + -- @param options Options if any specified. + parseSAX = function(self, xml, options) + if not options then options = { stripWhitespace=false } end + + -- Cache references for maximum speed + local find, sub, gsub, char, push, pop, concat = string.find, string.sub, string.gsub, string.char, table.insert, table.remove, table.concat + local first, last, match1, match2, match3, pos2, nsURI + local unpack = unpack or table.unpack + local pos = 1 + local state = "text" + local textStart = 1 + local currentElement={} + local currentAttributes={} + local currentAttributeCt -- manually track length since the table is re-used + local nsStack = {} + local anyElement = false + + + local entityMap = { ["lt"]="<", ["gt"]=">", ["amp"]="&", ["quot"]='"', ["apos"]="'" } + local entitySwap = function(orig,n,s) return entityMap[s] or n=="#" and utf8_enc(tonumber('0'..s)) or orig end + local function unescape(str) return gsub( str, '(&(#?)([%d%a]+);)', entitySwap ) end + + local function finishText() + if first>textStart and self._call.text then + local text = sub(xml,textStart,first-1) + if options.stripWhitespace then + text = gsub(text,'^%s+','') + text = gsub(text,'%s+$','') + if #text==0 then text=nil end + end + if text then self._call.text(unescape(text)) end + end + end + + local function findPI() + first, last, match1, match2 = find( xml, '^<%?([:%a_][:%w_.-]*) ?(.-)%?>', pos ) + if first then + finishText() + if self._call.pi then self._call.pi(match1,match2) end + pos = last+1 + textStart = pos + return true + end + end + + local function findComment() + first, last, match1 = find( xml, '^', pos ) + if first then + finishText() + if self._call.comment then self._call.comment(match1) end + pos = last+1 + textStart = pos + return true + end + end + + local function nsForPrefix(prefix) + if prefix=='xml' then return 'http://www.w3.org/XML/1998/namespace' end -- http://www.w3.org/TR/xml-names/#ns-decl + for i=#nsStack,1,-1 do if nsStack[i][prefix] then return nsStack[i][prefix] end end + stdnse.debug1(("Cannot find namespace for prefix %s"):format(prefix)) + return + end + + local function startElement() + anyElement = true + first, last, match1 = find( xml, '^<([%a_][%w_.-]*)', pos ) + if first then + currentElement[2] = nil -- reset the nsURI, since this table is re-used + currentElement[3] = nil -- reset the nsPrefix, since this table is re-used + finishText() + pos = last+1 + first,last,match2 = find(xml, '^:([%a_][%w_.-]*)', pos ) + if first then + currentElement[1] = match2 + currentElement[3] = match1 -- Save the prefix for later resolution + match1 = match2 + pos = last+1 + else + currentElement[1] = match1 + for i=#nsStack,1,-1 do if nsStack[i]['!'] then currentElement[2] = nsStack[i]['!']; break end end + end + currentAttributeCt = 0 + push(nsStack,{}) + return true + end + end + + local function findAttribute() + first, last, match1 = find( xml, '^%s+([:%a_][:%w_.-]*)%s*=%s*', pos ) + if first then + pos2 = last+1 + first, last, match2 = find( xml, '^"([^<"]*)"', pos2 ) -- FIXME: disallow non-entity ampersands + if first then + pos = last+1 + match2 = unescape(match2) + else + first, last, match2 = find( xml, "^'([^<']*)'", pos2 ) -- FIXME: disallow non-entity ampersands + if first then + pos = last+1 + match2 = unescape(match2) + end + end + end + if match1 and match2 then + local currentAttribute = {match1,match2} + local prefix,name = string.match(match1,'^([^:]+):([^:]+)$') + if prefix then + if prefix=='xmlns' then + nsStack[#nsStack][name] = match2 + else + currentAttribute[1] = name + currentAttribute[4] = prefix + end + else + if match1=='xmlns' then + nsStack[#nsStack]['!'] = match2 + currentElement[2] = match2 + end + end + currentAttributeCt = currentAttributeCt + 1 + currentAttributes[currentAttributeCt] = currentAttribute + return true + end + end + + local function findCDATA() + first, last, match1 = find( xml, '^', pos ) + if first then + finishText() + if self._call.text then self._call.text(match1) end + pos = last+1 + textStart = pos + return true + end + end + + local function closeElement() + first, last, match1 = find( xml, '^%s*(/?)>', pos ) + if first then + state = "text" + pos = last+1 + textStart = pos + + -- Resolve namespace prefixes AFTER all new/redefined prefixes have been parsed + if currentElement[3] then currentElement[2] = nsForPrefix(currentElement[3]) end + if self._call.startElement then self._call.startElement(unpack(currentElement)) end + if self._call.attribute then + for i=1,currentAttributeCt do + if currentAttributes[i][4] then currentAttributes[i][3] = nsForPrefix(currentAttributes[i][4]) end + self._call.attribute(unpack(currentAttributes[i])) + end + end + + if match1=="/" then + pop(nsStack) + if self._call.closeElement then self._call.closeElement(unpack(currentElement)) end + end + return true + end + end + + local function findElementClose() + first, last, match1, match2 = find( xml, '^', pos ) + if first then + nsURI = nil + for i=#nsStack,1,-1 do if nsStack[i]['!'] then nsURI = nsStack[i]['!']; break end end + else + first, last, match2, match1 = find( xml, '^', pos ) + if first then nsURI = nsForPrefix(match2) end + end + if first then + finishText() + if self._call.closeElement then self._call.closeElement(match1,nsURI) end + pos = last+1 + textStart = pos + pop(nsStack) + return true + end + end + + while pos<#xml do + if state=="text" then + if not (findPI() or findComment() or findCDATA() or findElementClose()) then + if startElement() then + state = "attributes" + else + first, last = find( xml, '^[^<]+', pos ) + pos = (first and last or pos) + 1 + end + end + elseif state=="attributes" then + if not findAttribute() then + if not closeElement() then + stdnse.debug1("Was in an element and couldn't find attributes or the close.") + return + end + end + end + end + + if not anyElement then stdnse.debug1("Parsing did not discover any elements") end + if #nsStack > 0 then stdnse.debug1("Parsing ended with unclosed elements") end + end, + +} + +--- Parses xml and spits out dom +-- @param xml, the xml body to be parsed. +-- @param options if any to use. Supporst stripWhitespaces currently. +function parseDOM (xml, options) + if not options then options={} end + local rich = not options.simple + local push, pop = table.insert, table.remove + local stack = {} + local doc = { type="document", name="#doc", kids={} } + local current = doc + local builder = parser:new{ + startElement = function(name,nsURI) + local el = { type="element", name=name, kids={}, el=rich and {} or nil, attr={}, nsURI=nsURI, parent=rich and current or nil } + if current==doc then + if doc.root then stdnse.debug2(("Encountered element '%s' when the document already has a root '%s' element"):format(name,doc.root.name)) return end + doc.root = el + end + push(current.kids,el) + if current.el then push(current.el,el) end + current = el + push(stack,el) + end, + attribute = function(name,value,nsURI) + if not current or current.type~="element" then stdnse.debug2(("Encountered an attribute %s=%s but I wasn't inside an element"):format(name,value)) return end + local attr = {type='attribute',name=name,nsURI=nsURI,value=value,parent=rich and current or nil} + if rich then current.attr[name] = value end + push(current.attr,attr) + end, + closeElement = function(name) + if current.name~=name or current.type~="element" then stdnse.debug2(("Received a close element notification for '%s' but was inside a '%s' %s"):format(name,current.name,current.type)) return end + pop(stack) + current = stack[#stack] + end, + text = function(value) + if current.type~='document' then + if current.type~="element" then stdnse.debug2(("Received a text notification '%s' but was inside a %s"):format(value,current.type)) return end + push(current.kids,{type='text',name='#text',value=value,parent=rich and current or nil}) + end + end, + comment = function(value) + push(current.kids,{type='comment',name='#comment',value=value,parent=rich and current or nil}) + end, + pi = function(name,value) + push(current.kids,{type='pi',name=name,value=value,parent=rich and current or nil}) + end + } + builder:parseSAX (xml,options) + return doc +end + +return _ENV; + diff --git a/scripts/hnap-info.nse b/scripts/hnap-info.nse new file mode 100644 index 000000000..2494e4f2e --- /dev/null +++ b/scripts/hnap-info.nse @@ -0,0 +1,116 @@ +local http = require "http" +local table = require "table" +local shortport = require "shortport" +local stdnse = require "stdnse" +local slaxml = require "slaxml" +local nmap = require "nmap" + +description = [[ +Retrieve hardwares details and configuration information utilizing HNAP, the "Home Network Administration Protocol". +It is an HTTP-Simple Object Access Protocol (SOAP)-based protocol which allows for remote topology discovery, +configuration, and management of devices (routers, cameras, PCs, NAS, etc.)]] + +--- +-- @usage +-- nmap --script hnap-info -p80,8080 +-- +-- @output +-- PORT STATE SERVICE REASON +-- 8080/tcp open http-proxy syn-ack +-- | hnap-info: +-- | Type: GatewayWithWiFi +-- | Device: Ingraham +-- | Vendor: Linksys +-- | Description: Linksys E1200 +-- | Model: E1200 +-- | Firmware: 1.0.00 build 11 +-- | Presentation URL: http://192.168.1.1/ +-- | SOAPACTIONS: +-- | http://purenetworks.com/HNAP1/IsDeviceReady +-- | http://purenetworks.com/HNAP1/GetDeviceSettings +-- | http://purenetworks.com/HNAP1/SetDeviceSettings +-- | http://purenetworks.com/HNAP1/GetDeviceSettings2 +-- | http://purenetworks.com/HNAP1/SetDeviceSettings2 +-- +-- +-- @xmloutput +-- GatewayWithWiFi +-- Ingraham +-- Linksys +-- Linksys E1200 +-- E1200 +-- 1.0.00 build 11 +-- http://192.168.1.1/ +-- +-- http://purenetworks.com/HNAP1/IsDeviceReady +-- http://purenetworks.com/HNAP1/GetDeviceSettings +-- http://purenetworks.com/HNAP1/SetDeviceSettings +-- http://purenetworks.com/HNAP1/GetDeviceSettings2 +-- http://purenetworks.com/HNAP1/SetDeviceSettings2 +--
+----------------------------------------------------------------------- + +author = "Gyanendra Mishra" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = { + "safe", + "discovery", + "default", +} + + +portrule = shortport.http + +local ELEMENTS = {["Type"] = "Type", +["DeviceName"] = "Device", +["VendorName"] = "Vendor", +["ModelDescription"] = "Description", +["ModelName"] = "Model", +["FirmwareVersion"] = "Firmware", +["PresentationURL"] = "Presentation URL", +["string"] = "SOAPACTIONS", +["SubDeviceURLs"] = "Sub Device URLs"} + +function get_text_callback(store, name) + if ELEMENTS[name] == nil then return end + name = ELEMENTS[name] + if name == 'SOAPACTIONS' or name == 'Sub Device URLs' or name == 'Type' then + return function(content) + store[name] = store[name] or {} + table.insert(store[name], content) + end + else + return function(content) + store[name] = content + end + end +end + +function action (host, port) + local output = stdnse.output_table() + local response = http.get(host, port, '/HNAP1') + if response.status and response.status == 200 then + local parser = slaxml.parser:new() + parser._call = {startElement = function(name) + parser._call.text = get_text_callback(output, name) end, + closeElement = function(name) parser._call.text = function() return nil end end + } + parser:parseSAX(response.body, {stripWhitespace=true}) + + -- set the port verson + port.version.name = "hnap" + port.version.name_confidence = 10 + port.version.product = output["Description"] or nil + port.version.version = output["Model"] or nil + port.version.devicetype = output["Type"] and output["Type"][1] or nil + port.version.cpe = port.version.cpe or {} + + if output["Vendor"] and output["Model"] then + table.insert(port.version.cpe, "cpe:/h:".. output["Vendor"]:lower() .. ":" .. output["Model"]:lower()) + end + nmap.set_port_version(host, port, "hardmatched") + + if #output >0 then return output end + end +end + diff --git a/scripts/script.db b/scripts/script.db index 6cb3d1997..24a300c0c 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -131,6 +131,7 @@ Entry { filename = "hadoop-tasktracker-info.nse", categories = { "default", "dis Entry { filename = "hbase-master-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "hbase-region-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "hddtemp-info.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "hnap-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "hostmap-bfk.nse", categories = { "discovery", "external", "intrusive", } } Entry { filename = "hostmap-ip2hosts.nse", categories = { "discovery", "external", } } Entry { filename = "hostmap-robtex.nse", categories = { "discovery", "external", "safe", } }