diff --git a/scripts/http-default-accounts.nse b/scripts/http-default-accounts.nse new file mode 100644 index 000000000..c5b0aca4c --- /dev/null +++ b/scripts/http-default-accounts.nse @@ -0,0 +1,286 @@ +description = [[ +http-default-accounts tests for access with default credentials in 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 +* router - Routers +* voip - VOIP devices +* security + +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 - Paths table containing the possible location of the target +* login_check - Login function of the target + +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 REASON +-- 80/tcp open http syn-ack +-- |_http-default-accounts: [Cacti] credentials found -> admin:admin Path:/cacti/ +-- Final times for host: srtt: 94615 rttvar: 71012 to: 378663 +-- +-- @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. +-- +-- Other useful arguments relevant to this script: +-- http.pipeline Sets max number of petitions in the same request. +-- http.useragent User agent for HTTP requests +--- + +author = "Paulino Calderon" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "auth", "safe"} + +require "creds" +require "http" +require "shortport" +portrule = shortport.http + +--- +--validate_fingerprints(fingerprints) +--Returns an error string if there is something wrong with +--fingerprint table. +--Modified's 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 + -- 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.print_debug(1, "%s: Loading cached fingerprints", SCRIPT_NAME) + 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.print_debug(1, "%s: Loading fingerprints: %s", SCRIPT_NAME, filename_full) + file = loadfile(filename_full) + if( not(file) ) then + stdnse.print_debug(1, "%s: Couldn't load the file: %s", SCRIPT_NAME, filename_full) + return false, "Couldn't load fingerprint file: " .. filename_full + end + setfenv(file, setmetatable({fingerprints = {}; }, {__index = _G})) + file() + fingerprints = getfenv(file)["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) +-- Removes trailing and leading dashes in a string +-- @param basepath Basepath string +-- @return Basepath string with no trailing or leading dashes +--- +local function format_basepath(basepath) + -- Remove trailing slash, if it exists + if(#basepath > 1 and string.sub(basepath, #basepath, #basepath) == '/') then + basepath = string.sub(basepath, 1, #basepath - 1) + end + -- Add a leading slash, if it doesn't exist + if(#basepath <= 1) then + basepath = '' + else + if(string.sub(basepath, 1, 1) ~= '/') then + basepath = '/' .. basepath + end + end + return basepath +end + +--- +-- register_http_credentials(username, password) +-- Stores HTTP credentials in the registry. If the registry entry hasn't been +-- initiated, it will create it and store the credentials. +-- @param login_username Username +-- @param login_password Password +--- +local function register_http_credentials(login_username, login_password) + local c = creds.Credentials:new( SCRIPT_NAME, host, port ) + c:add(login_username, login_password, creds.State.VALID ) +end + +--- +-- MAIN +-- Here we iterate through the paths to try to find a target. When a target is found +-- the login routine is initialized to check for default credentials authentication +--- +action = function(host, port) + local fingerprintload_status, fingerprints, requests, results + local fingerprint_filename = nmap.registry.args["http-default-accounts.fingerprintfile"] or "http-defaul-accounts-fingerprints.lua" + local category = nmap.registry.args["http-default-accounts.category"] or false + local basepath = nmap.registry.args["http-default-accounts.basepath"] or "/" + local output_lns = {} + + --Load fingerprint data or abort + status, fingerprints = load_fingerprints(fingerprint_filename, category) + if(not(status)) then + return stdnse.format_output(false, fingerprints) + end + stdnse.print_debug(1, "%s: %d fingerprints were loaded", SCRIPT_NAME, #fingerprints) + + --Format basepath: Removes or adds slashs + basepath = format_basepath(basepath) + + -- Add requests to the http pipeline + requests = {} + stdnse.print_debug(1, "%s: Searching for entries under path '%s' (change with '%s.basepath' argument)", SCRIPT_NAME, basepath, SCRIPT_NAME) + for i = 1, #fingerprints, 1 do + for j = 1, #fingerprints[i].paths, 1 do + requests = http.pipeline_add(basepath .. fingerprints[i].paths[j].path, nil, requests, 'GET') + end + end + + -- Nuclear launch detected! + results = http.pipeline_go(host, port, requests, nil) + if results == nil then + return "[ERROR] HTTP request table is empty. This should not happen since we at least made one request." + end + + -- Record 404 response, later it will be used to determine if page exists + local result, result_404, known_404 = http.identify_404(host, port) + if(result == false) then + return stdnse.format_output(false, result_404) + end + + -- Iterate through responses to find a candidate for login routine + local j = 1 + for i, fingerprint in ipairs(fingerprints) do + stdnse.print_debug(1, "%s: Processing %s", SCRIPT_NAME, fingerprint.name) + for _, probe in ipairs(fingerprint.paths) do + + if (results[j]) then + local path = basepath .. probe['path'] + + if( http.page_exists(results[j], result_404, known_404, path, true) ) then + for _, login_combo in ipairs(fingerprint.login_combos) do + stdnse.print_debug(2, "%s: Trying login combo -> %s:%s", SCRIPT_NAME, login_combo["username"], login_combo["password"]) + --Check default credentials + if( fingerprint.login_check(host, port, path, login_combo["username"], login_combo["password"]) ) then + + --Valid credentials found + stdnse.print_debug(1, "%s:[%s] valid default credentials found.", SCRIPT_NAME, fingerprint.name) + output_lns[#output_lns + 1] = string.format("[%s] credentials found -> %s:%s Path:%s", + fingerprint.name, login_combo["username"], login_combo["password"], path) + -- Add to http credentials table + register_http_credentials(login_combo["username"], login_combo["password"]) + end + end + end + end + j = j + 1 + end + end + + if #output_lns > 0 then + return stdnse.strjoin("\n", output_lns) + end +end diff --git a/scripts/script.db b/scripts/script.db index 3bd2a0014..eb349c445 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -65,6 +65,7 @@ Entry { filename = "http-barracuda-dir-traversal.nse", categories = { "auth", "e Entry { filename = "http-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "http-cakephp-version.nse", categories = { "discovery", "safe", } } Entry { filename = "http-date.nse", categories = { "discovery", "safe", } } +Entry { filename = "http-default-accounts.nse", categories = { "auth", "discovery", "safe", } } Entry { filename = "http-domino-enum-passwords.nse", categories = { "auth", "intrusive", } } Entry { filename = "http-enum.nse", categories = { "discovery", "intrusive", "vuln", } } Entry { filename = "http-favicon.nse", categories = { "default", "discovery", "safe", } }