From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- nselib/sslcert.lua | 1094 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1094 insertions(+) create mode 100644 nselib/sslcert.lua (limited to 'nselib/sslcert.lua') diff --git a/nselib/sslcert.lua b/nselib/sslcert.lua new file mode 100644 index 0000000..a8f84c1 --- /dev/null +++ b/nselib/sslcert.lua @@ -0,0 +1,1094 @@ +--- +-- A library providing functions for collecting SSL certificates and storing +-- them in the host-based registry. +-- +-- The library is largely based on code (copy-pasted) from David Fifields +-- ssl-cert script in an effort to allow certs to be cached and shared among +-- other scripts. +-- +-- STARTTLS functions are included for several protocols: +-- +-- * FTP +-- * IMAP +-- * LDAP +-- * NNTP +-- * MySQL +-- * POP3 +-- * PostgreSQL +-- * SMTP +-- * TDS (MS SQL Server) +-- * VNC (TLS and VeNCrypt auth types) +-- * XMPP +-- +-- @author Patrik Karlsson + +local asn1 = require "asn1" +local comm = require "comm" +local ftp = require "ftp" +local ldap = require "ldap" +local match = require "match" +local mssql = require "mssql" +local mysql = require "mysql" +local nmap = require "nmap" +local smtp = require "smtp" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +local tableaux = require "tableaux" +local tls = require "tls" +local vnc = require "vnc" +local xmpp = require "xmpp" +local have_openssl, openssl = pcall(require, "openssl") +_ENV = stdnse.module("sslcert", stdnse.seeall) + +if have_openssl then + --- Parse an X.509 certificate from DER-encoded string + -- + -- This uses OpenSSL's X.509 parsing routines, so if OpenSSL support is not + -- included, only the pem key of the returned table will be + -- present. + --@name parse_ssl_certificate + --@class function + --@param der DER-encoded certificate + --@return table containing decoded certificate or nil on failure + --@return error string if parsing failed + --@see nmap.get_ssl_certificate + _ENV.parse_ssl_certificate = nmap.socket.parse_ssl_certificate +else + local base64 = require "base64" + _ENV.parse_ssl_certificate = function(der) + return { + pem = ("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n"):format( + base64.enc(der):gsub("(" .. ("."):rep(64) .. ")", "%1\n"):gsub("\n$", "") + ) + } + end +end + +-- Mark whether this port supports STARTTLS, to save connection attempts later. +-- If it ever succeeds, it can't be marked as failing later, but if it fails +-- the first time, we won't try again. +local function starttls_supported(host, port, state) + host.registry.starttls = host.registry.starttls or {} + local reg = host.registry.starttls + local mutex = nmap.mutex(reg) + local key = ("%d/%s"):format(port.number, port.protocol) + if reg[key] ~= nil then + return reg[key] + end + -- try releasing mutex, ignore error if we don't hold it. + pcall(mutex, "done") + reg[key] = state + host.registry.starttls_failed = reg +end + +-- Check whether we've tried and failed to STARTTLS already +local function check_starttls_failed (host, port) + host.registry.starttls = host.registry.starttls or {} + local reg = host.registry.starttls + local key = ("%d/%s"):format(port.number, port.protocol) + local mutex = nmap.mutex(reg) + mutex "lock" + if reg[key] ~= nil then + -- somebody already did the hard work. + mutex "done" + return not reg[key] + end + -- no idea. Keep it locked until we know. +end + +-- Simple reconnect_ssl wrapper for most common case +local function tls_reconnect (func) + return function (host, port) + local err + local status, s = StartTLS[func](host, port) + if status then + status,err = s:reconnect_ssl() + if not status then + stdnse.debug1("Could not establish SSL session after STARTTLS command.") + s:close() + return false, "Failed to connect to server" + else + return true, s + end + end + return false, string.format("Failed to connect to server: %s", s or "unknown error") + end +end + +-- Class for sockets which wrap sends and receives in some sort of tunnel +-- Overload the wrap_close, wrap_send, and wrap_receive functions to use it. +-- The socket won't be able to reconnect_ssl, though, since Nsock has +-- no idea about the wrapper. Still useful for ssl-* scripts. +WrappedSocket = +{ + new = function(self, socket, o) + assert(socket, "socket must be connected socket!") + o = o or {} + o.socket = socket + setmetatable(o, self) + self.__index = function(instance, key) + return rawget(self, key) or instance.socket[key] + end + return o + end, + + close = function(self) + return self:wrap_close() + end, + + receive = function(self) + return self:wrap_receive() + end, + + send = function(self, data) + return self:wrap_send(data) + end, + + set_timeout = function(self, timeout) + return self.socket:set_timeout(timeout) + end, + + receive_buf = function(self, delimiter, keeppattern) + self.buffer = self.buffer or "" + local delim_func + if type(delimiter) == "function" then + delim_func = delimiter + else + delim_func = function(buf) + return string.find(buf, delimiter) + end + end + local start, finish = delim_func(self.buffer) + if start then + local rval + if keeppattern then + rval = string.sub(self.buffer, 1, finish) + else + rval = string.sub(self.buffer, 1, start - 1) + end + self.buffer = string.sub(self.buffer, finish + 1) + return true, rval + else + local status, data = self:receive() + if not status then + return status, data + end + self.buffer = self.buffer .. data + -- tail recursion + return self:receive_buf(delimiter, keeppattern) + end + end, + + receive_bytes = function(self, n) + local x = 0 + local read = {} + while x < n do + local status, data = self:receive() + if not status then + return status, data + end + read[#read+1] = data + x = x + #data + end + return true, table.concat(read) + end, + + receive_lines = function(self, n) + local x = 0 + local read = {} + local function incr() + x = x + 1 + end + while x < n do + local status, data = self:receive() + if not status then + return status, data + end + read[#read+1] = data + string.gsub(data, "\n", incr) + end + return true, table.concat(read) + end, + + } + + +StartTLS = { + + ftp_prepare_tls_without_reconnect = function(host, port) + -- Attempt to negotiate TLS over FTP for services that support it + -- Works for FTP (21) + + -- Open a standard TCP socket + local s, code, result, buf = ftp.connect(host, port) + if not s then + return false, string.format("Failed to connect to FTP server: %s", code) + end + if code ~= 220 then + return false, string.format("FTP protocol error: %s", code or result) + end + + -- Send AUTH TLS command, ask the service to start encryption + local status, err = ftp.starttls(s, buf) + if not status then + starttls_supported(host, port, false) + ftp.close(s) + return false, string.format("FTP AUTH TLS error: %s", err) + end + -- Should have a solid TLS over FTP session now... + starttls_supported(host, port, true) + return true, s + end, + + ftp_prepare_tls = tls_reconnect("ftp_prepare_tls_without_reconnect"), + + imap_prepare_tls_without_reconnect = function(host, port) + -- Attempt to negotiate TLS over IMAP for services that support it + -- Works for IMAP (143) + + -- Open a standard TCP socket + local s, err, result = comm.opencon(host, port, "", {lines=1, recv_before=true}) + if not s then + return false, string.format("Failed to connect to IMAP server: %s", err) + end + + if not string.match(result, "^%* OK") then + return false, "IMAP protocol mismatch" + end + + -- Check for STARTTLS support. + local status = s:send("A001 CAPABILITY\r\n") + status, result = s:receive_lines(1) + + if not (string.match(result, "STARTTLS")) then + starttls_supported(host, port, false) + stdnse.debug1("Server doesn't support STARTTLS") + return false, "Failed to connect to IMAP server" + end + + -- Send the STARTTLS message + status = s:send("A002 STARTTLS\r\n") + status, result = s:receive_lines(1) + + if not (string.match(result, "^A002 OK")) then + starttls_supported(host, port, false) + stdnse.debug1(string.format("Error: %s", result)) + return false, "Failed to connect to IMAP server" + end + + -- Should have a solid TLS over IMAP session now... + starttls_supported(host, port, true) + return true, s + end, + + imap_prepare_tls = tls_reconnect("imap_prepare_tls_without_reconnect"), + + ldap_prepare_tls_without_reconnect = function(host, port) + local s = nmap.new_socket() + -- Attempt to negotiate TLS over LDAP for services that support it + -- Works for LDAP (389) + + -- Open a standard TCP socket + local status, error = s:connect(host, port, "tcp") + if not status then + return false, "Failed to connect to LDAP server" + end + + -- Create an LDAP extendedRequest and specify the OID for the + -- STARTTLS operation (see http://www.ietf.org/rfc/rfc2830.txt) + local oid = "1.3.6.1.4.1.1466.20037" + + -- 0x80 = 10000001 = 10 0 00000 + -- hex binary Context Primitive value Field: requestName Value: 0 + local encodedOID = string.pack('Bs1', 0x80, oid) + + local ldapRequest, ldapRequestId + local ExtendedRequest = 23 + local ExtendedResponse = 24 + ldapRequest = ldap.encodeLDAPOp(ExtendedRequest, true, encodedOID) + ldapRequestId = ldap.encode(1) + + -- Send the STARTTLS request + local encoder = asn1.ASN1Encoder:new() + local data = encoder:encodeSeq(ldapRequestId .. ldapRequest) + status = s:send(data) + if not status then + return false, "STARTTLS failed" + end + + -- Decode the response + local response + status, response = s:receive() + if not status then + return false, "STARTTLS failed" + end + + local decoder = asn1.ASN1Decoder:new() + local len, pos, messageId, ldapOp, tmp = "" + len, pos = decoder.decodeLength(response, 2) + messageId, pos = ldap.decode(response, pos) + tmp, pos = string.unpack("B", response, pos) + ldapOp = asn1.intToBER(tmp) + + if ldapOp.number ~= ExtendedResponse then + starttls_supported(host, port, false) + stdnse.debug1(string.format( + "STARTTLS failed (got wrong op number: %d)", ldapOp.number)) + return false, "STARTTLS failed" + end + + local resultCode + len, pos = decoder.decodeLength(response, pos) + resultCode, pos = ldap.decode(response, pos) + + if resultCode ~= 0 then + starttls_supported(host, port, false) + stdnse.debug1(string.format( + "STARTTLS failed (LDAP error code is: %s)", tonumber(resultCode) or "not a number")) + return false, "STARTTLS failed" + end + + -- Should have a solid TLS over LDAP session now... + starttls_supported(host, port, true) + return true,s + end, + + ldap_prepare_tls = tls_reconnect("ldap_prepare_tls_without_reconnect"), + + lmtp_prepare_tls_without_reconnect = function(host, port) + -- Open a standard TCP socket + local s, result = smtp.connect(host, port, {lines=1, recv_before=1, ssl=false}) + if not s then + return false, string.format("Failed to connect to LMTP server: %s", result) + end + + local status + status, result = smtp.query(s, "LHLO", smtp.get_domain(host)) + if not status then + stdnse.debug1("LHLO with errors or timeout. Enable --script-trace to see what is happening.") + return false, string.format("Failed to LHLO: %s", result) + end + -- semantics of LHLO are same as EHLO + status, result = smtp.check_reply("EHLO", result) + if not status then + return false, string.format("Received LHLO error: %s", result) + end + + -- Send STARTTLS command ask the service to start encryption + status, result = smtp.query(s, "STARTTLS") + if status then + status, result = smtp.check_reply("STARTTLS", result) + end + + if not status then + starttls_supported(host, port, false) + stdnse.debug1("STARTTLS failed or unavailable. Enable --script-trace to see what is happening.") + + -- Send QUIT to clean up server side connection + smtp.quit(s) + return false, string.format("Failed to connect to SMTP server: %s", result) + end + -- Should have a solid TLS over LMTP session now... + starttls_supported(host, port, true) + return true, s + end, + + lmtp_prepare_tls = tls_reconnect("lmtp_prepare_tls_without_reconnect"), + + mysql_prepare_tls_without_reconnect = function(host, port) + local s, err = comm.opencon(host, port) + if not s then + return false, string.format("Failed to connect to MySQL server: %s", err) + end + local status, resp = mysql.receiveGreeting(s) + if not status then + return false, string.format("MySQL handshake error: %s", resp) + end + if 0 == resp.capabilities & mysql.Capabilities.SwitchToSSLAfterHandshake then + return false, "MySQL server does not support SSL" + end + local clicap = mysql.Capabilities.SwitchToSSLAfterHandshake + + mysql.Capabilities.LongPassword + + mysql.Capabilities.LongColumnFlag + + mysql.Capabilities.SupportsLoadDataLocal + + mysql.Capabilities.Speaks41ProtocolNew + + mysql.Capabilities.InteractiveClient + + mysql.Capabilities.SupportsTransactions + + mysql.Capabilities.Support41Auth + local packet = string.pack( "I4I4", 8, 80877103)) + if not s then + return false, ("Failed to connect to Postgres server: %s"):format(resp) + end + -- v2 has "Y", v3 has "S" + if string.match(resp, "^[SY]") then + starttls_supported(host, port, true) + return true, s + elseif string.match(resp, "^N") then + starttls_supported(host, port, false) + return false, "Postgres server does not support SSL" + end + return false, "Unknown response from Postgres server" + end, + + postgres_prepare_tls = tls_reconnect("postgres_prepare_tls_without_reconnect"), + + smtp_prepare_tls_without_reconnect = function(host, port) + -- Attempt to negotiate TLS over SMTP for services that support it + -- Works for SMTP (25) and SMTP Submission (587) + + -- Open a standard TCP socket + local s, result = smtp.connect(host, port, {lines=1, recv_before=1, ssl=false}) + if not s then + return false, string.format("Failed to connect to SMTP server: %s", result) + end + + local status + status, result = smtp.ehlo(s, smtp.get_domain(host)) + if not status then + stdnse.debug1("EHLO with errors or timeout. Enable --script-trace to see what is happening.") + return false, string.format("Failed to connect to SMTP server: %s", result) + end + + -- Send STARTTLS command ask the service to start encryption + status, result = smtp.query(s, "STARTTLS") + if status then + status, result = smtp.check_reply("STARTTLS", result) + end + + if not status then + starttls_supported(host, port, false) + stdnse.debug1("STARTTLS failed or unavailable. Enable --script-trace to see what is happening.") + + -- Send QUIT to clean up server side connection + smtp.quit(s) + return false, string.format("Failed to connect to SMTP server: %s", result) + end + -- Should have a solid TLS over SMTP session now... + starttls_supported(host, port, true) + return true, s + end, + + smtp_prepare_tls = tls_reconnect("smtp_prepare_tls_without_reconnect"), + + tds_prepare_tls_without_reconnect = function(host, port) + local tds = mssql.TDSStream:new() + local status, result = tds:Connect(host, port) + if not status then return status, result end + local prelogin = mssql.PreLoginPacket:new() + prelogin:SetRequestEncryption(true) + tds:Send( prelogin:ToBytes() ) + status, result = tds:Receive() + if not status then return status, result end + + local status, preloginResponse = mssql.PreLoginPacket.FromBytes(result) + if not status then return status, preloginResponse end + + local encryption + local optype, oppos, oplen, pos = string.unpack('>BI2I2', result) + while optype ~= mssql.PreLoginPacket.OPTION_TYPE.Terminator do + --stdnse.debug1("optype: %d, oppos: %x, oplen: %d", optype, oppos, oplen) + if optype == mssql.PreLoginPacket.OPTION_TYPE.Encryption then + encryption, pos = string.unpack('B', result, oppos + 1) + break + end + optype, oppos, oplen, pos = string.unpack('>BI2I2', result, pos) + end + if not encryption then + starttls_supported(host, port, false) + return false, "no encryption option found" + elseif encryption == 0 then + starttls_supported(host, port, false) + return false, "Server refused encryption" + elseif encryption == 3 then + starttls_supported(host, port, false) + return false, "Server does not support encryption" + end + + starttls_supported(host, port, true) + return true, WrappedSocket:new(tds._socket, { + wrap_close = function(self) + return tds:Disconnect() + end, + wrap_receive = function(self) + -- mostly lifted from mssql.TDSStream.Receive + -- TODO: Modify that function to allow receiving arbitrary response + -- types, since it's only because it forces type 0x04 that we had to + -- do this here (where we expect type 0x12) + local combinedData = "" + local readBuffer = "" + local pos = 1 + local tdsPacketAvailable = true + + -- Large messages (e.g. result sets) can be split across multiple TDS + -- packets from the server (which could themselves each be split across + -- multiple TCP packets or SMB messages). + while ( tdsPacketAvailable ) do + -- If there is existing data in the readBuffer, see if there's + -- enough to read the TDS headers for the next packet. If not, + -- do another read so we have something to work with. + if #readBuffer < 8 then + status, result = tds._socket:receive_bytes(8 - readBuffer:len()) + if not status then return status, result end + readBuffer = readBuffer .. result + end + + -- TDS packet validity check: packet at least as long as the TDS header + if #readBuffer < 8 then + return false, "Server returned short packet" + end + + -- read in the TDS headers + local packetType, messageStatus, packetLength + packetType, messageStatus, packetLength, pos = string.unpack(">BBI2", readBuffer, pos ) + local spid, packetId, window + spid, packetId, window, pos = string.unpack(">I2BB", readBuffer, pos ) + + if packetLength > #readBuffer then + status, result = tds._socket:receive_bytes(packetLength - #readBuffer) + if not status then return status, result end + readBuffer = readBuffer .. result + end + + -- We've read in an apparently valid TDS packet + local thisPacketData = readBuffer:sub( pos, packetLength ) + -- Append its data to that of any previous TDS packets + combinedData = combinedData .. thisPacketData + -- If we read in data beyond the end of this TDS packet, save it + -- so that we can use it in the next loop. + readBuffer = readBuffer:sub( packetLength + 1 ) + + -- Check the status flags in the TDS packet to see if the message is + -- continued in another TDS packet. + tdsPacketAvailable = ( + (messageStatus & mssql.TDSStream.MESSAGE_STATUS_FLAGS.EndOfMessage) + ~= mssql.TDSStream.MESSAGE_STATUS_FLAGS.EndOfMessage) + end + + -- return only the data section ie. without the headers + return true, combinedData + + end, + wrap_send = function(self, data) + return tds:Send(mssql.PacketType.PreLogin, data) + end, + }) + end, + -- no TLS reconnect for TDS because of the wrapped handshake thing. + tds_prepare_tls = function(host, port) + return false, "Full SSL connection over TDS not supported" + end, + + vnc_prepare_tls_without_reconnect = function(host,port) + local v = vnc.VNC:new( host, port ) + + local status, data = v:connect() + if not status then + return false, string.format("Failed to connect to VNC server: %s", data) + end + + status, data = v:handshake() + if not status then + return false, string.format("Failed VNC handshake: %s", data) + end + + local sock = v.socket + if v:supportsSecType(vnc.VNC.sectypes.VENCRYPT) then + + status, data = v:handshake_vencrypt() + if not status then + return false, string.format("Failed VeNCrypt handshake: %s", data) + end + local auth_order = { + -- X509 types are not anonymous, have real certs + vnc.VENCRYPT_SUBTYPES.X509VNC, + vnc.VENCRYPT_SUBTYPES.X509SASL, + vnc.VENCRYPT_SUBTYPES.X509NONE, + vnc.VENCRYPT_SUBTYPES.X509PLAIN, + -- TLS types use anonymous DH handshakes + vnc.VENCRYPT_SUBTYPES.TLSVNC, + vnc.VENCRYPT_SUBTYPES.TLSSASL, + vnc.VENCRYPT_SUBTYPES.TLSNONE, + vnc.VENCRYPT_SUBTYPES.TLSPLAIN, + -- PLAIN type doesn't use TLS + } + local best + for i=1, #auth_order do + if tableaux.contains(v.vencrypt.types, auth_order[i]) then + best = auth_order[i] + break + end + end + + if not best then + starttls_supported(host, port, false) + return false, "No TLS VeNCrypt auth subtype received" + end + sock:send(string.pack(">I4", best)) + local status, buf = sock:receive_buf(match.numbytes(1), true) + if not status or string.byte(buf, 1) ~= 1 then + starttls_supported(host, port, false) + return false, "VeNCrypt auth subtype refused" + end + starttls_supported(host, port, true) + return true, sock + elseif v:supportsSecType(vnc.VNC.sectypes.TLS) then + status = sock:send( string.pack("B", vnc.VNC.sectypes.TLS) ) + if not status then + starttls_supported(host, port, false) + return false, "Failed to select TLS authentication type" + end + else + starttls_supported(host, port, false) + return false, string.format("No TLS auth types supported") + end + starttls_supported(host, port, true) + return true, sock + end, + + vnc_prepare_tls = tls_reconnect("vnc_prepare_tls_without_reconnect"), + + xmpp_prepare_tls_without_reconnect = function(host,port) + local sock,status,err,result + local xmppStreamStart = string.format("\r\n\r\n",host.name) + local xmppStartTLS = "\r\n" + sock = nmap.new_socket() + sock:set_timeout(5000) + status, err = sock:connect(host, port) + if not status then + sock:close() + stdnse.debug1("Can't send: %s", err) + return false, "Failed to connect to XMPP server" + end + status, err = sock:send(xmppStreamStart) + if not status then + stdnse.debug1("Couldn't send: %s", err) + sock:close() + return false, "Failed to connect to XMPP server" + end + status, result = sock:receive() + if not status then + stdnse.debug1("Couldn't receive: %s", err) + sock:close() + return false, "Failed to connect to XMPP server" + end + status, err = sock:send(xmppStartTLS) + if not status then + stdnse.debug1("Couldn't send: %s", err) + sock:close() + return false, "Failed to connect to XMPP server" + end + status, result = sock:receive() + if not status then + stdnse.debug1("Couldn't receive: %s", err) + sock:close() + return false, "Failed to connect to XMPP server" + end + if string.find(result,"proceed") then + starttls_supported(host, port, true) + return true,sock + end + + status, result = sock:receive() -- might not be in the first reply + if not status then + stdnse.debug1("Couldn't receive: %s", err) + sock:close() + return false, "Failed to connect to XMPP server" + end + if string.find(result,"proceed") then + starttls_supported(host, port, true) + return true,sock + else + starttls_supported(host, port, false) + return false, "Failed to connect to XMPP server" + end + end, + + xmpp_prepare_tls = function(host, port) + local ls = xmpp.XMPP:new(host, port, { starttls = true } ) + ls.socket = nmap.new_socket() + ls.socket:set_timeout(ls.options.timeout * 1000) + + local status, err = ls.socket:connect(host, port) + if not status then + return nil + end + + status, err = ls:connect() + if not(status) then + return false, "Failed to connected" + end + starttls_supported(host, port, true) + return true, ls.socket + end +} + + +-- A table mapping port numbers to specialized SSL negotiation functions. +local SPECIALIZED_PREPARE_TLS = { + ftp = StartTLS.ftp_prepare_tls, + [21] = StartTLS.ftp_prepare_tls, + nntp = StartTLS.nntp_prepare_tls, + [119] = StartTLS.nntp_prepare_tls, + imap = StartTLS.imap_prepare_tls, + [143] = StartTLS.imap_prepare_tls, + ldap = StartTLS.ldap_prepare_tls, + [389] = StartTLS.ldap_prepare_tls, + lmtp = StartTLS.lmtp_prepare_tls, + pop3 = StartTLS.pop3_prepare_tls, + [110] = StartTLS.pop3_prepare_tls, + postgresql = StartTLS.postgres_prepare_tls, + [5432] = StartTLS.postgres_prepare_tls, + smtp = StartTLS.smtp_prepare_tls, + [25] = StartTLS.smtp_prepare_tls, + [587] = StartTLS.smtp_prepare_tls, + mysql = StartTLS.mysql_prepare_tls, + [3306] = StartTLS.mysql_prepare_tls, + xmpp = StartTLS.xmpp_prepare_tls, + [5222] = StartTLS.xmpp_prepare_tls, + [5269] = StartTLS.xmpp_prepare_tls, + vnc = StartTLS.vnc_prepare_tls, + [5900] = StartTLS.vnc_prepare_tls, + ["ms-sql-s"] = StartTLS.tds_prepare_tls +} + +local SPECIALIZED_PREPARE_TLS_WITHOUT_RECONNECT = { + ftp = StartTLS.ftp_prepare_tls_without_reconnect, + [21] = StartTLS.ftp_prepare_tls_without_reconnect, + nntp = StartTLS.nntp_prepare_tls_without_reconnect, + [119] = StartTLS.nntp_prepare_tls_without_reconnect, + imap = StartTLS.imap_prepare_tls_without_reconnect, + [143] = StartTLS.imap_prepare_tls_without_reconnect, + ldap = StartTLS.ldap_prepare_tls_without_reconnect, + [389] = StartTLS.ldap_prepare_tls_without_reconnect, + lmtp = StartTLS.lmtp_prepare_tls_without_reconnect, + pop3 = StartTLS.pop3_prepare_tls_without_reconnect, + [110] = StartTLS.pop3_prepare_tls_without_reconnect, + postgresql = StartTLS.postgres_prepare_tls_without_reconnect, + [5432] = StartTLS.postgres_prepare_tls_without_reconnect, + smtp = StartTLS.smtp_prepare_tls_without_reconnect, + [25] = StartTLS.smtp_prepare_tls_without_reconnect, + [587] = StartTLS.smtp_prepare_tls_without_reconnect, + mysql = StartTLS.mysql_prepare_tls_without_reconnect, + [3306] = StartTLS.mysql_prepare_tls_without_reconnect, + xmpp = StartTLS.xmpp_prepare_tls_without_reconnect, + [5222] = StartTLS.xmpp_prepare_tls_without_reconnect, + [5269] = StartTLS.xmpp_prepare_tls_without_reconnect, + vnc = StartTLS.vnc_prepare_tls_without_reconnect, + [5900] = StartTLS.vnc_prepare_tls_without_reconnect, +} + +-- these can't do reconnect_ssl +local SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT = { + ["ms-sql-s"] = StartTLS.tds_prepare_tls_without_reconnect, +} + +-- Wrap the specialized connection function with a check for previous fail +local function wrap_special_with_reg_check(special) + return special and function(host, port) + local oldfail = check_starttls_failed(host, port) + if oldfail then + return false, "Previous STARTTLS attempt failed" + else + local result = table.pack(special(host, port)) + local mutex = nmap.mutex(host.registry.starttls) + pcall(mutex, "done") + return table.unpack(result) + end + end +end + +--- Get a specialized SSL connection function without starting SSL +-- +-- For protocols that require some sort of START-TLS setup, this function will +-- return a function that can be used to produce a socket that is ready for SSL +-- messages. +-- @param port A port table with 'number' and 'service' keys +-- @return A STARTTLS function or nil +function getPrepareTLSWithoutReconnect(port) + if port.protocol == 'udp' then + return nil + end + if ( port.version and port.version.service_tunnel == 'ssl') then + return nil + end + local special = (SPECIALIZED_PREPARE_TLS_WITHOUT_RECONNECT[port.service] or + SPECIALIZED_PREPARE_TLS_WITHOUT_RECONNECT[port.number] or + SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.service] or + SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.number]) + return wrap_special_with_reg_check(special) +end + +--- Get a specialized SSL connection function to create an SSL socket +-- +-- For protocols that require some sort of START-TLS setup, this function will +-- return a function that can be used to produce an SSL-connected socket. +-- @param port A port table with 'number' and 'service' keys +-- @return A STARTTLS function or nil +function isPortSupported(port) + if port.protocol == 'udp' then + return nil + end + if ( port.version and port.version.service_tunnel == 'ssl') then + return nil + end + local special = (SPECIALIZED_PREPARE_TLS[port.service] or + SPECIALIZED_PREPARE_TLS[port.number]) + return wrap_special_with_reg_check(special) +end + +-- returns a function that yields a new tls record each time it is called +local function get_record_iter(sock) + local buffer = "" + local i = 1 + local fragment + return function () + local record, more + i, record, more = tls.record_read(buffer, i, fragment) + if record == nil then + if not more then + return nil, "no more" + end + local status, err + status, buffer, err = tls.record_buffer(sock, buffer, i) + if not status then + return nil, err + end + i, record = tls.record_read(buffer, i, fragment) + if record == nil then + return nil, "done" + end + end + fragment = record.fragment + return record + end +end + +local function handshake_cert (socket) + -- logic mostly lifted from ssl-enum-ciphers + -- TODO: implement TLSv1.3 handshake encryption so we can decrypt the + -- Certificate message. Until then, we don't attempt TLSv1.3 + local hello = tls.client_hello({protocol="TLSv1.2"}) + local status, err = socket:send(hello) + if not status then + return false, "Failed to send to server" + end + + local get_next_record = get_record_iter(socket) + local records = {} + local done = false + while not done do + local record + record, err = get_next_record() + if not record then + stdnse.debug1("no record: %s", err) + break + end + -- Collect message bodies into one record per type + records[record.type] = records[record.type] or record + for j = 1, #record.body do -- no ipairs because we append below + local b = record.body[j] + done = ((record.type == "alert" and b.level == "fatal") or + (record.type == "handshake" and b.type == "server_hello_done")) + table.insert(records[record.type].body, b) + end + end + + local handshake = records.handshake + if not handshake then + return false, "Server did not handshake" + end + + local certs + for i, b in ipairs(handshake.body) do + if b.type == "certificate" then + certs = b + break + end + end + if not certs or not next(certs.certificates) then + return false, "Server sent no certificate" + end + + local cert, err = parse_ssl_certificate(certs.certificates[1]) + if not cert then + return false, ("Unable to parse cert: %s"):format(err) + end + return true, cert +end + +--- Gets a certificate for the given host and port +-- The function will attempt to START-TLS for the ports known to require it. +-- @param host table as received by the script action function +-- @param port table as received by the script action function +-- @return status true on success, false on failure +-- @return cert userdata containing the SSL certificate, or error message on +-- failure. +function getCertificate(host, port) + local mutex = nmap.mutex("sslcert-cache-mutex") + mutex "lock" + + if ( host.registry["ssl-cert"] and + host.registry["ssl-cert"][port.number] ) then + stdnse.debug2("sslcert: Returning cached SSL certificate") + mutex "done" + return true, host.registry["ssl-cert"][port.number] + end + + local cert + + local wrapper = SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.service] or SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.number] + local special_table = have_openssl and SPECIALIZED_PREPARE_TLS or SPECIALIZED_PREPARE_TLS_WITHOUT_RECONNECT + local specialized = special_table[port.service] or special_table[port.number] + + local status = false + + -- If we don't already know the service is TLS wrapped check to see if we + -- have to use a wrapper and do a manual handshake + if wrapper and port.version.service_tunnel ~= 'ssl' then + local socket + status, socket = wrapper(host, port) + if not status then + stdnse.debug1("Wrapper function error: %s", socket) + else + status, cert = handshake_cert(socket) + socket:close() + end + end + + -- If that didn't work, see if we need a specialized connection method + if not status and specialized and port.version.service_tunnel ~= 'ssl' then + local socket + status, socket = specialized(host, port) + if not status then + stdnse.debug1("Specialized function error: %s", socket) + else + if have_openssl then + cert = socket:get_ssl_certificate() + status = not not cert + else + status, cert = handshake_cert(socket) + end + socket:close() + end + end + + -- Now try to connect with Nsock's SSL connection + if not status and have_openssl then + local socket = nmap.new_socket() + local errmsg + status, errmsg = socket:connect(host, port, "ssl") + if not status then + stdnse.debug1("SSL connect error: %s", errmsg) + else + cert = socket:get_ssl_certificate() + status = not not cert + socket:close() + end + end + + -- Finally, try to connect and manually handshake (maybe more tolerant of TLS + -- insecurity than OpenSSL) + if not status then + local socket = nmap.new_socket() + local errmsg + status, errmsg = socket:connect(host, port) + if not status then + stdnse.debug1("Connect error: %s", errmsg) + else + status, cert = handshake_cert(socket) + socket:close() + end + end + + if not status then + mutex "done" + return false, "No certificate found" + end + + host.registry["ssl-cert"] = host.registry["ssl-cert"] or {} + host.registry["ssl-cert"][port.number] = host.registry["ssl-cert"][port.number] or {} + host.registry["ssl-cert"][port.number] = cert + mutex "done" + return true, cert +end + + + +return _ENV; -- cgit v1.2.3