Merge from /nmap-exp/david/nmap-unconnected. This adds unconnected

socket support to NSE, with updates in scripts and libraries.

o [NSE] Added the ability to send and receive on unconnected sockets.
  This can be used, for example, to receive UDP broadcasts without
  using pcap. A number of scripts have been changed so that they can
  work as prerule scripts to discover services by UDP broadcasting,
  optionally add the discovered targets to the scanning queue:
    - ms-sql-info
    - upnp-info
    - dns-service-discovery
  The nmap.new_socket function can now optionally take a default
  protocol and address family, which will be used if the socket is not
  connected. There is a new nmap.sendto function to be used with
  unconnected UDP sockets. [David, Patrik]
This commit is contained in:
david 2010-10-16 00:48:44 +00:00
parent 20cbbfb014
commit 18157ed081
9 changed files with 992 additions and 370 deletions

133
scripts/db2-discover.nse Normal file
View file

@ -0,0 +1,133 @@
description = [[
Attempts do discover DB2 servers on the network using UDP
]]
---
-- @usage
-- sudo ./nmap -sU -p 523 --script db2-discover <ip>
--
-- @output
-- PORT STATE SERVICE
-- 523/udp open ibm-db2
-- | db2-discover:
-- | 10.0.200.132 (UBU804-DB2E) - IBM DB2 v9.07.0
-- |_ 10.0.200.119 (EDUSRV011) - IBM DB2 v9.07.0
-- Version 0.1
-- Created 08/27/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 10/10/2010 - v0.2 - add prerule, newtargets <patrik@cqure.net>
author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"safe", "discovery"}
require "stdnse"
require "shortport"
require "target"
prerule = function() return true end
portrule = shortport.version_port_or_service(523, "ibm-db2", "udp",
{"open", "open|filtered"})
--- Converts the prodrel server string to a version string
--
-- @param server_version string containing the product release
-- @return ver string containing the version information
local function parseVersion( server_version )
local pfx = string.sub(server_version,1,3)
if pfx == "SQL" then
local major_version = string.sub(server_version,4,5)
-- strip the leading 0 from the major version, for consistency with
-- nmap-service-probes results
if string.sub(major_version,1,1) == "0" then
major_version = string.sub(major_version,2)
end
local minor_version = string.sub(server_version,6,7)
local hotfix = string.sub(server_version,8)
server_version = major_version .. "." .. minor_version .. "." .. hotfix
else
return "Unknown version"
end
return ("IBM DB2 v%s"):format(server_version)
end
preaction = function()
local DB2GETADDR = "DB2GETADDR\0SQL09010\0"
local socket = nmap.new_socket("udp")
local result = {}
local host, port = "255.255.255.255", 523
socket:set_timeout(5000)
local status = socket:sendto( host, port, DB2GETADDR )
if ( not(status) ) then return end
while(true) do
local data
status, data = socket:receive()
if( not(status) ) then break end
local version, srvname = data:match("DB2RETADDR.(SQL%d+).(.-)%z")
local _, ip
status, _, _, ip, _ = socket:get_info()
if ( not(status) ) then return end
if target.ALLOW_NEW_TARGETS then target.add(ip) end
if ( status ) then
table.insert( result, ("%s - Host: %s; Version: %s"):format(ip, srvname, parseVersion( version ) ) )
end
end
socket:close()
return stdnse.format_output( true, result )
end
scanaction = function(host, port)
local DB2GETADDR = "DB2GETADDR\0SQL09010\0"
local socket = nmap.new_socket()
local result = {}
socket:set_timeout(5000)
local status, err = socket:connect( host, port, "udp")
if ( not(status) ) then return end
status, err = socket:send( DB2GETADDR )
if ( not(status) ) then return end
local data
status, data = socket:receive()
if( not(status) ) then
socket:close()
return
end
local version, srvname = data:match("DB2RETADDR.(SQL%d+).(.-)%z")
if ( status ) then
table.insert( result, ("Host: %s"):format(srvname) )
table.insert( result, ("Version: %s"):format(parseVersion(version)) )
end
socket:close()
-- set port to open
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

View file

