Nmap Development mailing list archives

[NSE Script] Microsoft SQL Server detection


From: Thomas Buchanan <tbuchanan () thecompassgrp net>
Date: Tue, 12 Jun 2007 10:54:05 -0500

Here's a script that tries to dig a little deeper than the MS SQL service detection probe. It reports some of the same information as the service detection, but then takes that information and does a little more with it.

Here's an example of the version detection (-sV) output for a SQL Server 2000 system:

PORT     STATE  SERVICE  VERSION
1434/udp open ms-sql-m Microsoft SQL Server 8.00.194 (ServerName: NT4SERVER-SP2; TCPPort: 1433)


Here's the output from the script that I worked up, run against that same server:

PORT     STATE  SERVICE
1434/udp open   ms-sql-m
|  MS SQL: Discovered Microsoft SQL Server 2000
|    Server name: NT4SERVER-SP2
|    Server version: 8.00.194
|    Instance name: MSSQLSERVER
|    TCP Port: 1433
|      sa user appears to have blank password
|       Microsoft SQL Server  2000 - 8.00.534 (Intel X86)
|_      Enterprise Edition on Windows NT 4.0 (Build 1381: Service Pack 6)


Some of the features of the script:
- Checks for blank 'sa' password
- Handles more than one instance of SQL Server running on the same system
- Makes TCP connections to the port listed for each instance
- Knows a little about specific versions of SQL Server, such as service pack levels for SQL Server 2005

PORT     STATE  SERVICE
1434/udp open   ms-sql-m
|  MS SQL: Discovered Microsoft SQL Server 2000
|    Server name: WINXP
|    Server version: 8.00.194
|    Instance name: MSSQLSERVER
|    TCP Port: 1433
|      Could not retrieve actual version information
|    Instance name: MSDE
|    TCP Port: 1336
|      sa user appears to have blank password
|       Microsoft SQL Server  2000 - 8.00.760 (Intel X86)
|_      Desktop Engine on Windows NT 5.1 (Build 2600: Service Pack 2)


PORT     STATE  SERVICE
1434/udp open   ms-sql-m
|  MS SQL: Discovered Microsoft SQL Server 2005 Express Edition
|    Server name: WINXP1
|    Server version: 9.00.3042.00 (SP2)
|    Instance name: SQLEXPRESS
|    TCP Port: 3061
|_     Could not retrieve actual version information



I've only been able to test this script against SQL Server 2000 and 2005. If anybody can test this against SQL Server 6.5 or 7, and let me know what happens, I'd be very interested.

This script might cause some problems with nmap output under certain circumstances. I've only observed the issue from one specific system, but if you notice anything, please report it.

Hope somebody finds this useful.  Enjoy,

Thomas
-- Microsoft SQL Server information gathering script
-- rev 1.0 (2007-06-09)

id = "MS SQL"

description = "Attempts to extract information from Microsoft SQL Server"

author = "Thomas Buchanan <tbuchanan () thecompassgrp net>"

license = "See nmaps COPYING for licence"

categories = {"discovery", "intrusive"}

portrule = function(host, port) 
        if 
                (       port.number == 1433
                        or port.number == 1434 )
                and port.protocol == "udp"
                -- if we only run against open or open|filtered ports anyway, leave next two lines commented
                -- and ( port.state == "open"
                -- or port.state == "open|filtered" )
        then
                return true
        else
                return false
        end
end

