diff --git a/nselib/sslcert.lua b/nselib/sslcert.lua index c6873c285..7d87f7624 100644 --- a/nselib/sslcert.lua +++ b/nselib/sslcert.lua @@ -863,9 +863,12 @@ local function get_record_iter(sock) local i = 1 local fragment return function () - local record - i, record = tls.record_read(buffer, i, fragment) + 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 @@ -881,6 +884,57 @@ local function get_record_iter(sock) end end +local function handshake_cert (socket) + -- logic mostly lifted from ssl-enum-ciphers + local hello = tls.client_hello() + 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 @@ -901,106 +955,70 @@ function getCertificate(host, port) local cert + local wrapper = SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.service] or SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.number] + local specialized = SPECIALIZED_PREPARE_TLS[port.service] or SPECIALIZED_PREPARE_TLS[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 - local wrapper - if not ( port.version.service_tunnel == 'ssl' ) then - wrapper = SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.service] or SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.number] + 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 wrapper then - local status, socket = wrapper(host, port) + -- 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 - mutex "done" - return false, socket - end - - -- logic mostly lifted from ssl-enum-ciphers - local hello = tls.client_hello() - local status, err = socket:send(hello) - if not status then - mutex "done" - return false, "Failed to connect to server" - end - - local get_next_record = get_record_iter(socket) - local records = {} - while true do - local record - record, err = get_next_record() - if not record then - stdnse.debug1("no record: %s", err) - socket:close() - break - end - -- Collect message bodies into one record per type - records[record.type] = records[record.type] or record - local done = false - 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 - if done then - socket:close() - break - end - end - - local handshake = records.handshake - if not handshake then - mutex "done" - 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 - mutex "done" - return false, "Server sent no certificate" - end - - cert, err = parse_ssl_certificate(certs.certificates[1]) - if not cert then - mutex "done" - return false, ("Unable to get cert: %s"):format(err) - end - else - -- If we don't already know the service is TLS wrapped check to see if - -- there a specialized function for this port - local specialized - if not ( port.version.service_tunnel == 'ssl' ) then - specialized = SPECIALIZED_PREPARE_TLS[port.service] or SPECIALIZED_PREPARE_TLS[port.number] - end - local status - local socket = nmap.new_socket() - if specialized then - status, socket = specialized(host, port) - if not status then - mutex "done" - stdnse.debug1("Specialized function error: %s", socket) - return false, "Failed to connect to server" - end + stdnse.debug1("Specialized function error: %s", socket) else - status = socket:connect(host, port, "ssl") - if ( not(status) ) then - mutex "done" - return false, "Failed to connect to server" - end + cert = socket:get_ssl_certificate() + status = not not cert + socket:close() end - cert = socket:get_ssl_certificate() - if ( cert == nil ) then - mutex "done" - return false, "Unable to get cert" + end + + -- Now try to connect with Nsock's SSL connection + if not status 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