Index: smtp-open-relay.nse =================================================================== --- smtp-open-relay.nse (revision 16835) +++ smtp-open-relay.nse (working copy) @@ -1,114 +1,235 @@ description = [[ Checks if an SMTP server is an open relay. + +This script attempts to relay by issuing a predefined combination of SMTP commands. The list +of commands is hardcoded. The commands used, are fuzzed like MAIL FROM and RCPT TO commands. ]] --- Arturo 'Buanzo' Busleiman / www.buanzo.com.ar / linux-consulting.buanzo.com.ar --- Same as Nmap--See http://nmap.org/book/man-legal.html file for licence details --- This is version 20070516. --- Changelog: --- * I changed it to the "demo" category until we figure out what --- to do about using real hostnames. -Fyodor --- + Added some strings to return in different places. --- * Changed "HELO www.[ourdomain]" to "EHLO [ourdomain]". +--- +-- @usage +-- nmap --script smtp-open-relay.nse [--script-args smtp-open-relay.domain=,smtp-open-relay.ip=
] -p 25,465,587 +-- +-- @output +-- Host script results: +-- | smtp-open-relay: +-- | MAIL FROM: -> RCPT TO:<"relaytest@nmap.scanme.org"> +-- | MAIL FROM: -> RCPT TO:<"relaytest%nmap.scanme.org"> +-- |_ MAIL FROM: -> RCPT TO: +-- +-- @args smtp-open-relay.domain Define the domain to be used in the anti-spam tests (default is nmap.scanme.org) +-- @args smtp-open-relay.ip Use this to change the IP address to be used (default is the target IP address) +-- +-- @changelog +-- 2007-05-16 Arturo 'Buanzo' Busleiman +-- + Added some strings to return in different places +-- * Changed "HELO www.[ourdomain]" to "EHLO [ourdomain]" -- * Fixed some API differences -- * The "ourdomain" variable's contents are used instead of hardcoded "insecure.org". Settable by the user. -- * Fixed tags -> categories (reported by Jason DePriest to nmap-dev) +-- 2009-09-20 Duarte Silva +-- * Rewrote the script +-- + Added documentation and some more comments +-- + Parameter to define the domain to be used instead of "ourdomain" variable +-- + Parameter to define the IP address to be used instead of the target IP address +-- * Script now detects servers that enforce authentication +-- * Changed script categories from demo to discovery and intrusive +-- * Renamed "spamtest" strings to "antispam" +-- 2010-02-20 Duarte Silva +-- * Renamed script parameters to follow the new naming convention +-- * Fixed problem with broken connections +-- * Changed script output to show all the successful tests +-- * Changed from string concatenation to string formatting +-- + External category +-- + Now the script will issue the QUIT message as specified in the SMTP RFC +----------------------------------------------------------------------- -categories = {"demo"} +author = "Arturo 'Buanzo' Busleiman " +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery","intrusive","external"} require "shortport" require "comm" -ourdomain="scanme.org" +portrule = shortport.port_or_service({ 25, 465, 587 }, { "smtp", "smtps", "submission" }) -portrule = shortport.port_or_service({25, 465, 587}, {"smtp", "smtps"}) +---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) -action = function(host, port) + -- Lets send the command + try(socket:send(request)) + -- Receive server response + local status, response = socket:receive_lines(1) + + if not status then + -- Don't really care what kind of error happened + return false + end + + return true, response +end + +function go(host, port) + -- Script default options + local domain = "nmap.scanme.org" + local ip = host.ip local socket = nmap.new_socket() - local result - local status = true + local options = { + timeout = 10000, + recv_before = true + } - local mailservername - local tor = {} - local i + socket:set_timeout(5000) - opt = {timeout=10000, recv_before=true} - socket, result = comm.tryssl(host, port, "EHLO " ..ourdomain.."\r\n", opt) - if not socket then - return "Unable to establish connection" + -- Be polite and when everything works out send the QUIT message. + local quit = function() + dorequest(socket, "QUIT\r\n") + socket:close() end - if (result == "TIMEOUT") then - socket:close() - return "Timeout. Try incresing settimeout, or enhance this." + -- Use the user provided options + if (nmap.registry.args["smtp-open-relay.domain"] ~= nil) then + domain = nmap.registry.args["smtp-open-relay.domain"] end --- close socket and return if there's an smtp status code != 250 - if not string.match(result, "^250") then - socket:close() - return "EHLO with errors or timeout. Enable --script-trace to see what is happening." + if (nmap.registry.args["smtp-open-relay.ip"] ~= nil) then + ip = nmap.registry.args["smtp-open-relay.ip"] end + + -- Try to connect to server + local response - mailservername = string.sub(result, string.find(result, '([.%w]+)',4)) + socket, response = comm.tryssl(host, port, string.format("EHLO %s\r\n", domain), options) --- read the rest of the response, if any + -- Failed connection attempt + if not socket then + return false, string.format("Couldn't establish connection on port %i", port.number) + end - while true do - status, result = socket:receive_lines(1) - if not status then - break - 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 --- Now that we have the mailservername, fill in the tor table - tor[0] = {f = "MAIL FROM:",t="RCPT TO:"} - tor[1] = {f = "MAIL FROM:<>",t="RCPT TO:"} - tor[2] = {f = "MAIL FROM:",t="RCPT TO:"} - tor[3] = {f = "MAIL FROM:",t="RCPT TO:"} - tor[4] = {f = "MAIL FROM:",t="RCPT TO:"} - tor[5] = {f = "MAIL FROM:",t="RCPT TO:"} - tor[6] = {f = "MAIL FROM:",t="RCPT TO:<\"relaytest@"..ourdomain.."\">"} - tor[7] = {f = "MAIL FROM:",t="RCPT TO:<\"relaytest%"..ourdomain.."\">"} - tor[8] = {f = "MAIL FROM:",t="RCPT TO:"} - tor[9] = {f = "MAIL FROM:",t="RCPT TO:<\"relaytest@"..ourdomain.."\"@[" .. host.ip .. "]>"} - tor[10] = {f = "MAIL FROM:",t="RCPT TO:"} - tor[11] = {f = "MAIL FROM:",t="RCPT TO:<@[" .. host.ip .. "]:relaytest@"..ourdomain..">"} - tor[12] = {f = "MAIL FROM:",t="RCPT TO:<@" .. mailservername .. ":relaytest@"..ourdomain..">"} - tor[13] = {f = "MAIL FROM:",t="RCPT TO:<"..ourdomain.."!relaytest>"} - tor[14] = {f = "MAIL FROM:",t="RCPT TO:<"..ourdomain.."!relaytest@[" .. host.ip .. "]>"} - tor[15] = {f = "MAIL FROM:",t="RCPT TO:<"..ourdomain.."!relaytest@" .. mailservername .. ">"} + -- Find out server name + local srvname = string.sub(response, string.find(response, '([.%w]+)', 4)) + local status = true - i = -1 - while true do - i = i+1 - if i > table.getn(tor) then break end + -- Read until end of response + while status do + status, response = socket:receive_lines(1) + end + + -- Antispam tests + local tests = { + { from = "MAIL FROM:<>", to = string.format("RCPT TO:", domain) }, + { from = string.format("MAIL FROM:", domain), to = string.format("RCPT TO:", domain) }, + { from = string.format("MAIL FROM:", srvname), to = string.format("RCPT TO:", domain) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:", domain) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:", domain, ip) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:", domain, srvname) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:<\"relaytest@%s\">", domain) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:<\"relaytest%%%s\">", domain) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:", domain, ip) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:<\"relaytest@%s\"@[%s]>", domain, ip) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:", domain, srvname) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:<@[%s]:relaytest@%s>", ip, domain) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:<@%s:relaytest@%s>", srvname, domain) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:<%s!relaytest>", domain) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:<%s!relaytest@[%s]>", domain, ip) }, + { from = string.format("MAIL FROM:", ip), to = string.format("RCPT TO:<%s!relaytest@%s>", domain, srvname) }, + } + + local combinations = {} + local index + + for index = 1, table.getn(tests), 1 do + local result, response = dorequest(socket, "RSET\r\n") --- for debugging, uncomment next line --- print (tor[i]["f"] .. " -> " .. tor[i]["t"]) + if not result then + return false, "Failed to issue RSET command" + end --- first, issue a RSET - socket:send("RSET\r\n") - status, result = socket:receive_lines(1) - if not string.match(result, "^250") then - socket:close() - return + -- If reset the envelope, doesn't work for one, wont work for others (critical command) + if not string.match(response, "^250") then + quit() + -- Check if server needs authentication + if string.match(response, "^530") then + return false, "Server isnt an open relay, authentication needed" + else + return false, "Unable to clear server envelope" + end end --- send MAIL FROM.... - socket:send(tor[i]["f"].."\r\n") - status, result = socket:receive_lines(1) - if string.match(result, "^250") then --- if we get a 250, then continue with RCPT TO: - socket:send(tor[i]["t"].."\r\n") - status, result = socket:receive_lines(1) - if string.match(result, "^250") then - socket:close() - return "OPEN RELAY found." + -- Lets try to issue MAIL FROM command + result, response = dorequest(socket, tests[index]["from"] .. "\r\n") + + -- If this command fails to be sent, then something went wrong with the connection + if not result then + return false, "Failed to issue MAIL FROM command" + end + + -- If MAIL FROM 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 (more + -- polite and will raise less red flags) + if string.match(response, "^530") then + quit() + return false, "Server isnt an open relay, authentication needed" + -- The command was accepted (otherwise, the script will step to the next test) + elseif string.match(response, "^250") then + -- Lets try to actually relay + result, response = dorequest(socket, tests[index]["to"] .. "\r\n") + + if not result then + return false, "Failed to issue RCPT TO command" end - end + + if string.match(response, "^530") then + quit() + return false, "Server isnt an open relay, authentication needed" + elseif string.match(response, "^250") then + -- Save the working from and to + table.insert(combinations, {from = tests[index]["from"], to = tests[index]["to"]}) + end + end end - socket:close() - return + quit() + return true, combinations end + +action = function(host, port) + local status, result = go(host, port) + + -- Something went wrong in the process, return the error message + if not status then + return stdnse.format_output(false, result) + end + + -- No combinations found + if #result == 0 then + return stdnse.format_output(false, "All tests failed, server doesn't seem to be an open relay") + end + + local message = {} + + -- Get all the combinations that worked + for i, combination in ipairs(result) do + table.insert(message, string.format("%s -> %s\n", combination.from, combination.to)) + end + + return stdnse.format_output(true, message) +end