Index: smtp-enum-users.nse =================================================================== --- smtp-enum-users.nse (revision 0) +++ smtp-enum-users.nse (revision 0) @@ -0,0 +1,187 @@ +description = [[ +Attempts to enumerate the users on a SMTP server by issuing the VRFY and the EXPN +commands. 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 as well as the command +performed to verify if the user exist. The script will stop querying the SMTP server +if authentication is enforced, or if the commands used aren't implemented. +]] + +--- +-- @usage +-- nmap --script smtp-user-enum.nse -p 25,465,587 +-- +-- @output +-- Host script results: +-- | smtp-user-enum: +-- | root +-- |_ test +-- +-- @args smtp-open-relay.domain Define the domain to be used in the anti-spam tests (default is nmap.scanme.org) +-- +-- @changelog +-- 2010-02-27 Duarte Silva +-- * First version ;) +----------------------------------------------------------------------- + +author = "Arturo 'Buanzo' Busleiman " +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery","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 + -- Try function + 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 + 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 + +function go(host, port) + -- Script default options + local domain = "nmap.scanme.org" + 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, usernames = unpwdb.usernames() + + if not status then + socket:close() + return false, "Failed to read the user names database" + end + + -- Use the user provided options + if (nmap.registry.args["smtp-open-relay.domain"] ~= nil) then + domain = nmap.registry.args["smtp-open-relay.domain"] + end + + -- Be polite and when everything works out send the QUIT message. + local quit = function() + dorequest(socket, "QUIT\r\n") + socket:close() + end + + -- 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 there's an STMP status code != 250 + if not string.match(response, "^250") then + quit() + return false, "Failed to issue EHLO command" + end + + local result = {} + local ignore_vrfy, ignore_expn = false, false + + -- 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. + local failure = function(message) + if #result > 0 then + return true, result + else + return false, message + end + end + + for username in usernames do + -- The server doesn't implement any of the methods. Just quit. + if ignore_vrfy and ignore_expn then + quit() + return false, "The server doesn't implement both the VRFY and EXPN commands" + elseif not ignore_vrfy then + -- Lets try to issue the command + status, response = dorequest(socket, string.format("VRFY %s\r\n", username)) + + -- 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", username, 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 + elseif string.match(response, "^250") then + table.insert(result, string.format("%s\n", username)) + end + else + status, response = dorequest(socket, string.format("EXPN %s\r\n", username)) + + if not status then + return failure(string.format("Failed to issue EXPN %s command (%s)\n", username, 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 + ignore_expn = true + elseif string.match(response, "^250") then + table.insert(result, string.format("%s\n", username)) + 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