action = function(host, port)

        -- create the socket used for our connection
        local socket = nmap.new_socket()
        
        -- set a reasonable timeout value
        socket:set_timeout(5000)
        
        -- do some exception handling / cleanup
        local catch = function()
                socket:close()
        end
        
        local try = nmap.new_try(catch)
        
        -- try to login to MS SQL network service, and obtain the real version information
        -- MS SQL 2000 does not report the correct version in the data sent in response to UDP probe (see below)
        local get_real_version = function(dst, dstPort)
          
          local outcome
          local payload
          
          local stat, resp
          
          -- build a TDS packet - type 0x12
          -- copied from packet capture of osql connection
          payload = "\018\001\000\047\000\000\001\000\000\000"
          payload = payload .. "\026\000\006\001\000\032\000\001\002\000"
          payload = payload .. "\033\000\001\003\000\034\000\004\004\000"
          payload = payload .. "\038\000\001\255\009\000\011\226\000\000"
          payload = payload .. "\000\000\120\023\000\000\000"
          
          socket = nmap.new_socket()
          
          -- connect to the server using the tcpPort captured from the UDP probe
          try(socket:connect(dst, dstPort, "tcp"))
          
          try(socket:send(payload))
          
          -- read in any response we might get
          stat, resp = socket:receive_bytes(1)
          
          if string.match(resp, "^\004") then
                
            -- build a login packet to send to SQL server
            -- username = sa, blank password
            -- for information about packet structure, see http://www.freetds.org/tds.html
            
            local query = "\016\001\000\128\000\000\001\000" -- TDS packet header
            query = query .. "\120\000\000\000\002\000\009\114" -- Login packet header = length, version
            query = query .. "\000\000\000\000\000\000\000\007" -- Login packet header continued = size, client version
            query = query .. "\140\018\000\000\000\000\000\000" -- Login packet header continued = Client PID, 
Connection ID
            query = query .. "\224\003\000\000\104\001\000\000" -- Login packet header continued = Option Flags 1 & 2, 
status flag, reserved flag, timezone
            query = query .. "\009\004\000\000\094\000\004\000" -- Login packet (Collation), then start offsets & 
lengths (client name, client length)
            query = query .. "\102\000\002\000\000\000\000\000" -- Login packet, offsets & lengths = username offset, 
username length, password offset, password length
            query = query .. "\106\000\004\000\114\000\000\000" -- Login packet, offsets & lengths = app name offset, 
app name length, server name offset, server name length
            query = query .. "\000\000\000\000\114\000\003\000" -- Login packet, offsets & lengths = unknown offset, 
unknown length, library name offset, library name length
            query = query .. "\120\000\000\000\120\000\000\000" -- Login packet, offsets & lengths = locale offset, 
locale length, database name offset, database name length
            query = query .. "\000\000\000\000\000\000\000\000" -- Login packet, MAC address + padding
            query = query .. "\000\000\000\000\000\000\000\000" -- Login packet, padding
            query = query .. "\000\000\000\000\000\000\078\000" -- Login packet, padding + start of client name (N)
            query = query .. "\077\000\065\000\080\000\115\000" -- Login packet = rest of client name (MAP) + username 
(s)
            query = query .. "\097\000\078\000\077\000\065\000" -- Login packet = username (a), app name (NMA)
            query = query .. "\080\000\078\000\083\000\069\000" -- Login packet = app name (P), library name (NSE)
            
            -- send the packet down the wire
            try(socket:send(query))
            
            -- read in any response we might get
            stat, resp = socket:receive_bytes(1)
          
            -- successful response to login packet should contain the string "SQL Server"
            -- however, the string is UCS2 encoded, so we have to add the \000 characters
            if string.match(resp, "S\000Q\000L\000") then
                  outcome = "\n    sa user appears to have blank password"
                  
                  -- since we have a successful login, send a query that will tell us what version the server is really 
