Index: smtp-enum-users.nse =================================================================== --- smtp-enum-users.nse (revision 0) +++ smtp-enum-users.nse (revision 0) @@ -0,0 +1,321 @@ +description = [[ +Attempts to enumerate the users on a SMTP server by issuing the VRFY and the EXPN +commands. If those commands aren't implemented the script will try to use the +RCPT TO command. The goal of this script is to discover all the user accounts in the +remote system. + +The script will output the list of user names that were found. The script will stop +querying the SMTP server if authentication is enforced. The script will not repeat +commands that aren't implemented (VRFY and EXPN). + +The user can specify which technique to use. If so the script will not use any other +techinque. To do that the user must use the smtp-enum-users.method argument with one +of the following parameters: + - VFRY + - EXPN + - RCPT + +If debug is enabled and an error occurrs while testing the target host, the error will be +printed with the list of any combinations that were found prior to the error. +]] + +--- +-- @usage +-- nmap --script smtp-user-enum.nse -p 25,465,587 +-- +-- @output +-- Host script results: +-- | smtp-user-enum: +-- | root +-- |_ test +-- +-- @args smtp-enum-users.domain Define the domain to be used in the SMTP commands. +-- @args smtp-enum-users.method Define the method to be used by the script +-- +-- @changelog +-- 2010-03-07 Duarte Silva +-- * First version ;) +----------------------------------------------------------------------- + +author = "Duarte Silva " +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery","external","intrusive"} + +require "shortport" +require "comm" +require 'unpwdb' + +portrule = shortport.port_or_service({ 25, 465, 587 }, { "smtp", "smtps", "submission" }) + +---Send a command and read the response (this function does exception handling, and if an +-- exception occurs, it will close the socket). +-- +--@param socket Socket used to send the command +--@param request Command to be sent +--@return False in case of failure +--@return True and the response in case of success +function dorequest(socket, request) + -- Exception handler. + local catch = function() + socket:close() + end + + local try = nmap.new_try(catch) + + -- Lets send the command. + try(socket:send(request)) + + -- Receive server response. + local status, response = socket:receive_lines(1) + + if not status then + -- Close the socket, the call to receive_lines doesn't use try. + socket:close() + + -- Supported error messages. + local messages = { + ["EOF"] = "connection closed", + ["TIMEOUT"] = "connection timeout", + ["ERROR"] = "failed to receive data" + } + + return false, (messages[response] or "unspecified error, for more information use --script-trace") + end + + return true, response +end + +---Get a domain to be used in the SMTP commands that need it. If the user specified one +-- through a script argument this function will return it. Otherwise it will try to find +-- the domain from the typed hostname and from the rDNS name. If it still can't find one +-- it will use the nmap.scanme.org by default. +-- +--@param host Current scanned host +--@return The hostname to be used +function get_hostname(host) + local domain = "nmap.scanme.org" + + -- Use the user provided options. + if (nmap.registry.args["smtp-enum-users.domain"] ~= nil) then + domain = nmap.registry.args["smtp-enum-users.domain"] + elseif type(host) == "table" then + if host.targetname then + domain = host.targetname + elseif (host.name ~= '' and host.name) then + domain = host.name + end + end + + return domain +end + +function go(host, port) + local socket = nmap.new_socket() + local options = { + timeout = 10000, + recv_before = true + } + + socket:set_timeout(5000) + + -- Get the current usernames list from the file. + local status, nextuser = unpwdb.usernames() + + if not status then + socket:close() + return false, "Failed to read the user names database" + end + + -- Be polite and when everything works out send the QUIT message. + local quit = function() + dorequest(socket, "QUIT\r\n") + socket:close() + end + + -- Get the domain to use in the commands. + local domain = get_hostname(host) + + -- Try to connect to server. + local response + + socket, response = comm.tryssl(host, port, string.format("EHLO %s\r\n", domain), options) + + -- Failed connection attempt. + if not socket then + return false, string.format("Couldn't establish connection on port %i", port.number) + end + + -- Close socket and return if EHLO command failed. + if not string.match(response, "^250") then + quit() + return false, "Failed to issue EHLO command" + end + + local result = {} + + -- This function is used when something goes wrong with the connection. It makes sure that + -- if it found users before the error occurred, they will be returned. If the debug flag is + -- enabled the error message will be appended to the user list. + local failure = function(message) + if #result > 0 then + if nmap.debugging() > 0 then + table.insert(result, string.format("ERROR: %s", message)) + end + + return true, result + else + return false, message + end + end + + local ignore_vrfy, ignore_expn, ignore_rcpt, issued_from = false, false, false, false + -- Get the method. + if (nmap.registry.args["smtp-enum-users.method"] ~= nil) then + local method = nmap.registry.args["smtp-enum-users.method"] + + if type(method) == "string" then + if string.find(method, "^VRFY$", 0) then + ignore_vrfy, ignore_expn, ignore_rcpt = false, true, true + elseif string.find(method, "^EXPN$", 0) then + ignore_vrfy, ignore_expn, ignore_rcpt = true, false, true + elseif string.find(method, "^RCPT$", 0) then + ignore_vrfy, ignore_expn, ignore_rcpt = true, true, false + end + end + end + + -- Get the first user to be tested. + local username = nextuser() + + while username do + -- User name and hostname combinations that can be used. + local combinations = { + string.format("%s", username), + string.format("%s@%s", username, domain) + } + local index + + if ignore_vrfy and ignore_expn and (not ignore_rcpt) then + -- Try to find the user by issuing the MAIL FROM and RCPT TO commands (the MAIL FROM only needs + -- to be issued one time) + if not issued_from then + -- Lets try to issue MAIL FROM command. + status, response = dorequest(socket, string.format("MAIL FROM:\r\n", domain)) + + -- If this command fails to be sent, then something went wrong with the connection. + if not status then + -- We don't go through the failure function because if the exceution gets here the two commands + -- that would have added user names into result aren't implemented. + return false, string.format("Failed to issue MAIL FROM: command (%s)", domain, response) + end + + -- The command was accepted. There isn't the need to test for authentication enforcing because that + -- would be noticeable in the VRFY or EXPN commands. + if string.match(response, "^250") then + issued_from = true + else + quit() + return false, "Server did not accept the MAIL FROM command" + end + end + + -- If the MAIL FROM command was issued with success we can start verying users. + if issued_from then + for index, combination in ipairs(combinations) do + status, response = dorequest(socket, string.format("RCPT TO:<%s>\r\n", combination)) + + if not status then + return failure(string.format("Failed to issue RCPT TO:<%s> command (%s)", combination, response)) + end + + if string.match(response, "^250") then + -- Save the working from and to combination. + table.insert(result, username) + -- If we found the user with a combination, don't test the following combinations. + break + end + end + -- Get the next user name. + username = nextuser() + end + else + if not ignore_vrfy then + for index, combination in ipairs(combinations) do + -- Lets try to issue the command + status, response = dorequest(socket, string.format("VRFY %s\r\n", combination)) + + -- If this command fails to be sent, then something went wrong with the connection. + if not status then + return failure(string.format("Failed to issue VRFY %s command (%s)\n", combination, response)) + end + + -- If the command failed, check if authentication is needed because all the other attempts will fail + -- and server may disconnect because of too many commands issued without authentication. + if string.match(response, "^530") then + quit() + return false, "Couldn't perform user enumeration, authentication needed" + elseif string.match(response, "^502") then + -- The server doesn't implement the command. + ignore_vrfy = true + break + elseif string.match(response, "^250") then + table.insert(result, string.format("%s\n", username)) + break + end + end + + -- If the command is implemented then the user was tested successfully. Otherwise the user needs to + -- be tested by the following technique. + if not ignore_vrfy then + username = nextuser() + end + elseif not ignore_expn then + for index, combination in ipairs(combinations) do + -- Lets try to issue the command + status, response = dorequest(socket, string.format("EXPN %s\r\n", combination)) + + -- If this command fails to be sent, then something went wrong with the connection. + if not status then + return failure(string.format("Failed to issue EXPN %s command (%s)\n", combination, response)) + end + + -- If the command failed, check if authentication is needed because all the other attempts will fail + -- and server may disconnect because of too many commands issued without authentication. + if string.match(response, "^530") then + quit() + return false, "Couldn't perform user enumeration, authentication needed" + elseif string.match(response, "^502") then + -- The server doesn't implement the command. + ignore_expn = true + break + elseif string.match(response, "^250") then + table.insert(result, string.format("%s\n", username)) + break + end + end + + -- If the command is implemented then the user was tested successfully. Otherwise the user needs to + -- be tested by the following technique. + if not ignore_expn then + username = nextuser() + end + else + -- No more techniques. + break + end + end + end + + quit() + return true, result +end + +action = function(host, port) + local status, result = go(host, port) + + if #result == 0 then + return stdnse.format_output(false, "Couldn't find any account names") + end + + return stdnse.format_output(status, result) +end