scripts/pop3-brute: add CAPA auth selection, STLS, and POP3S support

- Auto-detect auth method from CAPA (CRAM-MD5 > LOGIN > PLAIN > APOP > USER)
- Preserve pop3loginmethod arg as manual override for backward compat
- Driver.new accepts opts table matching brute.Engine calling convention
- brute.Engine:new receives single opts table, not positional args
- reconnect_ssl() replaces incorrect sslhandshake() after STLS
- STLS only attempted when server advertises it in CAPA (capas.STLS ~= nil)
- Implicit TLS support for POP3S on port 995
- Capture both return values from receive_lines() in implicit TLS path
- capas.SASL or capas.AUTH for SASL mechanism lookup
- APOP challenge pattern tightened to RFC 1939 format <id@host>
- Add @args doc block for pop3loginmethod script argument
- portrule extended to include port 1110
This commit is contained in:
Sweekar-cmd 2026-03-07 23:36:11 +05:45
parent 973e3a90e8
commit 035c0c150c
2 changed files with 314 additions and 153 deletions

View file

@ -1,190 +1,226 @@
---
-- POP3 functions.
-- POP3 helper functions for NSE scripts.
--
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
local base64 = require "base64"
local comm = require "comm"
local match = require "match"
local stdnse = require "stdnse"
local string = require "string"
local base64 = require "base64"
local comm = require "comm"
local match = require "match"
local stdnse = require "stdnse"
local stringaux = require "stringaux"
local table = require "table"
local string = string
local table = table
_ENV = stdnse.module("pop3", stdnse.seeall)
local HAVE_SSL, openssl = pcall(require,'openssl')
local HAVE_SSL, openssl = pcall(require, "openssl")
-- Error codes returned by login helpers.
-- Must remain module-level so scripts can access pop3.err.*
err = {
none = 0,
userError = 1,
pwError = 2,
none = 0,
userError = 1,
pwError = 2,
informationMissing = 3,
OpenSSLMissing = 4,
OpenSSLMissing = 4,
}
---
-- Check a POP3 response for <code>"+OK"</code>.
-- @param line First line returned from an POP3 request.
-- @return The string <code>"+OK"</code> if found or <code>nil</code> otherwise.
-- Check whether a POP3 response indicates success.
-- @param line POP3 response line.
-- @return true if response starts with "+OK", false otherwise.
function stat(line)
return string.match(line, "+OK")
return type(line) == "string" and line:match("^%+OK") ~= nil
end
---
-- Try to log in using the <code>USER</code>/<code>PASS</code> commands.
-- USER/PASS authentication.
-- @param socket Socket connected to POP3 server.
-- @param user User string.
-- @param user Username string.
-- @param pw Password string.
-- @return Status (true or false).
-- @return Error code if status is false.
-- @return status true on success, false on failure.
-- @return err Error code if status is false.
function login_user(socket, user, pw)
socket:send("USER " .. user .. "\r\n")
local status, line = socket:receive_lines(1)
if not stat(line) then return false, err.userError end
socket:send("PASS " .. pw .. "\r\n")
status, line = socket:receive_lines(1)
if stat(line) then return true, err.none
else return false, err.pwError
local _, line = socket:receive_lines(1)
if not stat(line) then
return false, err.userError
end
socket:send("PASS " .. pw .. "\r\n")
_, line = socket:receive_lines(1)
if stat(line) then
return true, err.none
end
return false, err.pwError
end
---
-- Try to login using the <code>AUTH</code> command using SASL/Plain method.
-- SASL PLAIN authentication.
-- @param socket Socket connected to POP3 server.
-- @param user User string.
-- @param user Username string.
-- @param pw Password string.
-- @return Status (true or false).
-- @return Error code if status is false.
-- @return status true on success, false on failure.
-- @return err Error code if status is false.
function login_sasl_plain(socket, user, pw)
local auth64 = base64.enc(user .. "\0" .. user .. "\0" .. pw)
socket:send("AUTH PLAIN " .. auth64 .. "\r\n")
local status, line = socket:receive_lines(1)
local _, line = socket:receive_lines(1)
if stat(line) then
return true, err.none
else
return false, err.pwError
end
return false, err.pwError
end
---
-- Try to login using the <code>AUTH</code> command using SASL/Login method.
-- @param user User string.
-- SASL LOGIN authentication.
-- @param socket Socket connected to POP3 server.
-- @param user Username string.
-- @param pw Password string.
-- @param pw String containing password to login.
-- @return Status (true or false).
-- @return Error code if status is false.
-- @return status true on success, false on failure.
-- @return err Error code if status is false.
function login_sasl_login(socket, user, pw)
local user64 = base64.enc(user)
local pw64 = base64.enc(pw)
socket:send("AUTH LOGIN\r\n")
local status, line = socket:receive_lines(1)
if not base64.dec(string.sub(line, 3)) == "User Name:" then
local _, line = socket:receive_lines(1)
if type(line) ~= "string" or line:sub(1, 1) ~= "+" then
return false, err.userError
end
socket:send(user64)
local status, line = socket:receive_lines(1)
if not base64.dec(string.sub(line, 3)) == "Password:" then
local prompt = base64.dec(line:sub(3)):lower()
if not prompt:find("user") then
return false, err.userError
end
socket:send(pw64)
socket:send(base64.enc(user) .. "\r\n")
_, line = socket:receive_lines(1)
if type(line) ~= "string" or line:sub(1, 1) ~= "+" then
return false, err.userError
end
local status, line = socket:receive_lines(1)
prompt = base64.dec(line:sub(3)):lower()
if not prompt:find("pass") then
return false, err.userError
end
socket:send(base64.enc(pw) .. "\r\n")
_, line = socket:receive_lines(1)
if stat(line) then
return true, err.none
else
return false, err.pwError
end
return false, err.pwError
end
---
-- Try to login using the <code>APOP</code> command.
-- APOP authentication (RFC 1939).
-- @param socket Socket connected to POP3 server.
-- @param user User string.
-- @param user Username string.
-- @param pw Password string.
-- @param challenge String containing challenge from POP3 server greeting.
-- @return Status (true or false).
-- @return Error code if status is false.
-- @param challenge APOP challenge string from the server greeting.
-- @return status true on success, false on failure.
-- @return err Error code if status is false.
function login_apop(socket, user, pw, challenge)
if type(challenge) ~= "string" then return false, err.informationMissing end
local apStr = stdnse.tohex(openssl.md5(challenge .. pw))
socket:send(("APOP %s %s\r\n"):format(user, apStr))
local status, line = socket:receive_lines(1)
if (stat(line)) then
return true, err.none
else
return false, err.pwError
if not HAVE_SSL then
return false, err.OpenSSLMissing
end
if type(challenge) ~= "string" then
return false, err.informationMissing
end
local digest = stdnse.tohex(openssl.md5(challenge .. pw))
socket:send(("APOP %s %s\r\n"):format(user, digest))
local _, line = socket:receive_lines(1)
if stat(line) then
return true, err.none
end
return false, err.pwError
end
---
-- Asks a POP3 server for capabilities.
--
-- See RFC 2449.
-- @param host Host to be queried.
-- @param port Port to connect to.
-- @return Table containing capabilities or nil on error.
-- @return nil or String error message.
function capabilities(host, port)
local socket, line, bopt, first_line = comm.tryssl(host, port, "" , {request_timeout=10000, recv_before=true})
if not socket then
return nil, "Could Not Connect"
-- SASL CRAM-MD5 authentication.
-- @param socket Socket connected to POP3 server.
-- @param user Username string.
-- @param pw Password string.
-- @return status true on success, false on failure.
-- @return err Error code if status is false.
function login_sasl_crammd5(socket, user, pw)
if not HAVE_SSL then
return false, err.OpenSSLMissing
end
if not stat(first_line) then
return nil, "No Response"
socket:send("AUTH CRAM-MD5\r\n")
local _, line = socket:receive_lines(1)
if type(line) ~= "string" or line:sub(1, 1) ~= "+" then
return false, err.pwError
end
local challenge = base64.dec(line:sub(3))
local digest = stdnse.tohex(openssl.hmac("md5", pw, challenge))
local auth = base64.enc(user .. " " .. digest)
socket:send(auth .. "\r\n")
_, line = socket:receive_lines(1)
if stat(line) then
return true, err.none
end
return false, err.pwError
end
---
-- Query POP3 server capabilities (RFC 2449).
-- @param host Host to query.
-- @param port Port to connect to.
-- @return capas Table of capabilities, or nil on error.
-- @return nil or error string on failure.
function capabilities(host, port)
local socket, _, _, greeting =
comm.tryssl(host, port, "", { recv_before = true })
if not socket then
return nil, "Could not connect"
end
if not stat(greeting) then
socket:close()
return nil, "Invalid POP3 greeting"
end
local capas = {}
if string.find(first_line, "<[%p%w]+>") then
-- APOP challenge must match <process-ID.clock@hostname> per RFC 1939
if greeting:find("<[^>]+@[^>]+>") then
capas.APOP = {}
end
local status = socket:send("CAPA\r\n")
if( not(status) ) then
return nil, "Failed to send"
end
status, line = socket:receive_buf(match.pattern_limit("%.", 2048), false)
if( not(status) ) then
return nil, "Failed to receive"
end
socket:send("CAPA\r\n")
local status, response =
socket:receive_buf(match.pattern_limit("%.\r?\n", 4096), false)
socket:close()
local lines = stringaux.strsplit("\r\n",line)
if not stat(table.remove(lines,1)) then
if not status then
return nil, "Failed to receive CAPA response"
end
-- Normalize line endings to handle both CRLF and LF-only servers
response = response:gsub("\r\n", "\n")
local lines = stringaux.strsplit("\n", response)
if not stat(table.remove(lines, 1)) then
capas.capa = false
return capas
end
for _, line in ipairs(lines) do
if ( line and #line>0 ) then
local capability = line:sub(line:find("[%w-]+"))
line = line:sub(#capability + 2)
if ( line ~= "" ) then
capas[capability] = stringaux.strsplit(" ", line)
else
capas[capability] = {}
for _, ln in ipairs(lines) do
if ln and #ln > 0 then
local name, args = ln:match("^(%S+)%s*(.*)")
if name then
capas[name] = args ~= "" and stringaux.strsplit(" ", args) or {}
end
end
end
@ -192,44 +228,4 @@ function capabilities(host, port)
return capas
end
---
-- Try to login using the <code>AUTH</code> command using SASL/CRAM-MD5 method.
-- @param socket Socket connected to POP3 server.
-- @param user User string.
-- @param pw Password string.
-- @return Status (true or false).
-- @return Error code if status is false.
function login_sasl_crammd5(socket, user, pw)
socket:send("AUTH CRAM-MD5\r\n")
local status, line = socket:receive_lines(1)
local challenge = base64.dec(string.sub(line, 3))
local digest = stdnse.tohex(openssl.hmac('md5', pw, challenge))
local authStr = base64.enc(user .. " " .. digest)
socket:send(authStr .. "\r\n")
local status, line = socket:receive_lines(1)
if stat(line) then
return true, err.none
else
return false, err.pwError
end
end
-- Overwrite functions requiring OpenSSL if we got no OpenSSL.
if not HAVE_SSL then
local no_ssl = function()
return false, err.OpenSSLMissing
end
login_apop = no_ssl
login_sasl_crammd5 = no_ssl
end
return _ENV;
return _ENV

View file

@ -1,3 +1,4 @@
<<<<<<< Updated upstream
local brute = require "brute"
local comm = require "comm"
local creds = require "creds"
@ -39,10 +40,63 @@ local function choose_auth(capas)
return pop3.login_apop, true
end
=======
local brute = require "brute"
local comm = require "comm"
local creds = require "creds"
local pop3 = require "pop3"
local shortport = require "shortport"
local stdnse = require "stdnse"
description = [[
Tries to log into a POP3 account by guessing usernames and passwords.
Automatically detects supported authentication mechanisms via CAPA and
upgrades to TLS using STLS when available on port 110. Supports implicit
TLS for POP3S (port 995). The auth method can be overridden manually via
the <code>pop3loginmethod</code> script argument.
]]
---
-- @args pop3loginmethod Override automatic auth selection. Valid values:
-- <code>"USER"</code>, <code>"SASL-PLAIN"</code>,
-- <code>"SASL-LOGIN"</code>, <code>"SASL-CRAM-MD5"</code>,
-- <code>"APOP"</code>. If not set, the best method is chosen from CAPA.
--
-- @output
-- PORT STATE SERVICE
-- 110/tcp open pop3
-- | pop3-brute:
-- | Accounts:
-- | admin:password - Valid credentials
-- | Statistics:
-- |_ Performed 101 guesses in 22 seconds, average tps: 4.5
author = {"Philip Pickering", "Piotr Olma", "Sweekar-cmd"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"intrusive", "brute"}
portrule = shortport.port_or_service({110, 995, 1110}, {"pop3", "pop3s"})
-- Select the best available auth method from CAPA capabilities.
-- Preference order: CRAM-MD5 > LOGIN > PLAIN > APOP > USER
local function choose_auth(capas)
local sasl = capas.SASL or capas.AUTH
if sasl then
local mechs = {}
for _, m in ipairs(sasl) do
mechs[m:upper()] = true
end
if mechs["CRAM-MD5"] then return pop3.login_sasl_crammd5, false end
if mechs["LOGIN"] then return pop3.login_sasl_login, false end
if mechs["PLAIN"] then return pop3.login_sasl_plain, false end
end
if capas.APOP then return pop3.login_apop, true end
>>>>>>> Stashed changes
return pop3.login_user, false
end
local Driver = {
<<<<<<< Updated upstream
new = function(self, host, port, login_function, is_apop, use_stls, implicit_tls)
local o = {}
setmetatable(o, self)
@ -53,18 +107,35 @@ local Driver = {
o.is_apop = is_apop
o.use_stls = use_stls
o.implicit_tls = implicit_tls
=======
new = function(self, host, port, opts)
local o = setmetatable({}, self)
self.__index = self
o.host = host
o.port = port
o.login_function = opts.login_function
o.is_apop = opts.is_apop
o.use_stls = opts.use_stls
o.implicit_tls = opts.implicit_tls
>>>>>>> Stashed changes
return o
end,
connect = function(self)
<<<<<<< Updated upstream
local opts = { timeout = 10000, recv_before = true }
local line
-- Implicit TLS (POP3S, usually port 995)
=======
local line
>>>>>>> Stashed changes
if self.implicit_tls then
self.socket = brute.new_socket()
local ok = self.socket:connect(self.host, self.port, "ssl")
if not ok then
<<<<<<< Updated upstream
local err = brute.Error:new("SSL connection failed.")
err:setAbort(true)
return false, err
@ -84,18 +155,48 @@ local Driver = {
local err = brute.Error:new("Invalid POP3 greeting.")
err:setAbort(true)
return false, err
=======
local e = brute.Error:new("SSL connection failed.")
e:setAbort(true)
return false, e
end
local _, resp = self.socket:receive_lines(1)
line = resp
else
self.socket, _, _, line =
comm.tryssl(self.host, self.port, "", { timeout = 10000, recv_before = true })
end
if not self.socket then
local e = brute.Error:new("Failed to connect.")
e:setAbort(true)
return false, e
end
if not pop3.stat(line) then
local e = brute.Error:new("Invalid POP3 greeting.")
e:setAbort(true)
return false, e
>>>>>>> Stashed changes
end
-- Extract APOP challenge
if self.is_apop then
<<<<<<< Updated upstream
self.additional = line:match("<[^>]+>")
end
-- Upgrade to TLS using STLS (only for port 110)
=======
self.additional = line:match("<[^>]+@[^>]+>")
end
>>>>>>> Stashed changes
if self.use_stls then
self.socket:send("STLS\r\n")
local _, resp = self.socket:receive_lines(1)
if not pop3.stat(resp) then
<<<<<<< Updated upstream
local err = brute.Error:new("STLS negotiation failed.")
err:setAbort(true)
return false, err
@ -108,6 +209,18 @@ local Driver = {
return false, err
end
self.socket = ssl
=======
local e = brute.Error:new("STLS negotiation failed.")
e:setAbort(true)
return false, e
end
local ok, ssl_err = self.socket:reconnect_ssl()
if not ok then
local e = brute.Error:new("TLS handshake failed: " .. (ssl_err or "unknown"))
e:setAbort(true)
return false, e
end
>>>>>>> Stashed changes
end
return true
@ -116,11 +229,15 @@ local Driver = {
login = function(self, username, password)
local ok, code =
self.login_function(self.socket, username, password, self.additional)
<<<<<<< Updated upstream
=======
>>>>>>> Stashed changes
if ok then
return true, creds.Account:new(username, password, creds.State.VALID)
end
<<<<<<< Updated upstream
local err
if code == pop3.err.pwError then
err = brute.Error:new("Wrong password.")
@ -146,6 +263,28 @@ local Driver = {
check = function(self)
return true
end,
=======
local e
if code == pop3.err.pwError then
e = brute.Error:new("Wrong password.")
elseif code == pop3.err.userError then
e = brute.Error:new("Wrong username.")
e:setInvalidAccount(username)
elseif code == pop3.err.OpenSSLMissing then
e = brute.Error:new("OpenSSL required for this authentication method.")
e:setAbort(true)
else
e = brute.Error:new("Login failed.")
end
return false, e
end,
disconnect = function(self)
if self.socket then self.socket:close() end
end,
check = function(self) return true end,
>>>>>>> Stashed changes
}
action = function(host, port)
@ -154,6 +293,7 @@ action = function(host, port)
return "Could not retrieve capabilities."
end
<<<<<<< Updated upstream
local login_function, is_apop = choose_auth(capas)
local use_stls = capas.STLS and port.number == 110
@ -172,6 +312,31 @@ action = function(host, port)
use_stls,
implicit_tls
)
=======
local login_function, is_apop
-- Manual override takes priority over auto-detection
local pMeth = stdnse.get_script_args("pop3loginmethod")
if pMeth then
if pMeth == "SASL-PLAIN" then login_function = pop3.login_sasl_plain
elseif pMeth == "SASL-LOGIN" then login_function = pop3.login_sasl_login
elseif pMeth == "SASL-CRAM-MD5" then login_function = pop3.login_sasl_crammd5
elseif pMeth == "APOP" then login_function = pop3.login_apop; is_apop = true
else login_function = pop3.login_user
end
else
login_function, is_apop = choose_auth(capas)
end
local implicit_tls = (port.number == 995 or port.service == "pop3s")
local use_stls = (port.number == 110 and not implicit_tls and capas.STLS ~= nil)
local engine = brute.Engine:new(Driver, host, port, {
login_function = login_function,
is_apop = is_apop,
use_stls = use_stls,
implicit_tls = implicit_tls,
})
>>>>>>> Stashed changes
engine.options.script_name = SCRIPT_NAME
local _, accounts = engine:start()