running
                  query = "\001\001\000\044\000\000\001\000" -- TDS Query packet
                  query = query .. "\083\000\069\000\076\000\069\000" -- SELE
                  query = query .. "\067\000\084\000\032\000\064\000" -- CT @
                  query = query .. "\064\000\086\000\069\000\082\000" -- @VER
                  query = query .. "\083\000\073\000\079\000\078\000" -- SION
                  query = query .. "\013\000\010\000"
                  
                  -- send the packet down the wire
                  try(socket:send(query))
              
              -- read in any response we might get
              stat, resp = socket:receive_bytes(1)
                  
                  -- strip out the embedded \000 characters
                  local banner = string.gsub(resp, "%z", "")
                  outcome = outcome .. "\n     " .. string.match(banner, "(Microsoft.-)\n")
                  outcome = outcome .. "\n" .. string.match(banner, "\n.-\n.-\n(.-Build.-)\n")
            end
            
            try(socket:close())
            
          end -- if string.match(response, "^\004")
          
          if outcome == nil then
            outcome = "\n    Could not retrieve actual version information"
          end
          
          return outcome
        end -- get_real_version(dst, dstPort)
        
        -- connect to the potential SQL server
        try(socket:connect(host.ip, port.number, "udp"))
        
        -- send a magic packet
        -- details here:  http://www.codeproject.com/cs/database/locate_sql_servers.asp
        try(socket:send("\002"))
        
        local status
        local response
        
        -- read in any response we might get
        status, response = socket:receive_bytes(1)
        
        try(socket:close())

        if (not status) then
                return
        end

        if (response == "TIMEOUT") then
                return
        end
        
        -- since we got something back, the port is definitely open
        nmap.set_port_state(host, port, "open")
        
        local result
        
        -- create a lua table to hold some information
        local serverInfo = {}
                        
        -- do some pattern matching to exract certain key elements from the response
        -- the data comes back as a long semicolon separated list
        
        -- A single server can have multiple instances, which are separated by a double semicolon
        -- cycle through each instance
        local count = 1
        for instance in string.gmatch(response, "(.-;;)") do
          result = instance
          serverInfo[count] = {}
          serverInfo[count].name = string.match(instance, "ServerName;(.-);")
          serverInfo[count].instanceName = string.match(instance, "InstanceName;(.-);")
          serverInfo[count].clustered = string.match(instance, "IsClustered;(.-);")
          serverInfo[count].version = string.match(instance, "Version;(.-);")
          serverInfo[count].tcpPort = string.match(instance, ";tcp;(.-);")
          serverInfo[count].namedPipe = string.match(instance, ";np;(.-);")
          count = count + 1
        end
        
        -- do some heuristics on the version to see if we can match the major releases
        if string.match(serverInfo[1].version, "^6%.0") then
          result = "Discovered Microsoft SQL Server 6.0"
        elseif string.match(serverInfo[1].version, "^6%.5") then
          result = "Discovered Microsoft SQL Server 6.5"
        elseif string.match(serverInfo[1].version, "^7%.0") then
          result = "Discovered Microsoft SQL Server 7.0"
        elseif string.match(serverInfo[1].version, "^8%.0") then
          result = "Discovered Microsoft SQL Server 2000"
        elseif string.match(serverInfo[1].version, "^9%.0") then
          -- The Express Edition of MS SQL Server 2005 has a default instance name of SQLEXPRESS
          for _,instance in ipairs(serverInfo) do
            if string.match(instance.instanceName, "SQLEXPRESS") then
              result = "Discovered Microsoft SQL Server 2005 Express Edition"
            end
          end
          if result == nil then
            result = "Discovered Microsoft SQL Server 2005"
          end
        else
          result = "Discovered Microsoft SQL Server"
        end
        if serverInfo[1].name ~= nil then
          result = result .. "\n  Server name: " .. serverInfo[1].name
        end
        if serverInfo[1].version ~= nil then
          result = result .. "\n  Server version: " .. serverInfo[1].version
          -- Check for some well known release versions of SQL Server 2005
          --  for more info, see http://support.microsoft.com/kb/321185
          if string.match(serverInfo[1].version, "9.00.3042") then
            result = result .. " (SP2)"
          elseif string.match(serverInfo[1].version, "9.00.3043") then
            result = result .. " (SP2)"
          elseif string.match(serverInfo[1].version, "9.00.2047") then
            result = result .. " (SP1)"
          elseif string.match(serverInfo[1].version, "9.00.1399") then
            result = result .. " (RTM)"
          end
        end
        for _,instance in ipairs(serverInfo) do
          if instance.instanceName ~= nil then
            result = result .. "\n  Instance name: " .. instance.instanceName
          end
          if instance.tcpPort ~= nil then
            result = result .. "\n  TCP Port: " .. instance.tcpPort
                result = result .. get_real_version(host.ip, instance.tcpPort)
          end
    end
        
        return result
        
end

_______________________________________________
Sent through the nmap-dev mailing list
http://cgi.insecure.org/mailman/listinfo/nmap-dev
Archived at http://SecLists.Org

Current thread: