From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- nselib/smtp.lua | 655 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 655 insertions(+) create mode 100644 nselib/smtp.lua (limited to 'nselib/smtp.lua') 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] = " 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", + }, + }, + ["DATA"] = { + cmd = "DATA", + success = { + [250] = "Requested mail action okay, completed", + [354] = "Start mail input; end with .", + }, + 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] = " 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] = " 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(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 CRLF 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 . 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; -- cgit v1.2.3