@ -33,10 +33,13 @@ get more information.
-- |_ Address=192.168.0.2 fe80:0:0:0:223:6cff:1234:5678
-- Version 0.3
-- Version 0.6
-- Created 01/06/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 01/13/2010 - v0.2 - modified to use existing dns library instead of mdns, changed output to be less DNS like
-- Revised 02/01/2010 - v0.3 - removed incorrect try/catch statements
-- Revised 10/04/2010 - v0.4 - added prerule and add target support <patrik@cqure.net>
-- Revised 10/05/2010 - v0.5 - added ip sort function and
-- Revised 10/10/2010 - v0.6 - multicast queries are now used in parallel to collect service information <patrik@cqure.net>
author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
@ -44,8 +47,10 @@ categories = {"default", "discovery", "safe"}
require 'shortport'
require 'dns'
require 'target'
portrule = shortport.portnumber(5353, "udp")
prerule = function() return true end
--- Gets a record from both the Answer and Additional section
--
@ -95,8 +100,10 @@ end
-- @param b table containing second item
-- @return true if the port of a is less than the port of b
local function serviceCompare(a, b)
local port_a = a.name:match("^(%d+)") or 0
local port_b = b.name:match("^(%d+)") or 0
-- if no port is found use 999999 for comparing, this way all services
-- without ports and device information gets printed at the end
local port_a = a.name:match("^(%d+)") or 999999
local port_b = b.name:match("^(%d+)") or 999999
if ( tonumber(port_a) < tonumber(port_b) ) then
return true
@ -104,97 +111,239 @@ local function serviceCompare(a, b)
return false
end
action = function(host, port)
--- Converts a string ip to a numeric value suitable for comparing
--
-- @param ip string containing the ip to convert
-- @return number containing the converted ip
local function ipToNumber(ip)
local o1, o2, o3, o4 = ip:match("^(%d*)%.(%d*)%.(%d*)%.(%d*)$")
return (256^3) * o1 + (256^2) * o2 + (256^1) * o3 + (256^0) * o4
end
local result = {}
local deviceinfo = {}
local status, response = dns.query( "_services._dns-sd._udp.local", { port = 5353, host = host.ip, dtype="PTR", retAll=true} )
--- Compare function used for sorting IP-addresses
--
-- @param a table containing first item
-- @param b table containing second item
-- @return true if the port of a is less than the port of b
local function ipCompare(a, b)
local ip_a = ipToNumber(a.name) or 0
local ip_b = ipToNumber(b.name) or 0
if not status then
if ( tonumber(ip_a) < tonumber(ip_b) ) then
return true
end
return false
end
--- Send a query for a particular service and store the response in a table
--
-- @param host string containing the ip to connect to
-- @param port number containing the port to connect to
-- @param svc the service record to retrieve
-- @param multiple true if responses from multiple hosts are expected
-- @param svcresponse table to which results are stored
local function queryService( host, port, svc, multiple, svcresponse )
local condvar = nmap.condvar(svcresponse)
local status, response = dns.query( svc, { port = port, host = host, dtype="PTR", retPkt=true, retAll=true, multiple=multiple, sendCount=1, timeout=2000} )
if not status then
stdnse.print_debug("Failed to query service: %s; Error: %s", svc, response)
return
end
svcresponse[svc] = svcresponse[svc] or {}
if ( multiple ) then
for _, r in ipairs(response) do
table.insert( svcresponse[svc], r )
end
else
svcresponse[svc] = response
end
condvar("broadcast")
end
--- Sends a unicast query for each discovered service to each host
--
-- @param host string containing the ip to connect to
-- @param record string containing the DNS record to query
-- @param result table to which the results are added
local function processRecords( response, result )
local service, deviceinfo = {}, {}
local txt = {}
local ip, ipv6, srv, address, port, proto
-- for each service response in answers, send a service query
for _, v in ipairs( response ) do
local record = ( #response.questions > 0 and response.questions[1].dname ) and response.questions[1].dname or ""
local service = {}
local txt = {}
local ip, ipv6, srv, address, port, proto
status, response = dns.query( v, { port = 5353, host = host.ip, dtype="PTR", retPkt=true} )
if not status then
return
end
status, ip = getRecordType( dns.types.A, response, false )
if status then
address = ip
end
status, ipv6 = getRecordType( dns.types.AAAA, response, false )
if status then
address = address .. " " .. ipv6
end
status, txt = getRecordType( dns.types.TXT, response, true )
if status then
for _, v in ipairs(txt) do
if v:len() > 0 then
table.insert(service, v)
end
status, ip = getRecordType( dns.types.A, response, false )
if status then address = ip end
status, ipv6 = getRecordType( dns.types.AAAA, response, false )
if status then address = address .. " " .. ipv6 end
status, txt = getRecordType( dns.types.TXT, response, true )
if status then
for _, v in ipairs(txt) do
if v:len() > 0 then
table.insert(service, v)
end
end
end
status, srv = getRecordType( dns.types.SRV, response, false )
if status then
local srvparams = stdnse.strsplit( ":", srv )
status, srv = getRecordType( dns.types.SRV, response, false )
if status then
local srvparams = stdnse.strsplit( ":", srv )
if #srvparams > 3 then
port = srvparams[3]
end
end
if #srvparams > 3 then
port = srvparams[3]
if address then
table.insert( service, ("Address=%s"):format( address ) )
end
if record == "_device-info._tcp.local" then
service.name = "Device Information"
deviceinfo = service
table.insert(result, deviceinfo)
else
local serviceparams = stdnse.strsplit("[.]", record)
if #serviceparams > 2 then
local servicename = serviceparams[1]:sub(2)
local proto = serviceparams[2]:sub(2)
if port == nil or proto == nil or servicename == nil then
service.name = record
else
service.name = string.format( "%s/%s %s", port, proto, servicename)
end
end
if address then
table.insert( service, ("Address=%s"):format( address ) )
end
table.insert( result, service )
end
if v == "_device-info._tcp.local" then
service.name = "Device Information"
deviceinfo = service
end
--- Returns the amount of currenlty active threads
--
-- @param threads table containing the list of threads
-- @return count number containing the number of non-dead threads
threadCount = function( threads )
local count = 0
for thread in pairs(threads) do
if ( coroutine.status(thread) == "dead" ) then
threads[thread] = nil
else
local serviceparams = stdnse.strsplit("[.]", v)
if #serviceparams > 2 then
local servicename = serviceparams[1]:sub(2)
local proto = serviceparams[2]:sub(2)
if port == nil or proto == nil or servicename == nil then
service.name = v
else
service.name = string.format( "%s/%s %s", port, proto, servicename)
end
end
table.insert( result, service )
count = count + 1
end
end
return count
end
--- Creates a service host table
--
-- ['_ftp._tcp.local'] = {10.10.10.10,20.20.20.20}
-- ['_http._tcp.local'] = {30.30.30.30,40.40.40.40}
--
-- @param response containing the response from <code>dns.query</code>
-- @return services table containing the service name as a key and all host addresses as value
local function createSvcHostTbl( response )
local services = {}
-- Create unique table of services
for _, r in ipairs( response ) do
for _, svc in ipairs(r.output ) do
services[svc] = services[svc] or {}
table.insert(services[svc], r.peer)
end
end
return services
end
preaction = function()
local result = {}
local host, port = "224.0.0.251", 5353
local status, response = dns.query( "_services._dns-sd._udp.local", { port = port, host = host, dtype="PTR", retAll=true, multiple=true, sendCount=1, timeout=2000} )
if not status then return end
local services = createSvcHostTbl(response)
local ipsvctbl = {}
local svcresponse = {}
local condvar = nmap.condvar( svcresponse )
local threads = {}
-- Start one collector thread for each service
for svc in pairs(services) do
local co = stdnse.new_thread( queryService, host, port, svc, true, svcresponse )
threads[co] = true
end
-- Wait for all threads to finish running
while threadCount(threads)>0 do
condvar("wait")
end
-- Process all records that were returned
for svcname, response in pairs(svcresponse) do
for _, r in ipairs( response ) do
ipsvctbl[r.peer] = ipsvctbl[r.peer] or {}
processRecords( r.output, ipsvctbl[r.peer] )
end
end
-- Restructure and build our output table
for ip, svctbl in pairs( ipsvctbl ) do
table.sort(svctbl, serviceCompare)
svctbl.name = ip
if target.ALLOW_NEW_TARGETS then target.add(ip) end
table.insert( result, svctbl )
end
table.sort( result, ipCompare )
return stdnse.format_output(true, result )
end
scanaction = function(host, port)
local result = {}
local status, response = dns.query( "_services._dns-sd._udp.local", { port = 5353, host = host.ip, dtype="PTR", retAll=true, sendCount=1, timeout=2000 } )
if not status then return end
local svcresponse = {}
local condvar = nmap.condvar( svcresponse )
local threads = {}
-- Start one collector thread for each service
for _, svc in ipairs(response) do
local co = stdnse.new_thread( queryService, host.ip, port, svc, false, svcresponse )
threads[co] = true
end
-- Wait for all threads to finish running
while threadCount(threads)>0 do
condvar("wait")
end
-- Process all records that were returned
for svcname, response in pairs(svcresponse) do
processRecords( response, result )
end
-- sort the tables per port
table.sort( result, serviceCompare )
-- we want the device information at the end
table.insert( result, deviceinfo )
-- set port to open
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

View file

@ -3,6 +3,8 @@ Attempts to extract information from Microsoft SQL Server instances.
]]
-- rev 1.0 (2007-06-09)
-- rev 1.1 (2009-12-06 - Added SQL 2008 identification T Sellers)
-- rev 1.2 (2010-10-03 - Added Broadcast support <patrik@cqure.net>)
-- rev 1.3 (2010-10-10 - Added prerule and newtargets support <patrik@cqure.net>)
author = "Thomas Buchanan"
@ -20,147 +22,36 @@ categories = {"default", "discovery", "intrusive"}
-- | Instance name: SQLEXPRESS
-- | 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 "stdnse"
require "shortport"
require("strbuf")
require "strbuf"
require "target"
prerule = function() return true end
portrule = shortport.portnumber({1433, 1434}, "udp", {"open", "open|filtered"})
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 = strbuf.new()
local stat, resp
-- 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
-- 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
outcome = "\n 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", "")
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, port))
-- 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 function process_response( response )
local result
@ -185,74 +76,266 @@ action = function(host, port)
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
result = "Discovered Microsoft SQL Server 6.0"
table.insert(result, "Discovered Microsoft SQL Server 6.0")
elseif string.match(serverInfo[1].version, "^6%.5") then
result = "Discovered Microsoft SQL Server 6.5"
table.insert(result, "Discovered Microsoft SQL Server 6.5")
elseif string.match(serverInfo[1].version, "^7%.0") then
result = "Discovered Microsoft SQL Server 7.0"
table.insert(result, "Discovered Microsoft SQL Server 7.0")
elseif string.match(serverInfo[1].version, "^8%.0") then
result = "Discovered Microsoft SQL Server 2000"
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
result = "Discovered Microsoft SQL Server 2005 Express Edition"
table.insert(result, "Discovered Microsoft SQL Server 2005 Express Edition")
end
end
if result == nil then
result = "Discovered Microsoft SQL Server 2005"
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
result = "Discovered Microsoft SQL Server 2008 Express Edition"
table.insert(result, "Discovered Microsoft SQL Server 2008 Express Edition")
end
end
if result == nil then
result = "Discovered Microsoft SQL Server 2008"
table.insert(result, "Discovered Microsoft SQL Server 2008")
end
else
result = "Discovered Microsoft SQL Server"
table.insert(result, "Discovered Microsoft SQL Server")
end
if serverInfo[1].name ~= nil then
result = result .. "\n Server name: " .. serverInfo[1].name
table.insert(result, "Server name: " .. serverInfo[1].name)
end
if serverInfo[1].version ~= nil then
result = result .. "\n Server version: " .. serverInfo[1].version
--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)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP2)" )
elseif string.match(serverInfo[1].version, "9.00.3043") then
result = result .. " (SP2)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP2)" )
elseif string.match(serverInfo[1].version, "9.00.2047") then
result = result .. " (SP1)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP1)" )
elseif string.match(serverInfo[1].version, "9.00.1399") then
result = result .. " (RTM)"
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
result = result .. " (CTP)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (CTP)" )
elseif string.match(serverInfo[1].version, "10.0.1600") then
result = result .. " (RTM)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (RTM)" )
elseif string.match(serverInfo[1].version, "10.0.2531") then
result = result .. " (SP1)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP1)" )
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
end
return result
return result, serverInfo
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
-- 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)
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)
-- 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
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)
end
if instance.tcpPort ~= nil then
table.insert(result, "TCP Port: " .. instance.tcpPort)
table.insert(result, get_real_version(ip, instance.tcpPort) )
end
end
if target.ALLOW_NEW_TARGETS then
target.add(ip)
end
result.name = ip
table.insert( output, result )
end
socket:close()
return stdnse.format_output( true, output )
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())
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()
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

