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 string = require "string" 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: * web - Web applications * routers - Routers * security - CCTVs and other security devices * industrial - Industrial systems * printer - Network-attached printers and printer servers * storage - Storage devices * virtualization - Virtualization systems * console - Remote consoles Please help improve this script by adding new entries to nselib/data/http-default-accounts.lua Remember each fingerprint must have: * name - Descriptive name * category - Category * login_combos - Table of login combinations * paths - Table containing possible path locations of the target * login_check - Login function of the target In addition, a fingerprint should have: * target_check - Target validation function. If defined, it will be called to validate the target before attempting any logins. * cpe - 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 -- -- cpe:/a:cacti:cacti -- / --
--
-- admin -- admin --
-- -- -- -- cpe:/a:nagios:nagios -- /nagios/ --
--
-- nagiosadmin -- CactiEZ --
-- -- -- -- @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 category of fingerprints to use. -- 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 -- 2016-12-04 nnposter -- * added CPE entries to individual fingerprints (where known) --- author = {"Paulino Calderon ", "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 --- -- load_fingerprints(filename, category) -- Loads data from file and returns table of fingerprints if sanity checks are passed -- Based on http-enum's load_fingerprints() -- @param filename Fingerprint filename -- @param cat Category of fingerprints to use -- @return Table of fingerprints --- local function load_fingerprints(filename, cat) local file, filename_full, fingerprints -- Check if fingerprints are cached if(nmap.registry.http_default_accounts_fingerprints ~= nil) then stdnse.debug(1, "Loading cached fingerprints") return nmap.registry.http_default_accounts_fingerprints end -- 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 false, "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 false, valid_flag end -- Category filter if ( cat ) then local filtered_fingerprints = {} for _, fingerprint in pairs(fingerprints) do if(fingerprint.category == cat) then table.insert(filtered_fingerprints, fingerprint) end end fingerprints = filtered_fingerprints end -- Check there are fingerprints to use if(#fingerprints == 0 ) then return false, "No fingerprints were loaded after processing ".. filename end 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(2, "Trying login combo -> %s:%s", user, 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 then return nil end -- Some credentials found. Generate the fingerprint 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) 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 fingerprintload_status, status, fingerprints, pathmap, requests, results local fingerprint_filename = stdnse.get_script_args("http-default-accounts.fingerprintfile") or "http-default-accounts-fingerprints.lua" local category = stdnse.get_script_args("http-default-accounts.category") or false 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 status, fingerprints = load_fingerprints(fingerprint_filename, category) 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 pathmap = {} 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! 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, "Processing %s", 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 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