From ee7e069e634ac96b9bb9604230d5b9507cfa0e10 Mon Sep 17 00:00:00 2001 From: patrik Date: Thu, 21 Jul 2011 06:16:20 +0000 Subject: [PATCH] o [NSE] Added the script smtp-brute that performs brute force password auditing against SMTP servers. [Patrik] o [NSE] Updated SMTP library to support authentication using both plain-text and the SASL library. [Patrik] --- CHANGELOG | 6 ++ nselib/smtp.lua | 79 ++++++++++++++++++++++++ scripts/script.db | 1 + scripts/smtp-brute.nse | 136 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 scripts/smtp-brute.nse diff --git a/CHANGELOG b/CHANGELOG index ef42fd3bd..c6f651ad7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,11 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the script smtp-brute that performs brute force password + auditing against SMTP servers. [Patrik] + +o [NSE] Updated SMTP library to support authentication using both plain-text + and the SASL library. [Patrik] + o [NSE] Added the script imap-brute that performs brute force password auditing against IMAP servers. [Patrik] diff --git a/nselib/smtp.lua b/nselib/smtp.lua index 688b96444..4e00d5ad0 100644 --- a/nselib/smtp.lua +++ b/nselib/smtp.lua @@ -8,6 +8,8 @@ module(... or "smtp", package.seeall) local comm = require 'comm' local nmap = require 'nmap' local stdnse = require 'stdnse' +local base64 = require 'base64' +local sasl = require 'sasl' local ERROR_MESSAGES = { ["EOF"] = "connection closed", @@ -580,3 +582,80 @@ quit = function(socket) socket:send("QUIT\r\n") socket:close() end + +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 receieve 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(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 diff --git a/scripts/script.db b/scripts/script.db index 7327d950a..2b7362ac2 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -184,6 +184,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-brute.nse", categories = { "brute", "intrusive", } } 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-brute.nse b/scripts/smtp-brute.nse new file mode 100644 index 000000000..39484197f --- /dev/null +++ b/scripts/smtp-brute.nse @@ -0,0 +1,136 @@ +description = [[ +Performs password guessing against SMTP servers using either LOGIN, PLAIN, +CRAM-MD5, DIGEST-MD5 or NTLM authentication. +]] + +--- +-- @usage +-- nmap -p 25 --script smtp-brute +-- +-- @output +-- PORT STATE SERVICE REASON +-- 25/tcp open stmp syn-ack +-- | smtp-brute: +-- | Accounts +-- | braddock:jules - Account is valid +-- | lane:sniper - Account is valid +-- | parker:scorpio - Account is valid +-- | Statistics +-- |_ Performed 1160 guesses in 41 seconds, average tps: 33 +-- +-- @args smtp-brute.auth authentication mechanism to use LOGIN, PLAIN, +-- CRAM-MD5, DIGEST-MD5 or NTLM + +-- Version 0.1 +-- Created 07/15/2011 - v0.1 - created by Patrik Karlsson + +require 'creds' +require 'brute' +require 'shortport' +require 'smtp' + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"brute", "intrusive"} + +portrule = shortport.port_or_service({ 25, 465, 587 }, + { "smtp", "smtps", "submission" }) + +local mech + +-- By using this connectionpool we don't need to reconnect the socket +-- for each attempt. +ConnectionPool = {} + +Driver = +{ + + -- Creates a new driver instance + -- @param host table as received by the action method + -- @param port table as received by the action method + -- @param pool an instance of the ConnectionPool + new = function(self, host, port) + local o = { host = host, port = port } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Connects to the server (retrieves a connection from the pool) + connect = function( self ) + self.socket = ConnectionPool[coroutine.running()] + if ( not(self.socket) ) then + self.socket = smtp.connect(self.host, self.port, { ssl = true, recv_before = true }) + if ( not(self.socket) ) then return false end + ConnectionPool[coroutine.running()] = self.socket + end + return true + end, + + -- Attempts to login to the server + -- @param username string containing the username + -- @param password string containing the password + -- @return status true on success, false on failure + -- @return brute.Error on failure and brute.Account on success + login = function( self, username, password ) + local status, err = smtp.login( self.socket, username, password, mech ) + if ( status ) then + smtp.quit(self.socket) + ConnectionPool[coroutine.running()] = nil + return true, brute.Account:new(username, password, creds.State.VALID) + end + if ( err:match("^ERROR: Failed to .*") ) then + self.socket:close() + ConnectionPool[coroutine.running()] = nil + local err = brute.Error:new( err ) + -- This might be temporary, set the retry flag + err:setRetry( true ) + return false, err + end + return false, brute.Error:new( "Incorrect password" ) + end, + + -- Disconnects from the server (release the connection object back to + -- the pool) + disconnect = function( self ) + return true + end, + +} + + +action = function(host, port) + + local socket, response = smtp.connect(host, port, { ssl = true, recv_before = true }) + if ( not(socket) ) then return "\n ERROR: Failed to connect to SMTP server" end + local status, response = smtp.ehlo(socket, smtp.get_domain(host)) + if ( not(status) ) then return "\n ERROR: EHLO command failed, aborting ..." end + local mechs = smtp.get_auth_mech(response) + if ( not(mechs) ) then + return "\n ERROR: Failed to retrieve authentication mechanisms form server" + end + smtp.quit(socket) + + local mech_prio = stdnse.get_script_args("smtp-brute.auth") + mech_prio = ( mech_prio and { mech_prio } ) or + { "LOGIN", "PLAIN", "CRAM-MD5", "DIGEST-MD5", "NTLM" } + + for _, mp in ipairs(mech_prio) do + for _, m in pairs(mechs) do + if ( mp == m ) then + mech = m + break + end + end + if ( mech ) then break end + end + + local engine = brute.Engine:new(Driver, host, port) + + engine.options.script_name = SCRIPT_NAME + status, result = engine:start() + + for _, sock in pairs(ConnectionPool) do sock:close() end + + return result +end \ No newline at end of file