diff --git a/nselib/smtp.lua b/nselib/smtp.lua new file mode 100644 index 000000000..0571cd77d --- /dev/null +++ b/nselib/smtp.lua @@ -0,0 +1,492 @@ +--- +-- Simple Mail Transfer Protocol (SMTP) operations. +-- +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html + +module(... or "smtp", package.seeall) + +local comm = require 'comm' +local nmap = require 'nmap' +local stdnse = require 'stdnse' + +local ERROR_MESSAGES = { + ["EOF"] = "connection closed", + ["TIMEOUT"] = "connection timeout", + ["ERROR"] = "failed to receive data" +} + +local SMTP_CMD = { + ["EHLO"] = { + cmd = "EHLO", + success = { + [250] = "Requested mail action okay, completed", + }, + errors = { + [421] = " Service not available, closing transmission channel", + [500] = "Syntax error, command unrecognised", + [501] = "Syntax error in parameters or arguments", + [504] = "Command parameter not implemented", + [550] = "Not implemented", + }, + }, + ["HELP"] = { + cmd = "HELP", + success = { + [211] = "System status, or system help reply", + [214] = "Help message", + }, + errors = { + [500] = "Syntax error, command unrecognised", + [501] = "Syntax error in parameters or arguments", + [502] = "Command not implemented", + [504] = "Command parameter not implemented", + [421] = " Service not available, closing transmission channel", + }, + }, + ["AUTH"] = { + cmd = "AUTH", + success = {[334] = ""}, + errors = { + [501] = "Authentication aborted", + }, + }, + ["MAIL"] = { + cmd = "MAIL", + success = { + [250] = "Requested mail action okay, completed", + }, + errors = { + [451] = "Requested action aborted: local error in processing", + [452] = "Requested action not taken: insufficient system storage", + [500] = "Syntax error, command unrecognised", + [501] = "Syntax error in parameters or arguments", + [421] = " Service not available, closing transmission channel", + [552] = "Requested mail action aborted: exceeded storage allocation", + }, + }, + ["RCPT"] = { + cmd = "RCPT", + success = { + [250] = "Requested mail action okay, completed", + [251] = "User not local; will forward to ", + }, + errors = { + [450] = "Requested mail action not taken: mailbox unavailable", + [451] = "Requested action aborted: local error in processing", + [452] = "Requested action not taken: insufficient system storage", + [500] = "Syntax error, command unrecognised", + [501] = "Syntax error in parameters or arguments", + [503] = "Bad sequence of commands", + [521] = " does not accept mail [rfc1846]", + [421] = " Service not available, closing transmission channel", + }, + }, + ["STARTTLS"] = { + cmd = "STARTTLS", + success = { + [220] = "Ready to start TLS" + }, + errors = { + [501] = "Syntax error (no parameters allowed)", + [454] = "TLS not available due to temporary reason", + }, + }, + ["RSET"] = { + cmd = "RSET", + success = { + [200] = "nonstandard success response, see rfc876)", + [250] = "Requested mail action okay, completed", + }, + errors = { + [500] = "Syntax error, command unrecognised", + [501] = "Syntax error in parameters or arguments", + [504] = "Command parameter not implemented", + [421] = " Service not available, closing transmission channel", + }, + }, + ["VRFY"] = { + cmd = "VRFY", + success = { + [250] = "Requested mail action okay, completed", + [251] = "User not local; will forward to ", + }, + errors = { + [500] = "Syntax error, command unrecognised", + [501] = "Syntax error in parameters or arguments", + [502] = "Command not implemented", + [504] = "Command parameter not implemented", + [550] = "Requested action not taken: mailbox unavailable", + [551] = "User not local; please try ", + [553] = "Requested action not taken: mailbox name not allowed", + [421] = " Service not available, closing transmission channel", + }, + }, + ["EXPN"] = { + cmd = "EXPN", + success = { + [250] = "Requested mail action okay, completed", + }, + errors = { + [550] = "Requested action not taken: mailbox unavailable", + [500] = "Syntax error, command unrecognised", + [501] = "Syntax error in parameters or arguments", + [502] = "Command not implemented", + [504] = "Command parameter not implemented", + [421] = " Service not available, closing transmission channel", + }, + }, +} + +-- Returns a domain to be used in the SMTP commands that need it. If the +-- user specified one through the script argument smtp.domain +-- 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 return the nmap.scanme.org by default. +-- +-- @param host The host table +-- @return The hostname to be used by the different SMTP commands. +get_domain = function(host) + local nmap_domain = "nmap.scanme.org" + + -- Use the user provided options. + local result = stdnse.get_script_args("smtp.domain") + if not result then + if type(host) == "table" then + if host.targetname then + result = host.targetname + elseif (host.name and #host.name ~= 0) then + result = host.name + end + end + end + + return result or nmap_domain +end + +--- Gets the authentication mechanisms that are listed in the response +-- of the client's EHLO command. +-- +-- @param response The response of the client's EHLO command. +-- @return An array of authentication mechanisms on success, or nil +-- when it can't find authentication. +get_auth_mech = function(response) + local list = {} + + for _, line in pairs(stdnse.strsplit("\r?\n", response)) do + local authstr = line:match("%d+\-AUTH%s(.*)$") + if authstr then + for mech in authstr:gmatch("[^%s]+") do + table.insert(list, mech) + end + return list + end + end + + return nil +end + +--- Checks the SMTP server reply to see if it supports the previously +-- sent SMTP command. +-- +-- @param cmd The SMTP command that was sent to the server +-- @param reply The SMTP server reply +-- @return true if the reply indicates that the SMTP command was +-- processed by the server correctly, or false on failures. +-- @return message The reply returned by the server on success, or an +-- error message on failures. +check_reply = function(cmd, reply) + local code, msg = string.match(reply, "^([0-9]+)%s*") + if code then + cmd = cmd:upper() + code = tonumber(code) + if SMTP_CMD[cmd] then + if SMTP_CMD[cmd].success[code] then + return true, reply + end + else + stdnse.print_debug(3, + "SMTP: check_smtp_reply failed: %s not supported", cmd) + return false, string.format("SMTP: %s %s", cmd, reply) + end + end + stdnse.print_debug(3, + "SMTP: check_smtp_reply failed: %s %s", cmd, reply) + return false, string.format("SMTP: %s %s", cmd, reply) +end + + +--- Queries the SMTP server for a specific service. +-- +-- This is a low level function that can be used to have more control +-- over the data exchanged. On network errors the socket will be closed. +-- +-- @param socket connected to the server +-- @param cmd The SMTP cmd to send to the server +-- @param data The data to send to the server +-- @param lines The minimum number of lines to receive, default value: 1. +-- @return true on success, or nil on failures. +-- @return response The returned response from the server on success, or +-- an error message on failures. +query = function(socket, cmd, data, lines) + if data then + cmd = cmd.." "..data + end + + local st, ret = socket:send(string.format("%s\r\n", cmd)) + if not st then + socket:close() + stdnse.print_debug(3, "SMTP: failed to send %s request.", cmd) + return st, string.format("SMTP failed to send %s request.", cmd) + end + + st, ret = socket:receive_lines(lines or 1) + if not st then + socket:close() + stdnse.print_debug(3, "SMTP %s: failed to receive data: %s.", + cmd, (ERROR_MESSAGES[ret] or 'unspecified error')) + return st, string.format("SMTP %s: failed to receive data: %s", + cmd, (ERROR_MESSAGES[ret] or 'unspecified error')) + end + + return st, ret +end + +--- Connects to the SMTP server based on the provided options. +-- +-- @param host The host table +-- @param port The port table +-- @param opts The connection option table, possible options: +-- ssl: try to connect using TLS +-- timeout: generic timeout value +-- recv_before: receive data before returning +-- lines: a minimum number of lines to receive +-- @return socket The socket descriptor, or nil on errors +-- @return response The response received on success and when +-- the recv_before is set, or the error message on failures. +connect = function(host, port, opts) + if opts.ssl then + local socket, _, _, ret = comm.tryssl(host, port, '', opts) + if not socket then + return socket, (ERROR_MESSAGES[ret] or 'unspecified error') + end + return socket, ret + else + local timeout, recv, lines + local socket = nmap.new_socket() + + if opts then + recv = opts.recv_before + timeout = opts.timeout + lines = opts.lines + end + socket:set_timeout(timeout or 8000) + + local st, ret = socket:connect(host, port, port.protocol) + if not st then + socket:close() + return st, (ERROR_MESSAGES[ret] or 'unspecified error') + end + + if recv then + st, ret = socket:receive_lines(lines or 1) + if not st then + socket:close() + return st, (ERROR_MESSAGES[ret] or 'unspecified error') + end + end + + return socket, ret + end +end + +--- Switches the plain text connection to be protected by the TLS protocol +-- by using the SMTP STARTTLS command. +-- +-- The socket will be reconnected by using SSL. On network errors or if the +-- SMTP command fails, the connection will be closed and the socket cleared. +-- +-- @param socket connected to server. +-- @return true on success, or nil on failures. +-- @return message On success this will contain the SMTP server response +-- to the client's STARTTLS command, or an error message on failures. +starttls = function(socket) + local st, reply, ret + + st, reply = query(socket, "STARTTLS") + if not st then + return st, reply + end + + st, ret = check_reply('STARTTLS', reply) + if not st then + quit(socket) + return st, ret + end + + st, ret = socket:reconnect_ssl() + if not st then + socket:close() + return st, ret + end + + return true, reply +end + +--- Sends the EHLO command to the SMTP server. +-- +-- On network errors or if the SMTP command fails, the connection +-- will be closed and the socket cleared. +-- +-- @param socket connected to server +-- @param domain to use in the EHLO command. +-- @return true on sucess, or false on failures. +-- @return response returned by the SMTP server on success, or an +-- error message on failures. +ehlo = function(socket, domain) + local st, ret, response + st, response = query(socket, "EHLO", domain) + if not st then + return st, response + end + + st, ret = check_reply("EHLO", response) + if not st then + quit(socket) + return st, ret + end + + return st, response +end + +--- Sends the HELP command to the SMTP server. +-- +-- On network errors or if the SMTP command fails, the connection +-- will be closed and the socket cleared. +-- +-- @param socket connected to server +-- @return true on success, or false on failures. +-- @return response returned by the SMTP server on success, or an +-- error message on failures. +help = function(socket) + local st, ret, response + st, response = query(socket, "HELP") + + if not st then + return st, response + end + + st, ret = check_reply("HELP", response) + if not st then + quit(socket) + return st, ret + end + + return st, response +end + +--- Sends the MAIL command to the SMTP server. +-- +-- On network errors or if the SMTP command fails, the connection +-- will be closed and the socket cleared. +-- +-- @param socket connected to server. +-- @param address of the sender. +-- @param esmtp_opts The additional ESMTP options table, possible values: +-- size: a decimal value to represent the message size in octets. +-- ret: include the message in the DSN, should be 'FULL' or 'HDRS'. +-- envid: envelope identifier, printable characters that would be +-- transmitted along with the message and included in the +-- failed DSN. +-- transid: a globally unique case-sensitive value that identifies +-- this particular transaction. +-- @return true on success, or false on failures. +-- @return response returned by the SMTP server on success, or an +-- error message on failures. +mail = function(socket, address, esmtp_opts) + local st, ret, response + + if esmtp_opts and next(esmtp_opts) then + local data = "" + -- we do not check for strange values, read the NSEDoc. + for k,v in pairs(esmtp_opts) do + k = k:upper() + data = string.format("%s %s=%s", data, k, v) + end + st, response = query(socket, "MAIL", + string.format("FROM:<%s>%s", + address, data)) + else + st, response = query(socket, "MAIL", + string.format("FROM:<%s>", address)) + end + + if not st then + return st, response + end + + st, ret = check_reply("MAIL", response) + if not st then + quit(socket) + return st, ret + end + + return st, response +end + +--- Sends the RSET command to the SMTP server. +-- +-- On network errors or if the SMTP command fails, the connection +-- will be closed and the socket cleared. +-- +-- @param socket connected to server. +-- @return true on success, or false on failures. +-- @return response returned by the SMTP server on success, or an +-- error message on failures. +reset = function(socket) + local st, ret, response + st, response = query(socket, "RSET") + + if not st then + return st, response + end + + st, ret = check_reply("RSET", response) + if not st then + quit(socket) + return st, ret + end + + return st, response +end + +--- Sends the VRFY command to verify the validity of a mailbox. +-- +-- On network errors or if the SMTP command fails, the connection +-- will be closed and the socket cleared. +-- +-- @param socket connected to server. +-- @param mailbox to verify. +-- @return true on success, or false on failures. +-- @return response returned by the SMTP server on success, or an +-- error message on failures. +verify = function(socket, mailbox) + local st, ret, response + st, response = query(socket, "VRFY", mailbox) + + st, ret = check_reply("VRFY", response) + if not st then + quit(socket) + return st, ret + end + + return st, response +end + +--- Sends the QUIT command to the SMTP server, and closes the socket. +-- +-- @param socket connected to server. +quit = function(socket) + stdnse.print_debug(3, "SMTP: sending 'QUIT'.") + socket:send("QUIT\r\n") + socket:close() +end