diff --git a/CHANGELOG b/CHANGELOG index 3f1ec44d2..2befd9204 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Add new script broadcast-ms-sql-discover and removed broadcast + support from ms-sql-info. [Patrik] + o [NSE] Added the ftp-proftpd-backdoor.nse script by mak Kolybabi, which checks for a backdoor in ProFTPD 1.3.3c. diff --git a/scripts/broadcast-ms-sql-discover.nse b/scripts/broadcast-ms-sql-discover.nse new file mode 100644 index 000000000..e6394575d --- /dev/null +++ b/scripts/broadcast-ms-sql-discover.nse @@ -0,0 +1,56 @@ +description = [[ +Discovers Microsoft SQL servers in the same broadcast domain. +]] + + +-- +-- Version 0.1 +-- Created 07/12/2010 - v0.1 - created by Patrik Karlsson + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery"} + +require 'mssql' +require 'target' + +prerule = function() return true end + +action = function() + + local OUTPUT_TBL = { + ["Server name"] = "info.servername", + ["Version"] = "version.version", + ["Clustered"] = "info.clustered", + ["Named pipe"] = "info.pipe", + ["Tcp port"] = "info.port" + } + + local status, result = mssql.Helper.Discover("255.255.255.255", 1434, true) + if ( not(status) ) then return end + + local results = {} + for ip, instances in pairs(result) do + local result_part = {} + if target.ALLOW_NEW_TARGETS then target.add(ip) end + for name, info in pairs(instances) do + local instance = {} + local version + status, version = mssql.Util.DecodeBrowserInfoVersion(info) + + for topic, varname in pairs(OUTPUT_TBL) do + local func = loadstring( "return " .. varname ) + setfenv(func, setmetatable({ info=info; version=version; }, {__index = _G})) + local result = func() + if ( result ) then + table.insert( instance, ("%s: %s"):format(topic, result) ) + end + end + instance.name = version.product + table.insert( result_part, { name = "Instance: " .. info.name, instance } ) + end + result_part.name = ip + table.insert( results, result_part ) + end + return stdnse.format_output( true, results ) +end \ No newline at end of file diff --git a/scripts/ms-sql-info.nse b/scripts/ms-sql-info.nse index 3a5c2e149..8799014f5 100644 --- a/scripts/ms-sql-info.nse +++ b/scripts/ms-sql-info.nse @@ -23,319 +23,148 @@ categories = {"default", "discovery", "intrusive"} -- | TCP Port: 1433 -- |_ Could not retrieve actual version information -- --- PORT STATE SERVICE --- 1434/udp open|filtered ms-sql-m --- | ms-sql-info: --- | 10.0.200.133 --- | Discovered Microsoft SQL Server 2008 Express Edition --- | Server name: WIN2K3-EPI-1 --- | Server version: 10.0.1600.22 (RTM) --- | Instance name: SQLEXPRESS --- | TCP Port: 1052 --- | Could not retrieve actual version information --- | 10.0.200.119 --- | Discovered Microsoft SQL Server 2000 --- | Server name: EDUSRV011 --- | Instance name: MSSQLSERVER --- | TCP Port: 1433 --- | Could not retrieve actual version information --- | Instance name: SQLEXPRESS --- | TCP Port: 1433 --- |_ Could not retrieve actual version information -require "stdnse" -require "shortport" -require "strbuf" -require "target" +require("shortport") +require("target") +require("mssql") -prerule = function() return true end +prerule = function() return false end portrule = shortport.portnumber({1433, 1434}, "udp", {"open", "open|filtered"}) -local function process_response( response ) + +local parse_version = function(ver_str) + + local version = {} + version.full = ver_str + version.product_long = ver_str:match("(Microsoft.-)\n") or "" + version.product = ver_str:match("^(Microsoft SQL Server %w-)%s") or "" + version.edition = ver_str:match("\n.-\n.-\n%s*(.-%sEdition)%s") or "" + version.edition_long = ver_str:match("\n.-\n.-\n%s*(.-Build.-)\n") or "" + version.version = ver_str:match("^Microsoft.-%-.-([%.%d+]+)") or "" + version.level = ver_str:match("^Microsoft.-%((.+)%)%s%-") or "" + version.windows = ver_str:match(" on%s(.*)\n$") or "" + version.real = true - 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 - - result = {} - - -- do some heuristics on the version to see if we can match the major releases - if string.match(serverInfo[1].version, "^6%.0") then - table.insert(result, "Discovered Microsoft SQL Server 6.0") - elseif string.match(serverInfo[1].version, "^6%.5") then - table.insert(result, "Discovered Microsoft SQL Server 6.5") - elseif string.match(serverInfo[1].version, "^7%.0") then - table.insert(result, "Discovered Microsoft SQL Server 7.0") - elseif string.match(serverInfo[1].version, "^8%.0") then - table.insert(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 - table.insert(result, "Discovered Microsoft SQL Server 2005 Express Edition") - end - end - if result == nil then - table.insert(result, "Discovered Microsoft SQL Server 2005") - end - elseif string.match(serverInfo[1].version, "^10%.0") then - -- The Express Edition of MS SQL Server 2008 has a default instance name of SQLEXPRESS - for _,instance in ipairs(serverInfo) do - if string.match(instance.instanceName, "SQLEXPRESS") then - table.insert(result, "Discovered Microsoft SQL Server 2008 Express Edition") - end - end - if result == nil then - table.insert(result, "Discovered Microsoft SQL Server 2008") - end + return true, version +end + +local function retrieve_version_as_user( info, user, pass ) + + local helper, status + local SQL_DB = "master" + + if ( info.servername and info.port ) then + local hosts + status, hosts = nmap.resolve(info.servername, nmap.address_family()) + + if ( status ) then + local err + for _, host in ipairs( hosts ) do + helper = mssql.Helper:new() + status, err = helper:Connect(host, info.port) + if ( status ) then break end + end + -- we failed to connect to all of the resolved hostnames, + -- fall back to sql browser ip + if ( not(status) ) then + helper = mssql.Helper:new() + status, err = helper:Connect( info.ip, info.port ) + end + else + -- resolve wasn't successful, fall back to browser service ip + stdnse.print_debug(3, "ERROR: Failed to resolve the hostname %s", info.servername) + helper = mssql.Helper:new() + status, err = helper:Connect( info.ip, info.port ) + end else - table.insert(result, "Discovered Microsoft SQL Server") - end - if serverInfo[1].name ~= nil then - table.insert(result, "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 - table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP2)" ) - elseif string.match(serverInfo[1].version, "9.00.3043") then - table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP2)" ) - elseif string.match(serverInfo[1].version, "9.00.2047") then - table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP1)" ) - elseif string.match(serverInfo[1].version, "9.00.1399") then - table.insert(result, "Server version: " .. serverInfo[1].version .. " (RTM)" ) - -- Check for versions of SQL Server 2008 - elseif string.match(serverInfo[1].version, "10.0.1075") then - table.insert(result, "Server version: " .. serverInfo[1].version .. " (CTP)" ) - elseif string.match(serverInfo[1].version, "10.0.1600") then - table.insert(result, "Server version: " .. serverInfo[1].version .. " (RTM)" ) - elseif string.match(serverInfo[1].version, "10.0.2531") then - table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP1)" ) - end + -- we're missing either the servername or the port + return false, "ERROR: Either servername or tcp port is missing" end - return result, serverInfo + if ( not(status) ) then return false, "ERROR: Failed to connect to server" end + + status, result = helper:Login( user, pass, SQL_DB, info.servername ) + if ( not(status) ) then + stdnse.print_debug(3, "%s: login failed, reason: %s", SCRIPT_NAME, result ) + return status, "Could not retrieve actual version information" + end + + local query = "SELECT @@version ver" + status, result = helper:Query( query ) + if ( not(status) ) then + stdnse.print_debug(3, "%s: query failed, reason: %s", SCRIPT_NAME, result ) + return status, "Could not retrieve actual version information" + end + + helper:Disconnect() + + if ( result.rows ) then return parse_version( result.rows[1][1] ) end end --- 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 = strbuf.new() - local stat, resp - - -- do some exception handling / cleanup - local catch = function() socket:close() end - local try = nmap.new_try(catch) - - - -- build a TDS packet - type 0x12 - -- copied from packet capture of osql connection - payload = 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(strbuf.dump(payload))) - - -- read in any response we might get - stat, resp = socket:receive_bytes(1) - - if string.match(resp, "^\004") then +local function process_response( serverInfo ) - -- 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 = strbuf.new() - query = 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(strbuf.dump(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 - table.insert( outcome, "sa user appears to have blank password" ) - - strbuf.clear(query) - -- since we have a successful login, send a query that will tell us what version the server is really running - query = 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(strbuf.dump(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", "") - table.insert( outcome, string.match(banner, "(Microsoft.-)\n") ) - table.insert( outcome, string.match(banner, "\n.-\n.-\n(.-Build.-)\n") ) - end - - try(socket:close()) - - end -- if string.match(response, "^\004") - - if #outcome == 0 then - table.insert( outcome, "Could not retrieve actual version information" ) - end - - return outcome -end -- get_real_version(dst, dstPort) + local SQL_USER, SQL_PASS = "sa", "" + local TABLE_DATA = { + ["Server name"] = "info.servername", + ["Server version"] = "version.version", + ["Server edition"] = "version.edition_long", + ["Clustered"] = "info.clustered", + ["Named pipe"] = "info.pipe", + ["Tcp port"] = "info.port", + } - -preaction = function() - - local host, port = "255.255.255.255", 1434 - -- create the socket used for our connection - local socket = nmap.new_socket("udp") - -- set a reasonable timeout value - socket:set_timeout(5000) + local result = {} - -- do some exception handling / cleanup - local catch = function() socket:close() end - local try = nmap.new_try(catch) - - -- send a magic packet - -- details here: http://www.codeproject.com/cs/database/locate_sql_servers.asp - try(socket:sendto(host, port, "\002")) - - local output = {} - - while(true) do - -- read in any response we might get - local status, response = socket:receive() - if ( not(status) ) then break end + for _, info in pairs(serverInfo) do + local result_part = {} - local status, _, _, ip, _ = socket:get_info() - if ( not(status) ) then return end - - local result, serverInfo = process_response( response ) - for _,instance in ipairs(serverInfo) do - if instance.instanceName ~= nil then - table.insert(result, "Instance name: " .. instance.instanceName) + -- The browser service could point to instances on other IP's + -- therefore the correct behavior should be to connect to the + -- servername returned for the instance rather than the browser IP. + -- In case this fails, due to name resolution or something else, fall + -- back to the browser service IP. + local status, version = retrieve_version_as_user(info, SQL_USER, SQL_PASS) + + if (status) then + if ( version.edition ) then + version.product = version.product .. " " .. version.edition end - if instance.tcpPort ~= nil then - table.insert(result, "TCP Port: " .. instance.tcpPort) - table.insert(result, get_real_version(ip, instance.tcpPort) ) + version.version = version.version .. (" (%s)"):format(version.level) + else + status, version = mssql.Util.DecodeBrowserInfoVersion(info) + end + + -- format output + for topic, varname in pairs(TABLE_DATA) do + local func = loadstring( "return " .. varname ) + setfenv(func, setmetatable({ info=info; version=version; }, {__index = _G})) + local result = func() + if ( result ) then + table.insert( result_part, ("%s: %s"):format(topic, result) ) end end + result_part.name = version.product - if target.ALLOW_NEW_TARGETS then - target.add(ip) + if ( version.real ) then + table.insert(result_part, "WARNING: Database was accessible as SA with empty password!") end - result.name = ip - table.insert( output, result ) + table.insert(result, { name = "Instance: " .. info.name, result_part } ) end - - socket:close() - return stdnse.format_output( true, output ) - + return result end -scanaction = 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(socket:connect(host, port, "udp")) - - -- send a magic packet - -- details here: http://www.codeproject.com/cs/database/locate_sql_servers.asp - try(socket:send("\002")) - - -- read in any response we might get - local status, response = socket:receive() - if (not(status)) then - socket:close() - return - end - - local _, _, ip, _ = try(socket:get_info()) +action = function( host, port ) - local result, serverInfo = process_response( response ) - for _,instance in ipairs(serverInfo) do - if instance.instanceName ~= nil then - table.insert(result, "Instance name: " .. instance.instanceName) - end - if instance.tcpPort ~= nil then - table.insert(result, "TCP Port: " .. instance.tcpPort) - table.insert(result, get_real_version(ip, instance.tcpPort) ) - end - end - socket:close() + local status, response = mssql.Helper.Discover( host, port ) + if ( not(status) ) then return end + + local result, serverInfo = process_response( response[host.ip] ) + if ( not(result) ) then return end nmap.set_port_state( host, port, "open") return stdnse.format_output( true, result ) end --- Function dispatch table -local actions = { - prerule = preaction, - hostrule = scanaction, - portrule = scanaction, -} - -function action (...) return actions[SCRIPT_TYPE](...) end - diff --git a/scripts/script.db b/scripts/script.db index fe7a42acc..d64313247 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -7,6 +7,7 @@ Entry { filename = "auth-owners.nse", categories = { "default", "safe", } } Entry { filename = "auth-spoof.nse", categories = { "malware", "safe", } } Entry { filename = "banner.nse", categories = { "discovery", "safe", } } Entry { filename = "broadcast-dns-service-discovery.nse", categories = { "discovery", "safe", } } +Entry { filename = "broadcast-ms-sql-discover.nse", categories = { "discovery", } } Entry { filename = "broadcast-upnp-info.nse", categories = { "discovery", "safe", } } Entry { filename = "broadcast-wsdd-discover.nse", categories = { "discovery", "safe", } } Entry { filename = "citrix-brute-xml.nse", categories = { "auth", "intrusive", } }