Index: scripts/smtp-open-relay.nse =================================================================== --- scripts/smtp-open-relay.nse (revision 16660) +++ scripts/smtp-open-relay.nse (working copy) @@ -1,114 +1,217 @@ 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 domain=,ip=
] -p 25,465,587 +-- +-- @output +-- Host script results: +-- |_ smtp-open-relay: Server isnt an open relay, authentication needed +-- +-- @args domain Define the domain to be used in the anti-spam tests (default is nmap.scanme.org) +-- @args 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" +----------------------------------------------------------------------- -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 + local failed = try(socket:send(request)) + + if failed then + return false + end + + local response = try(socket:receive_lines(1)) + + return true, response +end + +function go(host, port) + local domain = "nmap.scanme.org" + local ip = host.ip + local status = true 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" + -- Exception handling + local catch = function() + socket:close() end - if (result == "TIMEOUT") then - socket:close() - return "Timeout. Try incresing settimeout, or enhance this." + local try = nmap.new_try(catch) + + -- Use the user provided domain if exists + if (nmap.registry.args.domain ~= nil) then + domain = nmap.registry.args.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." + -- Use the user provided ip if exists + if (nmap.registry.args.ip ~= nil) then + ip = nmap.registry.args.ip end + + -- Try to connect to server + local response - mailservername = string.sub(result, string.find(result, '([.%w]+)',4)) + socket, response = comm.tryssl(host, port, "EHLO " .. domain .."\r\n", options) --- read the rest of the response, if any + -- Failed connection attempt + if not socket then + return false, "Couldn't establish connection on port " .. 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 + socket:close() + 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)) + -- Read until end of response + while status do + status, response = socket:receive_lines(1) + end + + -- Antispam tests + local tests = { + { from = "MAIL FROM:", to = "RCPT TO:" }, + { from = "MAIL FROM:<>", to = "RCPT TO:" }, + { from = "MAIL FROM:", to = "RCPT TO:" }, + { from = "MAIL FROM:", to = "RCPT TO:" }, + { from = "MAIL FROM:", to = "RCPT TO:" }, + { from = "MAIL FROM:", to = "RCPT TO:" }, + { from = "MAIL FROM:", to = "RCPT TO:<\"relaytest@" .. domain .. "\">" }, + { from = "MAIL FROM:", to = "RCPT TO:<\"relaytest%" .. domain .. "\">" }, + { from = "MAIL FROM:", to = "RCPT TO:" }, + { from = "MAIL FROM:", to = "RCPT TO:<\"relaytest@" .. domain .. "\"@[" .. ip .. "]>" }, + { from = "MAIL FROM:", to = "RCPT TO:" }, + { from = "MAIL FROM:", to = "RCPT TO:<@[" .. ip .. "]:relaytest@" .. domain .. ">" }, + { from = "MAIL FROM:", to = "RCPT TO:<@" .. srvname .. ":relaytest@" .. domain .. ">" }, + { from = "MAIL FROM:", to = "RCPT TO:<" .. domain .. "!relaytest>" }, + { from = "MAIL FROM:", to = "RCPT TO:<" .. domain .. "!relaytest@[" .. ip .. "]>" }, + { from = "MAIL FROM:", to = "RCPT TO:<" .. domain .. "!relaytest@" .. srvname .. ">" }, + } + + local index + + for index = 1, table.getn(tests), 1 do + local result, response = dorequest(socket, "RSET\r\n") - i = -1 - while true do - i = i+1 - if i > table.getn(tor) then break end + if not result then + socket:close() + return false, "Failed to issue RSET command" + end --- for debugging, uncomment next line --- print (tor[i]["f"] .. " -> " .. tor[i]["t"]) + -- If reset the envelope, doesn't work for one, wont work for others (critical command) + if not string.match(response, "^250") then + socket:close() --- first, issue a RSET - socket:send("RSET\r\n") - status, result = socket:receive_lines(1) - if not string.match(result, "^250") then + -- Check if server needs authentication + if string.match(response, "^530") then + return true, "Server isnt an open relay, authentication needed" + else + return true, "Unable to clear server envelope" + end + end + + -- 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 socket:close() - return + return false, "Failed to issue MAIL FROM command" 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 + -- 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 + 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 socket:close() - return "OPEN RELAY found." + return false, "Failed to issue RCPT TO command" end - end + + if string.match(response, "^530") then + socket:close() + return true, "Server isnt an open relay, authentication needed" + elseif string.match(response, "^250") then + socket:close() + return true, "Server is an open relay" + end + end end socket:close() - return + return true, "All tests failed, server doesnt seem to be an open relay" end + +action = function(host, port) + local status, message = go(host, port) + + if (status == false) then + if (nmap.debugging() > 0) then + return "ERROR: " .. message .. ", enable --script-trace to see what is happening" + else + return nil + end + end + + return message +end