diff --git a/CHANGELOG b/CHANGELOG index 0a69b64e8..0d37df19b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a sslcert library that gets and caches SSL certificates in the + registry. Modified the scripts ssl-cert and ssl-google-cert-catalog to take + advantage of this change. [Patrik] + o [NSE] Added host based registry, which allows scripts to share data between scripts scanning a specific host. [Patrik] diff --git a/nselib/sslcert.lua b/nselib/sslcert.lua new file mode 100644 index 000000000..abb596686 --- /dev/null +++ b/nselib/sslcert.lua @@ -0,0 +1,208 @@ +--- +-- 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. +-- + +module(... or "sslcert", package.seeall) + +require("xmpp") + +StartTLS = { + + ftp_starttls = function(host, port) + local s = nmap.new_socket() + -- Attempt to negotiate TLS over FTP for services that support it + -- Works for FTP (21) + + -- Open a standard TCP socket + local status, error = s:connect(host, port, "tcp") + local result + if not status then + return false, "Failed to connect to FTP server" + else + + -- Loop until the service presents a banner to deal with server + -- load and timing issues. There may be a better way to handle this. + local i = 0 + repeat + status, result = s:receive_lines(1) + i = i + 1 + until string.match(result, "^220") or i == 5 + + -- Send AUTH TLS command, ask the service to start encryption + local query = "AUTH TLS\r\n" + status = s:send(query) + status, result = s:receive_lines(1) + + if not (string.match(result, "^234")) then + stdnse.print_debug("1","%s",result) + stdnse.print_debug("1","AUTH TLS failed or unavailable. Enable --script-trace to see what is happening.") + + -- Send QUIT to clean up server side connection + local query = "QUIT\r\n" + status = s:send(query) + result = "" + + return false, "Failed to connect to FTP server" + end + + -- Service supports AUTH TLS, tell NSE start SSL negotiation + status, error = s:reconnect_ssl() + if not status then + stdnse.print_debug("1","Could not establish SSL session after AUTH TLS command.") + s:close() + return false, "Failed to connect to FTP server" + end + + end + -- Should have a solid TLS over FTP session now... + return true, s + end, + + smtp_starttls = function(host, port) + local s = nmap.new_socket() + -- 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 status, error = s:connect(host, port, "tcp") + + if not status then + return nil + else + local resultEHLO + -- Loop until the service presents a banner to deal with server + -- load and timing issues. There may be a better way to handle this. + local i = 0 + repeat + status, resultEHLO = s:receive_lines(1) + i = i + 1 + until string.match(resultEHLO, "^220") or i == 5 + + -- Send EHLO because the the server expects it + -- We are not going to check for STARTTLS in the capabilities + -- list, sometimes it is not advertised. + local query = "EHLO example.org\r\n" + status = s:send(query) + status, resultEHLO = s:receive_lines(1) + + if not (string.match(resultEHLO, "^250")) then + stdnse.print_debug("1","%s",resultEHLO) + stdnse.print_debug("1","EHLO with errors or timeout. Enable --script-trace to see what is happening.") + return false, "Failed to connect to SMTP server" + end + + resultEHLO = "" + + -- Send STARTTLS command ask the service to start encryption + local query = "STARTTLS\r\n" + status = s:send(query) + status, resultEHLO = s:receive_lines(1) + + if not (string.match(resultEHLO, "^220")) then + stdnse.print_debug("1","%s",resultEHLO) + stdnse.print_debug("1","STARTTLS failed or unavailable. Enable --script-trace to see what is happening.") + + -- Send QUIT to clean up server side connection + local query = "QUIT\r\n" + status = s:send(query) + resultEHLO = "" + + return false, "Failed to connect to SMTP server" + end + + -- Service supports STARTTLS, tell NSE start SSL negotiation + status, error = s:reconnect_ssl() + if not status then + stdnse.print_debug("1","Could not establish SSL session after STARTTLS command.") + s:close() + return false, "Failed to connect to SMTP server" + end + + end + -- Should have a solid TLS over SMTP session now... + return true, s + end, + + xmpp_starttls = 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 + return true, ls.socket + end +} + +-- A table mapping port numbers to specialized SSL negotiation functions. +local SPECIALIZED_FUNCS = { + [21] = StartTLS.ftp_starttls, + [25] = StartTLS.smtp_starttls, + [587] = StartTLS.smtp_starttls, + [5222] = StartTLS.xmpp_starttls, + [5269] = StartTLS.xmpp_starttls +} + +function isPortSupported(port) + return SPECIALIZED_FUNCS[port.number] +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.print_debug(2, "sslcert: Returning cached SSL certificate") + mutex "done" + return true, host.registry["ssl-cert"][port.number] + end + + -- Is there a specialized function for this port? + local specialized = SPECIALIZED_FUNCS[port.number] + local status + local socket = nmap.new_socket() + if specialized then + status, socket = specialized(host, port) + + if not status then + mutex "done" + return false, "Failed to connect to server" + end + else + local status + status = socket:connect(host, port, "ssl") + if ( not(status) ) then + mutex "done" + return false, "Failed to connect to server" + end + end + local cert = socket:get_ssl_certificate() + + 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 + + diff --git a/scripts/ssl-cert.nse b/scripts/ssl-cert.nse index 5f2965209..103c44f0b 100644 --- a/scripts/ssl-cert.nse +++ b/scripts/ssl-cert.nse @@ -64,217 +64,11 @@ license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = { "default", "safe", "discovery" } -require("nmap") -require("nsedebug") +require("sslcert") require("shortport") -require("stdnse") -require("xmpp") - -local stringify_name -local date_to_string -local table_find -local s - -function ftp_starttls(host, port) - -- Attempt to negotiate TLS over FTP for services that support it - -- Works for FTP (21) - - -- Open a standard TCP socket - local status, error = s:connect(host, port, "tcp") - - if not status then - return nil - else - - -- Loop until the service presents a banner to deal with server - -- load and timing issues. There may be a better way to handle this. - local i = 0 - repeat - status, result = s:receive_lines(1) - i = i + 1 - until string.match(result, "^220") or i == 5 - - -- Send AUTH TLS command, ask the service to start encryption - local query = "AUTH TLS\r\n" - status = s:send(query) - status, result = s:receive_lines(1) - - if not (string.match(result, "^234")) then - stdnse.print_debug("1","%s",result) - stdnse.print_debug("1","AUTH TLS failed or unavailable. Enable --script-trace to see what is happening.") - - -- Send QUIT to clean up server side connection - local query = "QUIT\r\n" - status = s:send(query) - result = "" - - return nil - end - - -- Service supports AUTH TLS, tell NSE start SSL negotiation - status, error = s:reconnect_ssl() - if not status then - stdnse.print_debug("1","Could not establish SSL session after AUTH TLS command.") - s:close() - return nil - end - - end - -- Should have a solid TLS over FTP session now... - return "Connected" -end - -function smtp_starttls(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 status, error = s:connect(host, port, "tcp") - - if not status then - return nil - else - - -- Loop until the service presents a banner to deal with server - -- load and timing issues. There may be a better way to handle this. - local i = 0 - repeat - status, resultEHLO = s:receive_lines(1) - i = i + 1 - until string.match(resultEHLO, "^220") or i == 5 - - -- Send EHLO because the the server expects it - -- We are not going to check for STARTTLS in the capabilities - -- list, sometimes it is not advertised. - local query = "EHLO example.org\r\n" - status = s:send(query) - status, resultEHLO = s:receive_lines(1) - - if not (string.match(resultEHLO, "^250")) then - stdnse.print_debug("1","%s",resultEHLO) - stdnse.print_debug("1","EHLO with errors or timeout. Enable --script-trace to see what is happening.") - return nil - end - - resultEHLO = "" - - -- Send STARTTLS command ask the service to start encryption - local query = "STARTTLS\r\n" - status = s:send(query) - status, resultEHLO = s:receive_lines(1) - - if not (string.match(resultEHLO, "^220")) then - stdnse.print_debug("1","%s",resultEHLO) - stdnse.print_debug("1","STARTTLS failed or unavailable. Enable --script-trace to see what is happening.") - - -- Send QUIT to clean up server side connection - local query = "QUIT\r\n" - status = s:send(query) - resultEHLO = "" - - return nil - end - - -- Service supports STARTTLS, tell NSE start SSL negotiation - status, error = s:reconnect_ssl() - if not status then - stdnse.print_debug("1","Could not establish SSL session after STARTTLS command.") - s:close() - return nil - end - - end - -- Should have a solid TLS over SMTP session now... - return "Connected" -end - -function xmpp_starttls(host, port) - local ls = xmpp.XMPP:new(host, port, { starttls = true } ) - ls.socket = s - 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 status then - return "Connected" - end -end - --- A table mapping port numbers to specialized SSL negotiation functions. -local SPECIALIZED_FUNCS = { - [21] = ftp_starttls, - [25] = smtp_starttls, - [587] = smtp_starttls, - [5222] = xmpp_starttls, - [5269] = xmpp_starttls -} portrule = function(host, port) - return shortport.ssl(host, port) or SPECIALIZED_FUNCS[port.number] -end - -action = function(host, port) - local specialized - - s = nmap.new_socket() - - -- Is there a specialized function for this port? - specialized = SPECIALIZED_FUNCS[port.number] - if specialized then - local status = specialized(host, port) - - if not status then - return nil - end - else - local status, error = s:connect(host, port, "ssl") - - if not status then - if nmap.verbosity() > 0 then - return error - else - return nil - end - end - - end - - local cert = s:get_ssl_certificate() - s:close() - - local lines = {} - lines[#lines + 1] = "Subject: " .. stringify_name(cert.subject) - - if nmap.verbosity() > 0 then - lines[#lines + 1] = "Issuer: " .. stringify_name(cert.issuer) - end - - if nmap.verbosity() > 0 then - lines[#lines + 1] = "Public Key type: " .. cert.pubkey.type - lines[#lines + 1] = "Public Key bits: " .. cert.pubkey.bits - end - - lines[#lines + 1] = "Not valid before: " .. - date_to_string(cert.validity.notBefore) - lines[#lines + 1] = "Not valid after: " .. - date_to_string(cert.validity.notAfter) - - if nmap.verbosity() > 0 then - lines[#lines + 1] = "MD5: " .. stdnse.tohex(cert:digest("md5"), { separator = " ", group = 4 }) - lines[#lines + 1] = "SHA-1: " .. stdnse.tohex(cert:digest("sha1"), { separator = " ", group = 4 }) - end - - if nmap.verbosity() > 1 then - lines[#lines + 1] = cert.pem - end - - add_cert(host, port.number, cert) - - return stdnse.strjoin("\n", lines) + return shortport.ssl(host, port) or sslcert.isPortSupported(port) end -- Find the index of a value in an array. @@ -288,6 +82,17 @@ function table_find(t, value) return nil end +function date_to_string(date) + if not date then + return "MISSING" + end + if type(date) == "string" then + return string.format("Can't parse; string is \"%s\"", date) + else + return os.date("%Y-%m-%d %H:%M:%S", os.time(date)) + end +end + -- These are the subject/issuer name fields that will be shown, in this order, -- without a high verbosity. local NON_VERBOSE_FIELDS = { "commonName", "organizationName", @@ -319,25 +124,46 @@ function stringify_name(name) return stdnse.strjoin("/", fields) end -function date_to_string(date) - if not date then - return "MISSING" +local function parseCertificate(cert) + local lines = {} + + lines[#lines + 1] = "Subject: " .. stringify_name(cert.subject) + + if nmap.verbosity() > 0 then + lines[#lines + 1] = "Issuer: " .. stringify_name(cert.issuer) end - if type(date) == "string" then - return string.format("Can't parse; string is \"%s\"", date) - else - return os.date("%Y-%m-%d %H:%M:%S", os.time(date)) + + if nmap.verbosity() > 0 then + lines[#lines + 1] = "Public Key type: " .. cert.pubkey.type + lines[#lines + 1] = "Public Key bits: " .. cert.pubkey.bits end + + lines[#lines + 1] = "Not valid before: " .. + date_to_string(cert.validity.notBefore) + lines[#lines + 1] = "Not valid after: " .. + date_to_string(cert.validity.notAfter) + + if nmap.verbosity() > 0 then + lines[#lines + 1] = "MD5: " .. stdnse.tohex(cert:digest("md5"), { separator = " ", group = 4 }) + lines[#lines + 1] = "SHA-1: " .. stdnse.tohex(cert:digest("sha1"), { separator = " ", group = 4 }) + end + + if nmap.verbosity() > 1 then + lines[#lines + 1] = cert.pem + end + return lines end -function add_cert(host, port, cert) - if not nmap.registry[host.ip] then - nmap.registry[host.ip] = {} - end - if not nmap.registry[host.ip][port] then - nmap.registry[host.ip][port] = {} - end +action = function(host, port) + local status, cert = sslcert.getCertificate(host, port) + if ( not(status) ) then + return + end + + local lines = parseCertificate(cert) - nmap.registry[host.ip][port]["ssl-cert"] = cert + return stdnse.strjoin("\n", lines) end + + diff --git a/scripts/ssl-google-cert-catalog.nse b/scripts/ssl-google-cert-catalog.nse index 7e57a79d2..39df74eba 100644 --- a/scripts/ssl-google-cert-catalog.nse +++ b/scripts/ssl-google-cert-catalog.nse @@ -23,19 +23,13 @@ matching domain name, it may be suspicious. This script requires the author = "Vasiliy Kulikov" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = { "safe", "discovery", "external" } -dependencies = { "ssl-cert" } +--dependencies = { "ssl-cert" } require("nmap") require("shortport") require("stdnse") require("dns") - - -local get_cert = function(host, port) - if nmap.registry[host.ip] and nmap.registry[host.ip][port] then - return nmap.registry[host.ip][port]["ssl-cert"] - end -end +require("sslcert") local format_date = function(day_num) return os.date("%d %b %Y", 60 * 60 * 24 * tonumber(day_num)) @@ -45,9 +39,9 @@ portrule = shortport.ssl action = function(host, port) local lines, sha1, query - local cert = get_cert(host, port.number) + local status, cert = sslcert.getCertificate(host, port) - if not cert then + if not status then return nil end