This commit is contained in:
Sweekar-cmd 2026-08-26 16:47:50 +08:00 committed by GitHub
commit 5a8ba063ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 441 additions and 237 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,136 +1,344 @@
<<<<<<< Updated upstream
local brute = require "brute"
local comm = require "comm"
local creds = require "creds"
local nmap = require "nmap"
local pop3 = require "pop3"
local shortport = require "shortport"
local string = require "string"
local stdnse = require "stdnse"
description = [[
Tries to log into a POP3 account by guessing usernames and passwords.
Automatically detects supported authentication mechanisms and upgrades
to TLS using STLS when available.
]]
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}, {"pop3", "pop3s"})
---
-- Choose best supported auth method from CAPA
local function choose_auth(capas)
if capas.AUTH then
local mechs = {}
for _, m in ipairs(capas.AUTH) do
mechs[m:upper()] = true
end
if mechs["CRAM-MD5"] then
return pop3.login_sasl_crammd5, false
elseif mechs["LOGIN"] then
return pop3.login_sasl_login, false
elseif mechs["PLAIN"] then
return pop3.login_sasl_plain, false
end
end
if capas.APOP then
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 The login method to use: <code>"USER"</code>
-- (default), <code>"SASL-PLAIN"</code>, <code>"SASL-LOGIN"</code>,
-- <code>"SASL-CRAM-MD5"</code>, or <code>"APOP"</code>. Defaults to <code>"USER"</code>,
-- @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-ported:
-- | Accounts:
-- | user:pass => Login correct
-- | Statistics:
-- |_ Performed 8 scans in 1 seconds, average tps: 8
author = {"Philip Pickering", "Piotr Olma"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
-- | 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"}
Driver = {
new = function(self, host, port, login_function, is_apop)
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)
self.__index = self
o.port = port
o.host = host
o.port = port
o.login_function = login_function
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,
-- Attempts to connect to the POP server
-- @return true on success
-- @return false, brute.Error object on failure
connect = function(self)
<<<<<<< Updated upstream
local opts = { timeout = 10000, recv_before = true }
local line
self.socket = brute.new_socket()
local opts = {timeout=10000, recv_before=true}
local best_opt, line, _
self.socket, _, best_opt, line = comm.tryssl(self.host, self.port, "" , opts)
-- 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
end
line = self.socket:receive_lines(1)
else
self.socket, _, _, line = comm.tryssl(self.host, self.port, "", opts)
end
if not self.socket then
local err = brute.Error:new("Failed to connect.")
err:setAbort(true)
return false, err
end --no connection
end
if not pop3.stat(line) then
local err = brute.Error:new("Failed to make a pop-connection.")
local err = brute.Error:new("Invalid POP3 greeting.")
err:setAbort(true)
return false, err
end -- no pop-connection
if self.is_apop then
self.additional = string.match(line, "<[%p%w]+>") --apop challenge
end
return true
end, --connect
-- Attempts to login to the POP server
--
-- @param username string containing the login username
-- @param password string containing the login password
-- @return status, true on success, false on failure
-- @return brute.Error object on failure
-- creds.Account object on success
login = function(self, username, password)
local pstatus
local perror
pstatus, perror = self.login_function(self.socket, username, password, self.additional)
if pstatus then
return true, creds.Account:new(username, password, creds.State.VALID)
else
local err
if (perror == pop3.err.pwError) then
err = brute.Error:new("Wrong password.")
elseif (perror == pop3.err.userError) then
err = brute.Error:new("Wrong username.")
err:setInvalidAccount(username)
else
err = brute.Error:new("Login failed.")
=======
local e = brute.Error:new("SSL connection failed.")
e:setAbort(true)
return false, e
end
return false, err
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
end, --login
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
end
local ssl = self.socket:sslhandshake()
if not ssl then
local err = brute.Error:new("TLS handshake failed.")
err:setAbort(true)
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
end,
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.")
elseif code == pop3.err.userError then
err = brute.Error:new("Wrong username.")
err:setInvalidAccount(username)
elseif code == pop3.err.OpenSSLMissing then
err = brute.Error:new("OpenSSL required for this authentication method.")
err:setAbort(true)
else
err = brute.Error:new("Login failed.")
end
return false, err
end,
disconnect = function(self)
self.socket:close()
end, --disconnect
if self.socket then
self.socket:close()
end
end,
check = function(self)
return true
end, --check
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
}
portrule = shortport.port_or_service({110, 995}, {"pop3","pop3s"})
action = function(host, port)
local pMeth = nmap.registry.args.pop3loginmethod
if (not pMeth) then pMeth = nmap.registry.pop3loginmethod end
if (not pMeth) then pMeth = "USER" end
--determine function we will use to login to server
local is_apop = false
local login_function
if (pMeth == "USER") then
login_function = pop3.login_user
elseif (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
local capas = pop3.capabilities(host, port)
if not capas then
return "Could not retrieve capabilities."
end
local engine = brute.Engine:new(Driver, host, port, login_function, is_apop)
<<<<<<< Updated upstream
local login_function, is_apop = choose_auth(capas)
local use_stls = capas.STLS and port.number == 110
local implicit_tls = (port.number == 995 or port.service == "pop3s")
if use_stls then
stdnse.print_debug(1, "POP3: Upgrading to TLS using STLS")
end
local engine = brute.Engine:new(
Driver,
host,
port,
login_function,
is_apop,
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 status, accounts = engine:start()
local _, accounts = engine:start()
return accounts
end