View file

@ -7,6 +7,9 @@ Attempts to extract system information from the UPnP service.
-- | upnp-info: System/1.0 UPnP/1.0 IGD/1.0
-- |_ Location: http://192.168.1.1:80/UPnP/IGD.xml
-- 2010-10-05 - add prerule support <patrik@cqure.net>
-- 2010-10-10 - add newtarget support <patrik@cqure.net>
author = "Thomas Buchanan"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
@ -16,69 +19,29 @@ categories = {"default", "safe"}
require("stdnse")
require("shortport")
require("strbuf")
require("target")
prerule = function() return true end
---
-- Runs on UDP port 1900
portrule = shortport.portnumber(1900, "udp", {"open", "open|filtered"})
---
-- Sends UPnP discovery packet to host,
-- and extracts service information from results
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 function process_response( response )
local catch = function() socket:close() end
local try = nmap.new_try(catch)
-- connect to the potential UPnP system
try(socket:connect(host, port))
local payload = strbuf.new()
-- for details about the UPnP message format, see http://upnp.org/resources/documents.asp
payload = payload .. "M-SEARCH * HTTP/1.1\r\n"
payload = payload .. "Host:239.255.255.250:1900\r\n"
payload = payload .. "ST:upnp:rootdevice\r\n"
payload = payload .. "Man:\"ssdp:discover\"\r\n"
payload = payload .. "MX:3\r\n\r\n"
try(socket:send(strbuf.dump(payload)))
local status
local response
-- read in any response we might get
status, response = socket:receive_bytes(1)
if (not status) or (response == "TIMEOUT") then
socket:close()
return
end
-- since we got something back, the port is definitely open
nmap.set_port_state(host, port, "open")
-- buffer to hold script output
local output
local output = {}
if response ~= nil then
-- We should get a response back that has contains one line for the server, and one line for the xml file location
-- these match any combination of upper and lower case responses
local server, location
server = string.match(response, "[Ss][Ee][Rr][Vv][Ee][Rr]:(.-)\010")
if server ~= nil then output = server .. "\n" end
server = string.match(response, "[Ss][Ee][Rr][Vv][Ee][Rr]:%s*(.-)\010")
if server ~= nil then table.insert(output, server ) end
location = string.match(response, "[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:(.-)\010")
if location ~= nil then
output = output .. "Location: " .. location
table.insert(output, "Location: " .. location )
local v = nmap.verbosity()
@ -98,17 +61,14 @@ action = function(host, port)
xport = 80
end
-- check if the IP address in the location matches the IP address we're scanning
-- if not, alert the user, but continue to scan the IP address we're interested in
if xhost ~= host.ip then
output = output .. "\n !! Location did not match target IP address !! "
-- return output
xhost = host.ip
end
local peer = {}
local _
-- extract the path name from the location field, but strip off the \r that HTTP servers return
xfile = string.match(location, "http://.-/(.-)\013")
if xfile ~= nil then
local payload = strbuf.new()
strbuf.clear(payload)
-- create an HTTP request for the file, using the host and port we extracted earlier
payload = payload .. "GET /" .. xfile .. " HTTP/1.1\r\n"
@ -119,7 +79,7 @@ action = function(host, port)
payload = payload .. "Cache-Control: no-cache\r\n"
payload = payload .. "Pragma: no-cache\r\n\r\n"
socket = nmap.new_socket()
local socket = nmap.new_socket()
socket:set_timeout(5000)
try(socket:connect(xhost, xport, "tcp"))
@ -132,7 +92,7 @@ action = function(host, port)
local webserver
-- extract information about the webserver that is handling responses for the UPnP system
webserver = string.match(response, "[Ss][Ee][Rr][Vv][Ee][Rr]:(.-)\010")
if webserver ~= nil then output = output .. "\nWebserver: " .. webserver end
if webserver ~= nil then table.insert(output, "Webserver: " .. webserver) end
-- the schema for UPnP includes a number of <device> entries, which can a number of interesting fields
for device in string.gmatch(response, "<deviceType>(.-)</UDN>") do
@ -144,11 +104,11 @@ action = function(host, port)
nm = string.match(device, "<modelName>(.-)</modelName>")
ver = string.match(device, "<modelNumber>(.-)</modelNumber>")
if fn ~= nil then output = output .. "\n Name: " .. fn end
if mnf ~= nil then output = output .. "\n Manufacturer: " .. mnf end
if mdl ~= nil then output = output .. "\n Model Descr: " .. mdl end
if nm ~= nil then output = output .. "\n Model Name: " .. nm end
if ver ~= nil then output = output .. "\n Model Version: " .. ver end
if fn ~= nil then table.insert(output, "Name: " .. fn) end
if mnf ~= nil then table.insert(output,"Manufacturer: " .. mnf) end
if mdl ~= nil then table.insert(output,"Model Descr: " .. mdl) end
if nm ~= nil then table.insert(output,"Model Name: " .. nm) end
if ver ~= nil then table.insert(output,"Model Version: " .. ver) end
end
end
end
@ -160,3 +120,122 @@ action = function(host, port)
return output
end
end
--- Converts a string ip to a numeric value suitable for comparing
--
-- @param ip string containing the ip to convert
-- @return number containing the converted ip
local function ipToNumber(ip)
local o1, o2, o3, o4 = ip:match("^(%d*)%.(%d*)%.(%d*)%.(%d*)$")
return (256^3) * o1 + (256^2) * o2 + (256^1) * o3 + (256^0) * o4
end
--- Compare function used for sorting IP-addresses
--
-- @param a table containing first item
-- @param b table containing second item
-- @return true if the port of a is less than the port of b
local function ipCompare(a, b)
local ip_a = ipToNumber(a.name)
local ip_b = ipToNumber(b.name)
if ( tonumber(ip_a) < tonumber(ip_b) ) then
return true
end
return false
end
---
-- Sends UPnP discovery packet to host,
-- and extracts service information from results
preaction = function(host, port)
-- create the socket used for our connection
local socket = nmap.new_socket("udp")
-- set a reasonable timeout value
socket:set_timeout(5000)
local payload = strbuf.new()
-- for details about the UPnP message format, see http://upnp.org/resources/documents.asp
payload = payload .. "M-SEARCH * HTTP/1.1\r\n"
payload = payload .. "Host:239.255.255.250:1900\r\n"
payload = payload .. "ST:upnp:rootdevice\r\n"
payload = payload .. "Man:\"ssdp:discover\"\r\n"
payload = payload .. "MX:3\r\n\r\n"
local status, err = socket:sendto("239.255.255.250", 1900, strbuf.dump(payload))
if (not(status)) then return err end
local response, output
local result = {}
while(true) do
-- read in any response we might get
status, response = socket:receive()
if (not status) then break end
local status, _, _, peer_ip, _ = socket:get_info()
if target.ALLOW_NEW_TARGETS then
target.add(peer_ip)
end
output = process_response( response )
output = { output }
output.name = peer_ip
table.insert( result, output )
end
socket:close()
table.sort(result, ipCompare)
return stdnse.format_output(true, 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)
local payload = strbuf.new()
-- for details about the UPnP message format, see http://upnp.org/resources/documents.asp
payload = payload .. "M-SEARCH * HTTP/1.1\r\n"
payload = payload .. "Host:239.255.255.250:1900\r\n"
payload = payload .. "ST:upnp:rootdevice\r\n"
payload = payload .. "Man:\"ssdp:discover\"\r\n"
payload = payload .. "MX:3\r\n\r\n"
local status, err = socket:connect(host, port, "udp" )
if ( not(status) ) then return err end
status, err = socket:send( strbuf.dump(payload) )
if ( not(status) ) then return err end
local response
status, response = socket:receive()
if (not status) then
socket:close()
return response
end
-- since we got something back, the port is definitely open
nmap.set_port_state(host, port, "open")
return stdnse.format_output(true, process_response( response ))
end
-- Function dispatch table
local actions = {
prerule = preaction,
hostrule = scanaction,
portrule = scanaction,
}
function action (...) return actions[SCRIPT_TYPE](...) end