From 18157ed0814df28db1b5e2cc8c0e217cff419016 Mon Sep 17 00:00:00 2001 From: david Date: Sat, 16 Oct 2010 00:48:44 +0000 Subject: [PATCH] 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] --- CHANGELOG | 13 + nse_nsock.cc | 107 +++++++- nselib/dhcp.lua | 25 +- nselib/dns.lua | 138 ++++++---- nselib/nmap.luadoc | 31 ++- scripts/db2-discover.nse | 133 ++++++++++ scripts/dns-service-discovery.nse | 295 +++++++++++++++------ scripts/ms-sql-info.nse | 413 ++++++++++++++++++------------ scripts/upnp-info.nse | 207 ++++++++++----- 9 files changed, 992 insertions(+), 370 deletions(-) create mode 100644 scripts/db2-discover.nse 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 +