diff options
Diffstat (limited to 'scripts/http-default-accounts.nse')
-rw-r--r-- | scripts/http-default-accounts.nse | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/scripts/http-default-accounts.nse b/scripts/http-default-accounts.nse new file mode 100644 index 0000000..36bd9ea --- /dev/null +++ b/scripts/http-default-accounts.nse @@ -0,0 +1,446 @@ +local _G = require "_G" +local creds = require "creds" +local http = require "http" +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local table = require "table" + +description = [[ +Tests for access with default credentials used by a variety of web applications and devices. + +It works similar to http-enum, we detect applications by matching known paths and launching a login routine using default credentials when found. +This script depends on a fingerprint file containing the target's information: name, category, location paths, default credentials and login routine. + +You may select a category if you wish to reduce the number of requests. We have categories like: +* <code>web</code> - Web applications +* <code>routers</code> - Routers +* <code>security</code> - CCTVs and other security devices +* <code>industrial</code> - Industrial systems +* <code>printer</code> - Network-attached printers and printer servers +* <code>storage</code> - Storage devices +* <code>virtualization</code> - Virtualization systems +* <code>console</code> - Remote consoles + +You can also select a specific fingerprint or a brand, such as BIG-IQ or Siemens. This matching is based on case-insensitive words. This means that "nas" will select Seagate BlackArmor NAS storage but not Netgear ReadyNAS. + +For a fingerprint to be used it needs to satisfy both the category and name criteria. + +By default, the script produces output only when default credentials are found, while staying silent when the target only matches some fingerprints (but no credentials are found). With increased verbosity (option -v), the script will also report all matching fingerprints. + +Please help improve this script by adding new entries to nselib/data/http-default-accounts.lua + +Remember each fingerprint must have: +* <code>name</code> - Descriptive name +* <code>category</code> - Category +* <code>login_combos</code> - Table of login combinations +* <code>paths</code> - Table containing possible path locations of the target +* <code>login_check</code> - Login function of the target + +In addition, a fingerprint should have: +* <code>target_check</code> - Target validation function. If defined, it will be called to validate the target before attempting any logins. +* <code>cpe</code> - Official CPE Dictionary entry (see https://nvd.nist.gov/cpe.cfm) + +Default fingerprint file: /nselib/data/http-default-accounts-fingerprints.lua +This script was based on http-enum. +]] + +--- +-- @usage +-- nmap -p80 --script http-default-accounts host/ip +-- +-- @output +-- PORT STATE SERVICE +-- 80/tcp open http +-- | http-default-accounts: +-- | [Cacti] at / +-- | admin:admin +-- | [Nagios] at /nagios/ +-- |_ nagiosadmin:CactiEZ +-- +-- @xmloutput +-- <table key="Cacti"> +-- <elem key="cpe">cpe:/a:cacti:cacti</elem> +-- <elem key="path">/</elem> +-- <table key="credentials"> +-- <table> +-- <elem key="username">admin</elem> +-- <elem key="password">admin</elem> +-- </table> +-- </table> +-- </table> +-- <table key="Nagios"> +-- <elem key="cpe">cpe:/a:nagios:nagios</elem> +-- <elem key="path">/nagios/</elem> +-- <table key="credentials"> +-- <table> +-- <elem key="username">nagiosadmin</elem> +-- <elem key="password">CactiEZ</elem> +-- </table> +-- </table> +-- </table> +-- +-- @args http-default-accounts.basepath Base path to append to requests. Default: "/" +-- @args http-default-accounts.fingerprintfile Fingerprint filename. Default: http-default-accounts-fingerprints.lua +-- @args http-default-accounts.category Selects a fingerprint category (or a list of categories). +-- @args http-default-accounts.name Selects fingerprints by a word (or a list of alternate words) included in their names. + +-- Revision History +-- 2013-08-13 nnposter +-- * added support for target_check() +-- 2014-04-27 +-- * changed category from safe to intrusive +-- 2016-08-10 nnposter +-- * added sharing of probe requests across fingerprints +-- 2016-10-30 nnposter +-- * removed a limitation that prevented testing of systems returning +-- status 200 for non-existent pages. +-- 2016-12-01 nnposter +-- * implemented XML structured output +-- * changed classic output to report empty credentials as <blank> +-- 2016-12-04 nnposter +-- * added CPE entries to individual fingerprints (where known) +-- 2018-12-17 nnposter +-- * added ability to select fingerprints by their name +-- 2020-07-11 nnposter +-- * added reporting of all matched fingerprints when verbosity is increased +--- + +author = {"Paulino Calderon <calderon@websec.mx>", "nnposter"} +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "auth", "intrusive"} + +portrule = shortport.http + +--- +--validate_fingerprints(fingerprints) +--Returns an error string if there is something wrong with +--fingerprint table. +--Modified version of http-enums validation code +--@param fingerprints Fingerprint table +--@return Error string if its an invalid fingerprint table +--- +local function validate_fingerprints(fingerprints) + + for i, fingerprint in pairs(fingerprints) do + if(type(i) ~= 'number') then + return "The 'fingerprints' table is an array, not a table; all indexes should be numeric" + end + -- Validate paths + if(not(fingerprint.paths) or + (type(fingerprint.paths) ~= 'table' and type(fingerprint.paths) ~= 'string') or + (type(fingerprint.paths) == 'table' and #fingerprint.paths == 0)) then + return "Invalid path found in fingerprint entry #" .. i + end + if(type(fingerprint.paths) == 'string') then + fingerprint.paths = {fingerprint.paths} + end + for i, path in pairs(fingerprint.paths) do + -- Validate index + if(type(i) ~= 'number') then + return "The 'paths' table is an array, not a table; all indexes should be numeric" + end + -- Convert the path to a table if it's a string + if(type(path) == 'string') then + fingerprint.paths[i] = {path=fingerprint.paths[i]} + path = fingerprint.paths[i] + end + -- Make sure the paths table has a 'path' + if(not(path['path'])) then + return "The 'paths' table requires each element to have a 'path'." + end + end + -- Check login combos + for i, combo in pairs(fingerprint.login_combos) do + -- Validate index + if(type(i) ~= 'number') then + return "The 'login_combos' table is an array, not a table; all indexes should be numeric" + end + -- Make sure the login_combos table has at least one login combo + if(not(combo['username']) or not(combo["password"])) then + return "The 'login_combos' table requires each element to have a 'username' and 'password'." + end + end + + -- Make sure they include the login function + if(type(fingerprint.login_check) ~= "function") then + return "Missing or invalid login_check function in entry #"..i + end + -- Make sure that the target validation is a function + if(fingerprint.target_check and type(fingerprint.target_check) ~= "function") then + return "Invalid target_check function in entry #"..i + end + -- Are they missing any fields? + if(fingerprint.category and type(fingerprint.category) ~= "string") then + return "Missing or invalid category in entry #"..i + end + if(fingerprint.name and type(fingerprint.name) ~= "string") then + return "Missing or invalid name in entry #"..i + end + end +end + +-- Simplify unlocking the mutex, ensuring we don't try to load the fingerprints +-- again by storing and returning an error message in place of the cached +-- fingerprints. +-- @param mutex Mutex that controls fingerprint loading +-- @param err Error message +-- @return Status (always false) +-- @return Error message passed in +local function bad_prints(mutex, err) + nmap.registry.http_default_accounts_fingerprints = err + mutex "done" + return false, err +end + +--- +-- Loads data from file and returns table of fingerprints if sanity checks are +-- passed. +-- @param filename Fingerprint filename +-- @param catlist Categories of fingerprints to use +-- @param namelist Alternate words required in fingerprint names +-- @return Status (true or false) +-- @return Table of fingerprints (or an error message) +--- +local function load_fingerprints(filename, catlist, namelist) + local file, filename_full, fingerprints + + -- Check if fingerprints are cached + local mutex = nmap.mutex("http_default_accounts_fingerprints") + mutex "lock" + local cached_fingerprints = nmap.registry.http_default_accounts_fingerprints + if type(cached_fingerprints) == "table" then + stdnse.debug(1, "Loading cached fingerprints") + mutex "done" + return true, cached_fingerprints + end + if type(cached_fingerprints) == "string" then + -- cached_fingerprints contains an error message from a prior load attempt + return bad_prints(mutex, cached_fingerprints) + end + assert(type(cached_fingerprints) == "nil", "Unexpected cached fingerprints") + + -- Try and find the file + -- If it isn't in Nmap's directories, take it as a direct path + filename_full = nmap.fetchfile('nselib/data/' .. filename) + if(not(filename_full)) then + filename_full = filename + end + + -- Load the file + stdnse.debug(1, "Loading fingerprints: %s", filename_full) + local env = setmetatable({fingerprints = {}}, {__index = _G}); + file = loadfile(filename_full, "t", env) + if( not(file) ) then + stdnse.debug(1, "Couldn't load the file: %s", filename_full) + return bad_prints(mutex, "Couldn't load fingerprint file: " .. filename_full) + end + file() + fingerprints = env.fingerprints + + -- Validate fingerprints + local valid_flag = validate_fingerprints(fingerprints) + if type(valid_flag) == "string" then + return bad_prints(mutex, valid_flag) + end + + -- Category filter + if catlist then + if type(catlist) ~= "table" then + catlist = {catlist} + end + local filtered_fingerprints = {} + for _, fingerprint in pairs(fingerprints) do + for _, cat in ipairs(catlist) do + if fingerprint.category == cat then + table.insert(filtered_fingerprints, fingerprint) + break + end + end + end + fingerprints = filtered_fingerprints + end + + -- Name filter + if namelist then + if type(namelist) ~= "table" then + namelist = {namelist} + end + local matchlist = {} + for _, name in ipairs(namelist) do + table.insert(matchlist, "%f[%w]" + .. tostring(name):lower():gsub("%W", "%%%1") + .. "%f[%W]") + end + local filtered_fingerprints = {} + for _, fingerprint in pairs(fingerprints) do + local fpname = fingerprint.name:lower() + for _, match in ipairs(matchlist) do + if fpname:find(match) then + table.insert(filtered_fingerprints, fingerprint) + break + end + end + end + fingerprints = filtered_fingerprints + end + + -- Check there are fingerprints to use + if(#fingerprints == 0 ) then + return bad_prints(mutex, "No fingerprints were loaded after processing ".. filename) + end + + -- Cache the fingerprints for other scripts, so we aren't reading the files every time + nmap.registry.http_default_accounts_fingerprints = fingerprints + mutex "done" + return true, fingerprints +end + +--- +-- format_basepath(basepath) +-- Modifies a given path so that it can be later prepended to another absolute +-- path to form a new absolute path. +-- @param basepath Basepath string +-- @return Basepath string with a leading slash and no trailing slashes. +-- (Empty string is returned if the input is an empty string +-- or "/".) +--- +local function format_basepath(basepath) + if basepath:sub(1,1) ~= "/" then + basepath = "/" .. basepath + end + return basepath:gsub("/+$","") +end + +--- +-- test_credentials(host, port, fingerprint, path) +-- Tests default credentials of a given fingerprint against a given path. +-- Any successful credentials are registered in the Nmap credential repository. +-- @param host table as received by the scripts action method +-- @param port table as received by the scripts action method +-- @param fingerprint as defined in the fingerprint file +-- @param path againt which the the credentials will be tested +-- @return out table suitable for inclusion in the script structured output +-- (or nil if no credentials succeeded) +-- @return txtout table suitable for inclusion in the script textual output +--- +local function test_credentials (host, port, fingerprint, path) + local credlst = {} + for _, login_combo in ipairs(fingerprint.login_combos) do + local user = login_combo.username + local pass = login_combo.password + stdnse.debug(1, "[%s] Trying login combo %s:%s", fingerprint.name, + stdnse.string_or_blank(user), stdnse.string_or_blank(pass)) + if fingerprint.login_check(host, port, path, user, pass) then + stdnse.debug(1, "[%s] Valid default credentials found", fingerprint.name) + local cred = stdnse.output_table() + cred.username = user + cred.password = pass + table.insert(credlst, cred) + end + end + if #credlst == 0 and nmap.verbosity() < 2 then return nil end + -- Some credentials found or increased verbosity. Generate the output report + local out = stdnse.output_table() + out.cpe = fingerprint.cpe + out.path = path + out.credentials = credlst + local txtout = {} + txtout.name = ("[%s] at %s"):format(fingerprint.name, path) + if #credlst == 0 then + table.insert(txtout, "(no valid default credentials found)") + return out, txtout + end + for _, cred in ipairs(credlst) do + table.insert(txtout,("%s:%s"):format(stdnse.string_or_blank(cred.username), + stdnse.string_or_blank(cred.password))) + end + -- Register the credentials + local credreg = creds.Credentials:new(SCRIPT_NAME, host, port) + for _, cred in ipairs(credlst) do + credreg:add(cred.username, cred.password, creds.State.VALID ) + end + return out, txtout +end + + +action = function(host, port) + local fingerprint_filename = stdnse.get_script_args("http-default-accounts.fingerprintfile") or "http-default-accounts-fingerprints.lua" + local catlist = stdnse.get_script_args("http-default-accounts.category") + local namelist = stdnse.get_script_args("http-default-accounts.name") + local basepath = stdnse.get_script_args("http-default-accounts.basepath") or "/" + local output = stdnse.output_table() + local text_output = {} + + -- Determine the target's response to "404" HTTP requests. + local status_404, result_404, known_404 = http.identify_404(host,port) + -- The default target_check is the existence of the probe path on the target. + -- To reduce false-positives, fingerprints that lack target_check() will not + -- be tested on targets on which a "404" response is 200. + local default_target_check = + function (host, port, path, response) + if status_404 and result_404 == 200 then return false end + return http.page_exists(response, result_404, known_404, path, true) + end + + --Load fingerprint data or abort + local status, fingerprints = load_fingerprints(fingerprint_filename, catlist, namelist) + if(not(status)) then + return stdnse.format_output(false, fingerprints) + end + stdnse.debug(1, "%d fingerprints were loaded", #fingerprints) + + --Format basepath: Removes or adds slashs + basepath = format_basepath(basepath) + + -- Add requests to the http pipeline + local pathmap = {} + local requests = nil + stdnse.debug(1, "Trying known locations under path '%s' (change with '%s.basepath' argument)", basepath, SCRIPT_NAME) + for _, fingerprint in ipairs(fingerprints) do + for _, probe in ipairs(fingerprint.paths) do + -- Multiple fingerprints may share probe paths so only unique paths will + -- be added to the pipeline. Table pathmap keeps track of their position + -- within the pipeline. + local path = probe.path + if not pathmap[path] then + requests = http.pipeline_add(basepath .. path, + {bypass_cache=true, redirect_ok=false}, + requests, 'GET') + pathmap[path] = #requests + end + end + end + + -- Nuclear launch detected! + local results = http.pipeline_go(host, port, requests) + if results == nil then + return stdnse.format_output(false, + "HTTP request table is empty. This should not happen since we at least made one request.") + end + + -- Iterate through fingerprints to find a candidate for login routine + for _, fingerprint in ipairs(fingerprints) do + local target_check = fingerprint.target_check or default_target_check + local credentials_found = false + stdnse.debug(1, "[%s] Examining target", fingerprint.name) + for _, probe in ipairs(fingerprint.paths) do + local result = results[pathmap[probe.path]] + if result and not credentials_found then + local path = basepath .. probe.path + if target_check(host, port, path, result) then + stdnse.debug(1, "[%s] Target matched", fingerprint.name) + local out, txtout = test_credentials(host, port, fingerprint, path) + if out then + output[fingerprint.name] = out + table.insert(text_output, txtout) + credentials_found = true + end + end + end + end + end + if #text_output > 0 then + return output, stdnse.format_output(true, text_output) + end +end |