diff --git a/nselib/pop3.lua b/nselib/pop3.lua index 179bd5266..a6ca73da5 100644 --- a/nselib/pop3.lua +++ b/nselib/pop3.lua @@ -1,235 +1,194 @@ --- --- POP3 functions. +-- POP3 helper functions for NSE scripts. +-- +-- @copyright Same as Nmap +-- See https://nmap.org/book/man-legal.html -- --- @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") - -err = { - none = 0, - userError = 1, - pwError = 2, - informationMissing = 3, - OpenSSLMissing = 4, +-- Error codes returned by login helpers +local err = { + none = 0, + userError = 1, + pwError = 2, + informationMissing = 3, + OpenSSLMissing = 4, } --- --- Check a POP3 response for "+OK". --- @param line First line returned from an POP3 request. --- @return The string "+OK" if found or nil otherwise. -function stat(line) - return string.match(line, "+OK") +-- Check whether a POP3 response indicates success. +-- @param line POP3 response line +-- @return true if response starts with "+OK" +local function stat(line) + return type(line) == "string" and line:match("^%+OK") end - - --- --- Try to log in using the USER/PASS commands. --- @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. +-- USER / PASS authentication 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 AUTH command using SASL/Plain 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. +-- SASL PLAIN authentication 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 AUTH command using SASL/Login method. --- @param user User string. --- @param pw Password string. --- @param pw String containing password to login. --- @return Status (true or false). --- @return Error code if status is false. +-- SASL LOGIN authentication 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) + local prompt = base64.dec(string.sub(line or "", 3)):lower() + + if not prompt:find("user") then return false, err.userError end - socket:send(user64) + socket:send(base64.enc(user) .. "\r\n") + _, line = socket:receive_lines(1) - local status, line = socket:receive_lines(1) - - if not base64.dec(string.sub(line, 3)) == "Password:" then + prompt = base64.dec(string.sub(line or "", 3)):lower() + if not prompt:find("pass") then return false, err.userError end - socket:send(pw64) - - local status, line = socket:receive_lines(1) + 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 APOP command. --- @param socket Socket connected to POP3 server. --- @param user User 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. +-- APOP authentication (RFC 1939) 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 +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) + + local challenge = base64.dec(string.sub(line or "", 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) +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 present in greeting + if greeting:find("<[^>]+>") then capas.APOP = {} end - local status = socket:send("CAPA\r\n") - if( not(status) ) then - return nil, "Failed to send" - end + socket:send("CAPA\r\n") + local status, response = + socket:receive_buf(match.pattern_limit("%.\r?\n", 4096), false) - status, line = socket:receive_buf(match.pattern_limit("%.", 2048), false) - if( not(status) ) then - return nil, "Failed to receive" - end socket:close() + if not status then + return nil, "Failed to receive CAPA response" + end - local lines = stringaux.strsplit("\r\n",line) - if not stat(table.remove(lines,1)) then + local lines = stringaux.strsplit("\r\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] = {} - end + if line and #line > 0 then + local name, args = line:match("^(%S+)%s*(.*)") + capas[name] = args ~= "" and stringaux.strsplit(" ", args) or {} end end return capas end ---- --- Try to login using the AUTH 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 diff --git a/scripts/msrpc-epm-securitycenter.nse b/scripts/msrpc-epm-securitycenter.nse new file mode 100644 index 000000000..d4e8aeaa9 --- /dev/null +++ b/scripts/msrpc-epm-securitycenter.nse @@ -0,0 +1,69 @@ +local msrpc = require "msrpc" +local shortport = require "shortport" +local string = require "string" + +description = [[ +Checks for the presence of the Windows Security Center service by querying +the MSRPC Endpoint Mapper over TCP port 135. + +The Security Center service is present on Windows 10 but not on Windows +Server 2019, and can be used as a heuristic when OS fingerprinting results +are inconclusive. +]] +--- +-- @usage +-- nmap -p 135 --script msrpc-epm-securitycenter +-- +-- @output +-- | msrpc-epm-securitycenter: +-- | Security Center service present (likely Windows 10) +-- + +author = "Sweekar-cmd (https://github.com/Sweekar-cmd)" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = { "discovery", "safe" } + +portrule = shortport.port_or_service(135, "msrpc") + +action = function(host, port) + local status, rpcstate = msrpc.start_ex(host, port) + if not status then + return nil + end + + status = msrpc.bind(rpcstate, + msrpc.EPMAPPER_UUID, + msrpc.EPMAPPER_VERSION) + if not status then + msrpc.stop(rpcstate) + return nil + end + + local handle = nil + local found = false + + repeat + local result + status, result = msrpc.epmapper_lookup(rpcstate, handle) + if not status or not result then + break + end + + handle = result.new_handle + + if result.annotation and + string.find(result.annotation:lower(), + "security center", 1, true) then + found = true + break + end + until handle == nil + + msrpc.stop(rpcstate) + + if found then + return "Security Center service present (likely Windows 10)" + end + + return "Security Center service not detected" +end