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 may have:
* target_check
- Target validation function. If defined, it will be called to validate the target before attempting any logins.
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
-- /
--
--
--
-- 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
return output, stdnse.format_output(true, text_output)
end