summaryrefslogtreecommitdiffstats
path: root/nselib/smtp.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/smtp.lua')
-rw-r--r--nselib/smtp.lua655
1 files changed, 655 insertions, 0 deletions
diff --git a/nselib/smtp.lua b/nselib/smtp.lua
new file mode 100644
index 0000000..f0d3fba
--- /dev/null
+++ b/nselib/smtp.lua
@@ -0,0 +1,655 @@
+---
+-- Simple Mail Transfer Protocol (SMTP) operations.
+--
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+-- @args smtp.domain The domain to be returned by get_domain, overriding the
+-- target's own domain name.
+
+local base64 = require "base64"
+local comm = require "comm"
+local sasl = require "sasl"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+_ENV = stdnse.module("smtp", stdnse.seeall)
+
+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] = "<domain> 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] = "<domain> 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] = "<domain> 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 <forward-path>",
+ },
+ 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] = "<domain> does not accept mail [rfc1846]",
+ [421] = "<domain> Service not available, closing transmission channel",
+ },
+ },
+ ["DATA"] = {
+ cmd = "DATA",
+ success = {
+ [250] = "Requested mail action okay, completed",
+ [354] = "Start mail input; end with <CRLF>.<CRLF>",
+ },
+ errors = {
+ [451] = "Requested action aborted: local error in processing",
+ [554] = "Transaction failed",
+ [500] = "Syntax error, command unrecognised",
+ [501] = "Syntax error in parameters or arguments",
+ [503] = "Bad sequence of commands",
+ [421] = "<domain> Service not available, closing transmission channel",
+ [552] = "Requested mail action aborted: exceeded storage allocation",
+ [554] = "Transaction failed",
+ [451] = "Requested action aborted: local error in processing",
+ [452] = "Requested action not taken: insufficient system storage",
+ },
+ },
+ ["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] = "<domain> Service not available, closing transmission channel",
+ },
+ },
+ ["VRFY"] = {
+ cmd = "VRFY",
+ success = {
+ [250] = "Requested mail action okay, completed",
+ [251] = "User not local; will forward to <forward-path>",
+ },
+ 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 <forward-path>",
+ [553] = "Requested action not taken: mailbox name not allowed",
+ [421] = "<domain> 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] = "<domain> 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
+-- <code>smtp.domain</code> 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(stringaux.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.debug3(
+ "SMTP: check_smtp_reply failed: %s not supported", cmd)
+ return false, string.format("SMTP: %s %s", cmd, reply)
+ end
+ end
+ stdnse.debug3(
+ "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.
+-- This function automatically adds <code>CRLF<code> at the end.
+--
+-- @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.debug3("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.debug3("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)
+ local socket, _, ret
+ if opts.ssl then
+ socket, _, _, ret = comm.tryssl(host, port, '', opts)
+ else
+ socket, _, ret = comm.opencon(host, port, nil, opts)
+ end
+ if not socket then
+ return socket, (ERROR_MESSAGES[ret] or 'unspecified error')
+ end
+ return socket, ret
+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 success, 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 RCPT 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 recipient.
+-- @return true on success, or false on failures.
+-- @return response returned by the SMTP server on success, or an
+-- error message on failures.
+recipient = function(socket, address)
+ local st, ret, response
+
+ st, response = query(socket, "RCPT",
+ string.format("TO:<%s>", address))
+
+ if not st then
+ return st, response
+ end
+
+ st, ret = check_reply("RCPT", response)
+ if not st then
+ quit(socket)
+ return st, ret
+ end
+
+ return st, response
+end
+
+--- Sends data to the SMTP server.
+--
+-- This function will automatically adds <code><CRLF>.<CRLF></code> at the
+-- end. On network errors or if the SMTP command fails, the connection
+-- will be closed and the socket cleared.
+--
+-- @param socket connected to server.
+-- @param data to be sent.
+-- @return true on success, or false on failures.
+-- @return response returned by the SMTP server on success, or an
+-- error message on failures.
+datasend = function(socket, data)
+ local st, ret, response
+
+ st, response = query(socket, "DATA")
+ if not st then
+ return st, response
+ end
+
+ st, ret = check_reply("DATA", response)
+ if not st then
+ quit(socket)
+ return st, ret
+ end
+
+ if data then
+ st, response = query(socket, data.."\r\n.")
+ if not st then
+ return st, response
+ end
+
+ st, ret = check_reply("DATA", response)
+ if not st then
+ quit(socket)
+ return st, ret
+ end
+ 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.debug3("SMTP: sending 'QUIT'.")
+ socket:send("QUIT\r\n")
+ socket:close()
+end
+
+--- Attempts to authenticate with the SMTP server. The supported authentication
+-- mechanisms are: LOGIN, PLAIN, CRAM-MD5, DIGEST-MD5 and NTLM.
+--
+-- @param socket connected to server.
+-- @param username SMTP username.
+-- @param password SMTP password.
+-- @param mech Authentication mechanism.
+-- @return true on success, or false on failures.
+-- @return response returned by the SMTP server on success, or an
+-- error message on failures.
+
+login = function(socket, username, password, mech)
+ assert(mech == "LOGIN" or mech == "PLAIN" or mech == "CRAM-MD5"
+ or mech == "DIGEST-MD5" or mech == "NTLM",
+ ("Unsupported authentication mechanism (%s)"):format(mech or "nil"))
+ local status, response = query(socket, "AUTH", mech)
+ if ( not(status) ) then
+ return false, "ERROR: Failed to send AUTH to server"
+ end
+
+ if ( mech == "LOGIN" ) then
+ local tmp = response:match("334 (.*)")
+ if ( not(tmp) ) then
+ return false, "ERROR: Failed to decode LOGIN response"
+ end
+ tmp = base64.dec(tmp):lower()
+ if ( not(tmp:match("^username")) ) then
+ return false, ("ERROR: Expected \"Username\", but received (%s)"):format(tmp)
+ end
+ status, response = query(socket, base64.enc(username))
+ if ( not(status) ) then
+ return false, "ERROR: Failed to read LOGIN response"
+ end
+ tmp = response:match("334 (.*)")
+ if ( not(tmp) ) then
+ return false, "ERROR: Failed to decode LOGIN response"
+ end
+ tmp = base64.dec(tmp):lower()
+ if ( not(tmp:match("^password")) ) then
+ return false, ("ERROR: Expected \"password\", but received (%s)"):format(tmp)
+ end
+ status, response = query(socket, base64.enc(password))
+ if ( not(status) ) then
+ return false, "ERROR: Failed to read LOGIN response"
+ end
+ if ( response:match("^235") ) then
+ return true, "Login success"
+ end
+ return false, response
+ end
+
+
+ if ( mech == "NTLM" ) then
+ -- sniffed of the wire, seems to always be the same
+ -- decodes to some NTLMSSP blob greatness
+ status, response = query(socket, "TlRMTVNTUAABAAAAB7IIogYABgA3AAAADwAPACgAAAAFASgKAAAAD0FCVVNFLUFJUi5MT0NBTERPTUFJTg==")
+ if ( not(status) ) then return false, "ERROR: Failed to receive NTLM challenge" end
+ end
+
+
+ local chall = response:match("^334 (.*)")
+ chall = (chall and base64.dec(chall))
+ if (not(chall)) then return false, "ERROR: Failed to retrieve challenge" end
+
+ -- All mechanisms expect username and pass
+ -- add the otheronce for those who need them
+ local mech_params = { username, password, chall, "smtp" }
+ local auth_data = sasl.Helper:new(mech):encode(table.unpack(mech_params))
+ auth_data = base64.enc(auth_data)
+
+ status, response = query(socket, auth_data)
+ if ( not(status) ) then
+ return false, ("ERROR: Failed to authenticate using SASL %s"):format(mech)
+ end
+
+ if ( mech == "DIGEST-MD5" ) then
+ local rspauth = response:match("^334 (.*)")
+ if ( rspauth ) then
+ rspauth = base64.dec(rspauth)
+ status, response = query(socket,"")
+ end
+ end
+
+ if ( response:match("^235") ) then return true, "Login success" end
+
+ return false, response
+end
+
+return _ENV;