1
0
mirror of https://github.com/nmap/nmap.git synced 2026-02-13 00:46:32 +00:00

Allow ssl-* to work with TDS (MS SQL server)

This commit is contained in:
dmiller
2016-03-19 20:44:33 +00:00
parent 7a430d154b
commit 2b86ab11dc
2 changed files with 329 additions and 18 deletions

View File

@@ -1,5 +1,9 @@
# Nmap Changelog ($Id$); -*-text-*-
o [NSE] Added support in sslcert.lua for Microsoft SQL Server's TDS protocol,
so you can now grab certs with ssl-cert or check ciphers with
ssl-enum-ciphers. [Daniel Miller]
Nmap 7.10 [2016-03-17]
o [NSE] Added 12 NSE scripts from 7 authors, bringing the total up to 527!

View File

@@ -19,13 +19,17 @@
local asn1 = require "asn1"
local bin = require "bin"
local bit = require "bit"
local comm = require "comm"
local ftp = require "ftp"
local ldap = require "ldap"
local mssql = require "mssql"
local nmap = require "nmap"
local smtp = require "smtp"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local tls = require "tls"
local xmpp = require "xmpp"
_ENV = stdnse.module("sslcert", stdnse.seeall)
@@ -56,6 +60,104 @@ local function tls_reconnect (func)
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 = {
-- TODO: Implement STARTTLS for NNTP
@@ -279,6 +381,110 @@ StartTLS = {
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 pos, optype, oppos, oplen = bin.unpack('>CSS', 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 = bin.unpack('C', result, oppos + 1)
break
end
pos, optype, oppos, oplen = bin.unpack('>CSS', result, pos)
end
if not encryption then
return false, "no encryption option found"
elseif encryption == 0 then
return false, "Server refused encryption"
elseif encryption == 3 then
return false, "Server does not support encryption"
end
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 recieving 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
pos, packetType, messageStatus, packetLength = bin.unpack(">CCS", readBuffer, pos )
local spid, packetId, window
pos, spid, packetId, window = bin.unpack(">SCC", 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 = (
bit.band( 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,
xmpp_prepare_tls_without_reconnect = function(host,port)
local sock,status,err,result
local xmppStreamStart = string.format("<?xml version='1.0' ?>\r\n<stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' to='%s' version='1.0'>\r\n",host.name)
@@ -350,6 +556,7 @@ StartTLS = {
end
}
-- A table mapping port numbers to specialized SSL negotiation functions.
local SPECIALIZED_PREPARE_TLS = {
ftp = StartTLS.ftp_prepare_tls,
@@ -367,7 +574,8 @@ local SPECIALIZED_PREPARE_TLS = {
[587] = StartTLS.smtp_prepare_tls,
xmpp = StartTLS.xmpp_prepare_tls,
[5222] = StartTLS.xmpp_prepare_tls,
[5269] = StartTLS.xmpp_prepare_tls
[5269] = StartTLS.xmpp_prepare_tls,
["ms-sql-s"] = StartTLS.tds_prepare_tls
}
local SPECIALIZED_PREPARE_TLS_WITHOUT_RECONNECT = {
@@ -389,6 +597,11 @@ local SPECIALIZED_PREPARE_TLS_WITHOUT_RECONNECT = {
[5269] = StartTLS.xmpp_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,
}
--- Get a specialized SSL connection function without starting SSL
--
-- For protocols that require some sort of START-TLS setup, this function will
@@ -401,7 +614,9 @@ function getPrepareTLSWithoutReconnect(port)
return nil
end
return (SPECIALIZED_PREPARE_TLS_WITHOUT_RECONNECT[port.number] or
SPECIALIZED_PREPARE_TLS_WITHOUT_RECONNECT[port.service])
SPECIALIZED_PREPARE_TLS_WITHOUT_RECONNECT[port.service] or
SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.number] or
SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.service])
end
--- Get a specialized SSL connection function to create an SSL socket
@@ -418,6 +633,30 @@ function isPortSupported(port)
SPECIALIZED_PREPARE_TLS[port.service])
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
i, record = tls.record_read(buffer, i, fragment)
if record == nil then
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
--- 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
@@ -436,27 +675,95 @@ function getCertificate(host, port)
return true, host.registry["ssl-cert"][port.number]
end
-- Is there a specialized function for this port?
local specialized = SPECIALIZED_PREPARE_TLS[port.number]
local status
local socket = nmap.new_socket()
if specialized then
status, socket = specialized(host, port)
local cert
-- do we have to use a wrapper and do a manual handshake?
local wrapper = SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.number] or SPECIALIZED_WRAPPED_TLS_WITHOUT_RECONNECT[port.service]
if wrapper then
local status, socket = wrapper(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
else
local status
status = socket:connect(host, port, "ssl")
if ( not(status) ) then
mutex "done"
return false, "Failed to connect to server"
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 = parse_ssl_certificate(certs.certificates[1])
if not cert then
mutex "done"
return false, "Unable to get cert"
end
else
-- Is there a specialized function for this port?
local specialized = SPECIALIZED_PREPARE_TLS[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
status = socket:connect(host, port, "ssl")
if ( not(status) ) then
mutex "done"
return false, "Failed to connect to server"
end
end
cert = socket:get_ssl_certificate()
if ( cert == nil ) then
mutex "done"
return false, "Unable to get cert"
end
end
local cert = socket:get_ssl_certificate()
if ( cert == nil ) then
return false, "Unable to get cert"
end
host.registry["ssl-cert"] = host.registry["ssl-cert"] or {}