diff --git a/CHANGELOG b/CHANGELOG
index 0d16d3548..8f88f5a36 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,18 @@
# Nmap Changelog ($Id$); -*-text-*-
+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]
+
o [NSE] Improved ssh2's kex_init() parameters: all of the algorithm
and language lists can be set using new keys in the "options" table
argument. These all default to the same value used before. Also, the
diff --git a/nse_nsock.cc b/nse_nsock.cc
index 0157cfe11..aac0f8d65 100644
--- a/nse_nsock.cc
+++ b/nse_nsock.cc
@@ -47,6 +47,9 @@ typedef struct nse_nsock_udata
lua_State *thread;
+ int proto;
+ int af;
+
const char *direction;
const char *action;
@@ -77,6 +80,7 @@ static nsock_pool new_pool (lua_State *L)
{
nsock_pool nsp = nsp_new(NULL);
nsock_pool *nspp;
+ nsp_setbroadcast(nsp, true);
lua_pushlightuserdata(L, &NSOCK_POOL);
nspp = (nsock_pool *) lua_newuserdata(L, sizeof(nsock_pool));
*nspp = nsp;
@@ -426,8 +430,35 @@ static nse_nsock_udata *check_nsock_udata (lua_State *L, int idx, int open)
{
nse_nsock_udata *nu =
(nse_nsock_udata *) luaL_checkudata(L, idx, NMAP_NSOCK_SOCKET);
- if (open && nu->nsiod == NULL)
- luaL_error(L, "socket must be connected\n");
+
+ if (open && nu->nsiod == NULL) {
+ /* The socket hasn't been connected or setup yet. Try doing a setup, or
+ throw and error if that's not possible. */
+ if (nu->proto == IPPROTO_UDP) {
+ nsock_pool nsp;
+
+ nsp = get_pool(L);
+ nu->nsiod = nsi_new(nsp, NULL);
+ if (nu->source_addr.ss_family != AF_UNSPEC) {
+ nsi_set_localaddr(nu->nsiod, &nu->source_addr, nu->source_addrlen);
+ } else if (o.spoofsource) {
+ struct sockaddr_storage ss;
+ size_t sslen;
+ o.SourceSockAddr(&ss, &sslen);
+ nsi_set_localaddr(nu->nsiod, &ss, sslen);
+ }
+ if (o.ipoptionslen)
+ nsi_set_ipoptions(nu->nsiod, o.ipoptions, o.ipoptionslen);
+
+ if (nsock_setup_udp(nsp, nu->nsiod, nu->af) == -1) {
+ luaL_error(L, "Error in setup of iod with proto %d and af %d: %s (%d)",
+ nu->proto, nu->af, socket_strerror(socket_errno()), socket_errno());
+ }
+ } else {
+ luaL_error(L, "socket must be connected\n");
+ }
+ }
+
return nu;
}
@@ -470,7 +501,19 @@ static int l_connect (lua_State *L)
const char *addr, *targetname; check_target(L, 2, &addr, &targetname);
const char *default_proto = NULL;
unsigned short port = check_port(L, 3, &default_proto);
- if (default_proto == NULL) default_proto = "tcp";
+ if (default_proto == NULL) {
+ switch (nu->proto) {
+ case IPPROTO_TCP:
+ default_proto = "tcp";
+ break;
+ case IPPROTO_UDP:
+ default_proto = "udp";
+ break;
+ default:
+ default_proto = "tcp";
+ break;
+ }
+ }
int what = luaL_checkoption(L, 4, default_proto, op);
struct addrinfo *dest;
int error_id;
@@ -512,17 +555,22 @@ static int l_connect (lua_State *L)
fatal("nsi_set_hostname(\"%s\" failed in %s()", targetname, __func__);
}
+ nu->af = dest->ai_addr->sa_family;
+
switch (what)
{
case TCP:
+ nu->proto = IPPROTO_TCP;
nsock_connect_tcp(nsp, nu->nsiod, callback, nu->timeout, nu,
dest->ai_addr, dest->ai_addrlen, port);
break;
case UDP:
+ nu->proto = IPPROTO_UDP;
nsock_connect_udp(nsp, nu->nsiod, callback, nu, dest->ai_addr,
dest->ai_addrlen, port);
break;
case SSL:
+ nu->proto = IPPROTO_TCP;
nsock_connect_ssl(nsp, nu->nsiod, callback, nu->timeout, nu,
dest->ai_addr, dest->ai_addrlen, IPPROTO_TCP, port, nu->ssl_session);
break;
@@ -543,6 +591,31 @@ static int l_send (lua_State *L)
return yield(L, nu, "SEND", TO, 0, NULL);
}
+static int l_sendto (lua_State *L)
+{
+ nsock_pool nsp = get_pool(L);
+ nse_nsock_udata *nu = check_nsock_udata(L, 1, 1);
+ size_t size;
+ const char *addr, *targetname; check_target(L, 2, &addr, &targetname);
+ const char *default_proto = NULL;
+ unsigned short port = check_port(L, 3, &default_proto);
+ const char *string = luaL_checklstring(L, 4, &size);
+ int error_id;
+ struct addrinfo *dest;
+
+ error_id = getaddrinfo(addr, NULL, NULL, &dest);
+ if (error_id)
+ return safe_error(L, gai_strerror(error_id));
+
+ if (dest == NULL)
+ return safe_error(L, "getaddrinfo returned success but no addresses");
+
+ nsock_sendto(nsp, nu->nsiod, callback, nu->timeout, nu, dest->ai_addr, dest->ai_addrlen, port, string, size);
+ trace(nu->nsiod, hexify((unsigned char *) string, size).c_str(), TO);
+ return yield(L, nu, "SEND", TO, 0, NULL);
+
+}
+
static void receive_callback (nsock_pool nsp, nsock_event nse, void *udata)
{
nse_nsock_udata *nu = (nse_nsock_udata *) udata;
@@ -771,13 +844,25 @@ static int l_bind (lua_State *L)
return success(L);
}
-static void initialize (lua_State *L, int idx, nse_nsock_udata *nu)
+static const char *default_af_string(int af)
{
+ if (af == AF_INET)
+ return "inet";
+ else
+ return "inet6";
+}
+
+static void initialize (lua_State *L, int idx, nse_nsock_udata *nu,
+ int proto, int af)
+{
+
lua_createtable(L, 2, 0); /* room for thread in array */
lua_pushliteral(L, "");
lua_rawseti(L, -2, BUFFER_I);
lua_setfenv(L, idx);
nu->nsiod = NULL;
+ nu->proto = proto;
+ nu->af = af;
nu->ssl_session = NULL;
nu->source_addr.ss_family = AF_UNSPEC;
nu->source_addrlen = sizeof(nu->source_addr);
@@ -789,13 +874,22 @@ static void initialize (lua_State *L, int idx, nse_nsock_udata *nu)
LUALIB_API int l_nsock_new (lua_State *L)
{
+ static const char *proto_strings[] = { "tcp", "udp", NULL };
+ int proto_map[] = { IPPROTO_TCP, IPPROTO_UDP };
+ static const char *af_strings[] = { "inet", "inet6", NULL };
+ int af_map[] = { AF_INET, AF_INET6 };
+ int proto, af;
nse_nsock_udata *nu;
+
+ proto = proto_map[luaL_checkoption(L, 1, "tcp", proto_strings)];
+ af = af_map[luaL_checkoption(L, 2, default_af_string(o.af()), af_strings)];
+
lua_settop(L, 0);
nu = (nse_nsock_udata *) lua_newuserdata(L, sizeof(nse_nsock_udata));
luaL_getmetatable(L, NMAP_NSOCK_SOCKET);
lua_setmetatable(L, -2);
- initialize(L, 1, nu);
+ initialize(L, 1, nu, proto, af);
return 1;
}
@@ -812,7 +906,7 @@ static int l_close (lua_State *L)
#endif
if (!nu->is_pcap) /* pcap sockets are closed by pcap_gc */
nsi_delete(nu->nsiod, NSOCK_PENDING_NOTIFY);
- initialize(L, 1, nu);
+ initialize(L, 1, nu, nu->proto, nu->af);
return success(L);
}
@@ -989,6 +1083,7 @@ LUALIB_API int luaopen_nsock (lua_State *L)
static const luaL_Reg l_nsock[] = {
{"bind", l_bind},
{"send", l_send},
+ {"sendto", l_sendto},
{"receive", l_receive},
{"receive_lines", l_receive_lines},
{"receive_bytes", l_receive_bytes},
diff --git a/nselib/dhcp.lua b/nselib/dhcp.lua
index 3c9ba2322..034b1df0c 100644
--- a/nselib/dhcp.lua
+++ b/nselib/dhcp.lua
@@ -360,11 +360,9 @@ local function dhcp_send(interface, host, packet, transaction_id)
local results = {}
- -- Create a pcap socket to listen for the response. I used to consider this a hack, but
- -- it really isn't -- it's kinda how this has to be done.
- local pcap = nmap.new_socket()
- pcap:pcap_open(interface, 590, false, "udp port 68")
- pcap:set_timeout(5000)
+ local bind_socket = nmap.new_socket("udp")
+ bind_socket:bind(nil, 68)
+ bind_socket:set_timeout(5000)
stdnse.print_debug(1, "dhcp: Starting listener")
-- Create the UDP socket (TODO: enable SO_BROADCAST if we need to)
@@ -379,15 +377,15 @@ local function dhcp_send(interface, host, packet, transaction_id)
socket:send(packet)
-- Read the response
- local status, length, layer2, layer3 = pcap:pcap_receive();
+ local status, data = bind_socket:receive()
-- This pulls back 4 bytes in the packet that correspond to the transaction id. This should be randomly
-- generated and different for every instance of a script (to prevent collisions)
- while status and layer3:sub(33, 36) ~= transaction_id do
- status, length, layer2, layer3 = pcap:pcap_receive();
- end
+ while status and data:sub(5, 8) ~= transaction_id do
+ local status, data = bind_socket:receive()
+ end
if(status == false) then
- stdnse.print_debug(1, "dhcp: Error calling pcap_receive(): %s", err)
- return false, "Error calling pcap_receive(): " .. err
+ stdnse.print_debug(1, "dhcp: Error calling bind_socket:receive(): %s", err)
+ return false, "Error calling bind_socket:receive(): " .. err
end
-- If no data was captured (ie, a timeout), return an error
@@ -396,12 +394,9 @@ local function dhcp_send(interface, host, packet, transaction_id)
return false, "TIMEOUT"
end
- -- Cut off the address/transport headers
- data = string.sub(data, 29) -- I doubt this is the right way to do this, but since we're only supporting IPv4 + UDP, maybe it'll work
-
-- Close our sockets
socket:close()
- pcap:close()
+ bind_socket:close()
-- Finally, return the data
return true, data
diff --git a/nselib/dns.lua b/nselib/dns.lua
index 831f4f3cf..584596ba8 100644
--- a/nselib/dns.lua
+++ b/nselib/dns.lua
@@ -40,21 +40,51 @@ types = {
-- @param port Port to connect to.
-- @param timeout Number of ms to wait for a response.
-- @param cnt Number of tries.
+-- @param multiple If true, keep reading multiple responses until timeout.
-- @return Status (true or false).
-- @return Response (if status is true).
-local function sendPackets(data, host, port, timeout, cnt)
- local socket = nmap.new_socket()
- socket:set_timeout(timeout)
- socket:connect(host, port, "udp")
+local function sendPackets(data, host, port, timeout, cnt, multiple)
+ local socket = nmap.new_socket("udp")
+ local responses = {}
+
+ socket:set_timeout(timeout)
+
+ if ( not(multiple) ) then
+ socket:connect( host, port, "udp" )
+ end
+
+ for i = 1, cnt do
+ local status, err
+
+ if ( multiple ) then
+ status, err = socket:sendto(host, port, data)
+ else
+ status, err = socket:send(data)
+ end
+
+ if (not(status)) then return false, err end
- for i = 1, cnt do
- socket:send(data)
local response
- local status, response = socket:receive_bytes(1)
-
- if (status) then
+
+ if ( multiple ) then
+ while(true) do
+ status, response = socket:receive()
+ if( not(status) ) then break end
+
+ local status, _, _, ip, _ = socket:get_info()
+ table.insert(responses, { data = response, peer = ip } )
+ end
+ else
+ status, response = socket:receive()
+ if ( status ) then
+ local status, _, _, ip, _ = socket:get_info()
+ table.insert(responses, { data = response, peer = ip } )
+ end
+ end
+
+ if (#responses>0) then
socket:close()
- return true, response
+ return true, responses
end
end
socket:close()
@@ -130,6 +160,42 @@ local function getAuthDns(rPkt)
return false
end
+local function processResponse( response, dname, dtype, options )
+
+ local rPkt = decode(response)
+ -- is it a real answer?
+ if gotAnswer(rPkt) then
+ if (options.retPkt) then
+ return true, rPkt
+ else
+ return findNiceAnswer(dtype, rPkt, options.retAll)
+ end
+ else -- if not, ask the next server in authority
+
+ local next_server = getAuthDns(rPkt)
+
+ -- if we got a CNAME, ask for the CNAME
+ if type(next_server) == 'table' and next_server.cname then
+ options.tries = option.tries - 1
+ return query(next_server.cname, options)
+ end
+
+ -- only ask next server in authority, if
+ -- we got an auth dns and
+ -- it isn't the one we just asked
+ if next_server and next_server ~= host and options.tries > 1 then
+ options.host = next_server
+ options.tries = option.tries - 1
+ return query(dname, options)
+ end
+ end
+
+ -- nothing worked
+ stdnse.print_debug(1, "dns.query() failed to resolve the requested query%s%s", dname and ": " or ".", dname or "")
+ return false, "No Answers"
+
+end
+
---
-- Query DNS servers for a DNS record.
-- @param dname Desired domain name entry.
@@ -141,6 +207,7 @@ end
-- * retAll: Return all answers, not just the first.
-- * retPkt: Return the packet instead of using the answer-fetching mechanism.
-- * norecurse If true, do not set the recursion (RD) flag.
+-- * multiple If true, expects multiple hosts to respond to multicast request
-- @return True if a dns response was received and contained an answer of the requested type,
-- or the decoded dns response was requested (retPkt) and is being returned - or False otherwise.
-- @return String answer of the requested type, Table of answers or a String error message of one of the following:
@@ -148,9 +215,9 @@ end
function query(dname, options)
if not options then options = {} end
- local dtype, host, port, tries = options.dtype, options.host, options.port, options.tries
+ local dtype, host, port = options.dtype, options.host, options.port
- if not tries then tries = 10 end -- don't get into an infinite loop
+ if not options.tries then options.tries = 10 end -- don't get into an infinite loop
if not options.sendCount then options.sendCount = 2 end
@@ -182,7 +249,7 @@ function query(dname, options)
local data = encode(pkt)
- local status, response = sendPackets(data, host, port, options.timeout, options.sendCount)
+ local status, response = sendPackets(data, host, port, options.timeout, options.sendCount, options.multiple)
-- if working with know nameservers, try the others
@@ -194,37 +261,18 @@ function query(dname, options)
-- if we got any response:
if status then
- local rPkt = decode(response)
- -- is it a real answer?
- if gotAnswer(rPkt) then
- if (options.retPkt) then
- return true, rPkt
- else
- return findNiceAnswer(dtype, rPkt, options.retAll)
- end
- else -- if not, ask the next server in authority
-
- local next_server = getAuthDns(rPkt)
-
- -- if we got a CNAME, ask for the CNAME
- if type(next_server) == 'table' and next_server.cname then
- options.tries = tries - 1
- return query(next_server.cname, options)
- end
-
- -- only ask next server in authority, if
- -- we got an auth dns and
- -- it isn't the one we just asked
- if next_server and next_server ~= host and tries > 1 then
- options.host = next_server
- options.tries = tries - 1
- return query(dname, options)
- end
- end
-
- -- nothing worked
- stdnse.print_debug(1, "dns.query() failed to resolve the requested query%s%s", dname and ": " or ".", dname or "")
- return false, "No Answers"
+ if ( options.multiple ) then
+ local multiresponse = {}
+ for _, r in ipairs( response ) do
+ local status, presponse = processResponse( r.data, dname, dtype, options )
+ if( status ) then
+ table.insert( multiresponse, { ['output']=presponse, ['peer']=r.peer } )
+ end
+ end
+ return true, multiresponse
+ else
+ return processResponse( response[1].data, dname, dtype, options)
+ end
else
stdnse.print_debug(1, "dns.query() got zero responses attempting to resolve query%s%s", dname and ": " or ".", dname or "")
return false, "No Answers"
@@ -348,7 +396,7 @@ answerFetcher[types.CNAME] = function(dec, retAll)
stdnse.print_debug(1, "dns.answerFetcher found no records of the required type: NS, PTR or CNAME")
return false, "No Answers"
end
- return true, answers
+ return true, answers
end
-- Answer fetcher for MX records.
diff --git a/nselib/nmap.luadoc b/nselib/nmap.luadoc
index b6dda959c..7655485a8 100644
--- a/nselib/nmap.luadoc
+++ b/nselib/nmap.luadoc
@@ -323,10 +323,12 @@ function new_try(handler)
-- NSE sockets are the recommended way to do network I/O. They support
-- connect-style sending and receiving over TCP and UDP (and SSL),
-- as well as raw socket receiving.
+-- @param protocol a protocol string (optional, defaults to "tcp").
+-- @param af an address family string (optional, defaults to "inet").
-- @return A new NSE socket.
-- @see pcap_open
-- @usage local socket = nmap.new_socket()
-function new_socket()
+function new_socket(protocol, af)
--- Sets the local address of a socket.
--
@@ -385,7 +387,8 @@ function bind(addr, port)
-- @param host Host table, hostname or IP address.
-- @param port Port table or number.
-- @param protocol "tcp", "udp", or
--- "ssl" (default "tcp").
+-- "ssl" (default "tcp", or whatever was set in
+-- new_socket).
-- @return Status (true or false).
-- @return Error code (if status is false).
-- @see new_socket
@@ -428,6 +431,30 @@ function reconnect_ssl()
-- @usage local status, err = socket:send(data)
function send(data)
+--- Sends data on an unconnected socket to a given destination.
+--
+-- Sockets that have not been connected do not have an implicit
+-- destination address, so the send function doesn't work. Instead
+-- the destination must be given with each send using this function. The
+-- protocol and address family of the socket must have been set in
+-- new_socket. On
+-- success the function returns a true value. If the send operation fails, the
+-- function returns a false value (false or nil) along
+-- with an error string. The error strings are
+-- * "Trying to send through a closed socket": There was no call to socket:connect before the send operation.
+-- * "TIMEOUT": The operation took longer than the specified timeout for the socket.
+-- * "ERROR": An error occurred inside the underlying Nsock library.
+-- * "CANCELLED": The operation was cancelled.
+-- * "KILL": For example the script scan is aborted due to a faulty script.
+-- * "EOF": An EOF was read (probably will not occur for a send operation).
+-- @param host The hostname or IP address to send to.
+-- @param port The port number to send to.
+-- @param data The data to send.
+-- @return Status (true or false).
+-- @return Error code (if status is false).
+-- @usage local status, err = socket:send(data)
+function sendto(host, port, data)
+
--- Receives data from an open socket.
--
-- The receive method does a non-blocking receive operation on an open socket.
diff --git a/scripts/db2-discover.nse b/scripts/db2-discover.nse
new file mode 100644
index 000000000..b97ad8b41
--- /dev/null
+++ b/scripts/db2-discover.nse
@@ -0,0 +1,133 @@
+description = [[
+Attempts do discover DB2 servers on the network using UDP
+]]
+
+---
+-- @usage
+-- sudo ./nmap -sU -p 523 --script db2-discover
+--
+-- @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
+-- Revised 10/10/2010 - v0.2 - add prerule, newtargets
+
+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
\ No newline at end of file
diff --git a/scripts/dns-service-discovery.nse b/scripts/dns-service-discovery.nse
index 7ab727a55..fbbc419d3 100644
--- a/scripts/dns-service-discovery.nse
+++ b/scripts/dns-service-discovery.nse
@@ -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
-- 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
+-- 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
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 dns.query
+-- @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
+
diff --git a/scripts/ms-sql-info.nse b/scripts/ms-sql-info.nse
index 21fec2737..3a5c2e149 100644
--- a/scripts/ms-sql-info.nse
+++ b/scripts/ms-sql-info.nse
@@ -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 )
+-- rev 1.3 (2010-10-10 - Added prerule and newtargets support )
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
+
diff --git a/scripts/upnp-info.nse b/scripts/upnp-info.nse
index b674b0ccd..4d00608bb 100644
--- a/scripts/upnp-info.nse
+++ b/scripts/upnp-info.nse
@@ -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
+-- 2010-10-10 - add newtarget support
+
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 entries, which can a number of interesting fields
for device in string.gmatch(response, "(.-)") do
@@ -144,11 +104,11 @@ action = function(host, port)
nm = string.match(device, "(.-)")
ver = string.match(device, "(.-)")
- 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
+