1
0
mirror of https://github.com/nmap/nmap.git synced 2026-02-11 07:56:35 +00:00

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

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

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

View File

@@ -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

View File

@@ -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},

View File

@@ -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

View File

@@ -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
-- * <code>retAll</code>: Return all answers, not just the first.
-- * <code>retPkt</code>: Return the packet instead of using the answer-fetching mechanism.
-- * <code>norecurse</code> If true, do not set the recursion (RD) flag.
-- * <code>multiple</code> 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.

View File

@@ -323,10 +323,12 @@ function new_try(handler)
-- NSE sockets are the recommended way to do network I/O. They support
-- <code>connect</code>-style sending and receiving over TCP and UDP (and SSL),
-- as well as raw socket receiving.
-- @param protocol a protocol string (optional, defaults to <code>"tcp"</code>).
-- @param af an address family string (optional, defaults to <code>"inet"</code>).
-- @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 <code>"tcp"</code>, <code>"udp"</code>, or
-- <code>"ssl"</code> (default <code>"tcp"</code>).
-- <code>"ssl"</code> (default <code>"tcp"</code>, or whatever was set in
-- <code>new_socket</code>).
-- @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 <code>send</code> 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
-- <code>new_socket</code>. On
-- success the function returns a true value. If the send operation fails, the
-- function returns a false value (<code>false</code> or <code>nil</code>) along
-- with an error string. The error strings are
-- * <code>"Trying to send through a closed socket"</code>: There was no call to <code>socket:connect</code> before the send operation.
-- * <code>"TIMEOUT"</code>: The operation took longer than the specified timeout for the socket.
-- * <code>"ERROR"</code>: An error occurred inside the underlying Nsock library.
-- * <code>"CANCELLED"</code>: The operation was cancelled.
-- * <code>"KILL"</code>: For example the script scan is aborted due to a faulty script.
-- * <code>"EOF"</code>: 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.

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

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

View File

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

View File

@@ -3,6 +3,8 @@ Attempts to extract information from Microsoft SQL Server instances.
]]
-- rev 1.0 (2007-06-09)
-- rev 1.1 (2009-12-06 - Added SQL 2008 identification T Sellers)
-- rev 1.2 (2010-10-03 - Added Broadcast support <patrik@cqure.net>)
-- rev 1.3 (2010-10-10 - Added prerule and newtargets support <patrik@cqure.net>)
author = "Thomas Buchanan"
@@ -20,147 +22,36 @@ categories = {"default", "discovery", "intrusive"}
-- | Instance name: SQLEXPRESS
-- | TCP Port: 1433
-- |_ Could not retrieve actual version information
--
-- PORT STATE SERVICE
-- 1434/udp open|filtered ms-sql-m
-- | ms-sql-info:
-- | 10.0.200.133
-- | Discovered Microsoft SQL Server 2008 Express Edition
-- | Server name: WIN2K3-EPI-1
-- | Server version: 10.0.1600.22 (RTM)
-- | Instance name: SQLEXPRESS
-- | TCP Port: 1052
-- | Could not retrieve actual version information
-- | 10.0.200.119
-- | Discovered Microsoft SQL Server 2000
-- | Server name: EDUSRV011
-- | Instance name: MSSQLSERVER
-- | TCP Port: 1433
-- | Could not retrieve actual version information
-- | Instance name: SQLEXPRESS
-- | TCP Port: 1433
-- |_ Could not retrieve actual version information
require('stdnse')
require "stdnse"
require "shortport"
require("strbuf")
require "strbuf"
require "target"
prerule = function() return true end
portrule = shortport.portnumber({1433, 1434}, "udp", {"open", "open|filtered"})
action = function(host, port)
-- create the socket used for our connection
local socket = nmap.new_socket()
-- set a reasonable timeout value
socket:set_timeout(5000)
-- do some exception handling / cleanup
local catch = function()
socket:close()
end
local try = nmap.new_try(catch)
-- try to login to MS SQL network service, and obtain the real version information
-- MS SQL 2000 does not report the correct version in the data sent in response to UDP probe (see below)
local get_real_version = function(dst, dstPort)
local outcome
local payload = strbuf.new()
local stat, resp
-- build a TDS packet - type 0x12
-- copied from packet capture of osql connection
payload = payload .. "\018\001\000\047\000\000\001\000\000\000"
payload = payload .. "\026\000\006\001\000\032\000\001\002\000"
payload = payload .. "\033\000\001\003\000\034\000\004\004\000"
payload = payload .. "\038\000\001\255\009\000\011\226\000\000"
payload = payload .. "\000\000\120\023\000\000\000"
socket = nmap.new_socket()
-- connect to the server using the tcpPort captured from the UDP probe
try(socket:connect(dst, dstPort, "tcp"))
try(socket:send(strbuf.dump(payload)))
-- read in any response we might get
stat, resp = socket:receive_bytes(1)
if string.match(resp, "^\004") then
-- build a login packet to send to SQL server
-- username = sa, blank password
-- for information about packet structure, see http://www.freetds.org/tds.html
local query = strbuf.new()
query = query .. "\016\001\000\128\000\000\001\000" -- TDS packet header
query = query .. "\120\000\000\000\002\000\009\114" -- Login packet header = length, version
query = query .. "\000\000\000\000\000\000\000\007" -- Login packet header continued = size, client version
query = query .. "\140\018\000\000\000\000\000\000" -- Login packet header continued = Client PID, Connection ID
query = query .. "\224\003\000\000\104\001\000\000" -- Login packet header continued = Option Flags 1 & 2, status flag, reserved flag, timezone
query = query .. "\009\004\000\000\094\000\004\000" -- Login packet (Collation), then start offsets & lengths (client name, client length)
query = query .. "\102\000\002\000\000\000\000\000" -- Login packet, offsets & lengths = username offset, username length, password offset, password length
query = query .. "\106\000\004\000\114\000\000\000" -- Login packet, offsets & lengths = app name offset, app name length, server name offset, server name length
query = query .. "\000\000\000\000\114\000\003\000" -- Login packet, offsets & lengths = unknown offset, unknown length, library name offset, library name length
query = query .. "\120\000\000\000\120\000\000\000" -- Login packet, offsets & lengths = locale offset, locale length, database name offset, database name length
query = query .. "\000\000\000\000\000\000\000\000" -- Login packet, MAC address + padding
query = query .. "\000\000\000\000\000\000\000\000" -- Login packet, padding
query = query .. "\000\000\000\000\000\000\078\000" -- Login packet, padding + start of client name (N)
query = query .. "\077\000\065\000\080\000\115\000" -- Login packet = rest of client name (MAP) + username (s)
query = query .. "\097\000\078\000\077\000\065\000" -- Login packet = username (a), app name (NMA)
query = query .. "\080\000\078\000\083\000\069\000" -- Login packet = app name (P), library name (NSE)
-- send the packet down the wire
try(socket:send(strbuf.dump(query)))
-- read in any response we might get
stat, resp = socket:receive_bytes(1)
-- successful response to login packet should contain the string "SQL Server"
-- however, the string is UCS2 encoded, so we have to add the \000 characters
if string.match(resp, "S\000Q\000L\000") then
outcome = "\n sa user appears to have blank password"
strbuf.clear(query)
-- since we have a successful login, send a query that will tell us what version the server is really running
query = query .. "\001\001\000\044\000\000\001\000" -- TDS Query packet
query = query .. "\083\000\069\000\076\000\069\000" -- SELE
query = query .. "\067\000\084\000\032\000\064\000" -- CT @
query = query .. "\064\000\086\000\069\000\082\000" -- @VER
query = query .. "\083\000\073\000\079\000\078\000" -- SION
query = query .. "\013\000\010\000"
-- send the packet down the wire
try(socket:send(strbuf.dump(query)))
-- read in any response we might get
stat, resp = socket:receive_bytes(1)
-- strip out the embedded \000 characters
local banner = string.gsub(resp, "%z", "")
outcome = outcome .. "\n " .. string.match(banner, "(Microsoft.-)\n")
outcome = outcome .. "\n" .. string.match(banner, "\n.-\n.-\n(.-Build.-)\n")
end
try(socket:close())
end -- if string.match(response, "^\004")
if outcome == nil then
outcome = "\n Could not retrieve actual version information"
end
return outcome
end -- get_real_version(dst, dstPort)
-- connect to the potential SQL server
try(socket:connect(host, port))
-- send a magic packet
-- details here: http://www.codeproject.com/cs/database/locate_sql_servers.asp
try(socket:send("\002"))
local status
local response
-- read in any response we might get
status, response = socket:receive_bytes(1)
try(socket:close())
if (not status) then
return
end
if (response == "TIMEOUT") then
return
end
-- since we got something back, the port is definitely open
nmap.set_port_state(host, port, "open")
local function process_response( response )
local result
@@ -185,74 +76,266 @@ action = function(host, port)
count = count + 1
end
result = {}
-- do some heuristics on the version to see if we can match the major releases
if string.match(serverInfo[1].version, "^6%.0") then
result = "Discovered Microsoft SQL Server 6.0"
table.insert(result, "Discovered Microsoft SQL Server 6.0")
elseif string.match(serverInfo[1].version, "^6%.5") then
result = "Discovered Microsoft SQL Server 6.5"
table.insert(result, "Discovered Microsoft SQL Server 6.5")
elseif string.match(serverInfo[1].version, "^7%.0") then
result = "Discovered Microsoft SQL Server 7.0"
table.insert(result, "Discovered Microsoft SQL Server 7.0")
elseif string.match(serverInfo[1].version, "^8%.0") then
result = "Discovered Microsoft SQL Server 2000"
table.insert(result, "Discovered Microsoft SQL Server 2000")
elseif string.match(serverInfo[1].version, "^9%.0") then
-- The Express Edition of MS SQL Server 2005 has a default instance name of SQLEXPRESS
for _,instance in ipairs(serverInfo) do
if string.match(instance.instanceName, "SQLEXPRESS") then
result = "Discovered Microsoft SQL Server 2005 Express Edition"
table.insert(result, "Discovered Microsoft SQL Server 2005 Express Edition")
end
end
if result == nil then
result = "Discovered Microsoft SQL Server 2005"
table.insert(result, "Discovered Microsoft SQL Server 2005")
end
elseif string.match(serverInfo[1].version, "^10%.0") then
-- The Express Edition of MS SQL Server 2008 has a default instance name of SQLEXPRESS
for _,instance in ipairs(serverInfo) do
if string.match(instance.instanceName, "SQLEXPRESS") then
result = "Discovered Microsoft SQL Server 2008 Express Edition"
table.insert(result, "Discovered Microsoft SQL Server 2008 Express Edition")
end
end
if result == nil then
result = "Discovered Microsoft SQL Server 2008"
table.insert(result, "Discovered Microsoft SQL Server 2008")
end
else
result = "Discovered Microsoft SQL Server"
table.insert(result, "Discovered Microsoft SQL Server")
end
if serverInfo[1].name ~= nil then
result = result .. "\n Server name: " .. serverInfo[1].name
table.insert(result, "Server name: " .. serverInfo[1].name)
end
if serverInfo[1].version ~= nil then
result = result .. "\n Server version: " .. serverInfo[1].version
--result = result .. "\n Server version: " .. serverInfo[1].version
-- Check for some well known release versions of SQL Server 2005
-- for more info, see http://support.microsoft.com/kb/321185
if string.match(serverInfo[1].version, "9.00.3042") then
result = result .. " (SP2)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP2)" )
elseif string.match(serverInfo[1].version, "9.00.3043") then
result = result .. " (SP2)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP2)" )
elseif string.match(serverInfo[1].version, "9.00.2047") then
result = result .. " (SP1)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP1)" )
elseif string.match(serverInfo[1].version, "9.00.1399") then
result = result .. " (RTM)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (RTM)" )
-- Check for versions of SQL Server 2008
elseif string.match(serverInfo[1].version, "10.0.1075") then
result = result .. " (CTP)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (CTP)" )
elseif string.match(serverInfo[1].version, "10.0.1600") then
result = result .. " (RTM)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (RTM)" )
elseif string.match(serverInfo[1].version, "10.0.2531") then
result = result .. " (SP1)"
table.insert(result, "Server version: " .. serverInfo[1].version .. " (SP1)" )
end
end
for _,instance in ipairs(serverInfo) do
if instance.instanceName ~= nil then
result = result .. "\n Instance name: " .. instance.instanceName
end
if instance.tcpPort ~= nil then
result = result .. "\n TCP Port: " .. instance.tcpPort
result = result .. get_real_version(host.ip, instance.tcpPort)
end
end
end
return result
return result, serverInfo
end
-- try to login to MS SQL network service, and obtain the real version information
-- MS SQL 2000 does not report the correct version in the data sent in response to UDP probe (see below)
local get_real_version = function(dst, dstPort)
local outcome = {}
local payload = strbuf.new()
local stat, resp
-- do some exception handling / cleanup
local catch = function() socket:close() end
local try = nmap.new_try(catch)
-- build a TDS packet - type 0x12
-- copied from packet capture of osql connection
payload = payload .. "\018\001\000\047\000\000\001\000\000\000"
payload = payload .. "\026\000\006\001\000\032\000\001\002\000"
payload = payload .. "\033\000\001\003\000\034\000\004\004\000"
payload = payload .. "\038\000\001\255\009\000\011\226\000\000"
payload = payload .. "\000\000\120\023\000\000\000"
socket = nmap.new_socket()
-- connect to the server using the tcpPort captured from the UDP probe
try(socket:connect(dst, dstPort, "tcp"))
try(socket:send(strbuf.dump(payload)))
-- read in any response we might get
stat, resp = socket:receive_bytes(1)
if string.match(resp, "^\004") then
-- build a login packet to send to SQL server
-- username = sa, blank password
-- for information about packet structure, see http://www.freetds.org/tds.html
local query = strbuf.new()
query = query .. "\016\001\000\128\000\000\001\000" -- TDS packet header
query = query .. "\120\000\000\000\002\000\009\114" -- Login packet header = length, version
query = query .. "\000\000\000\000\000\000\000\007" -- Login packet header continued = size, client version
query = query .. "\140\018\000\000\000\000\000\000" -- Login packet header continued = Client PID, Connection ID
query = query .. "\224\003\000\000\104\001\000\000" -- Login packet header continued = Option Flags 1 & 2, status flag, reserved flag, timezone
query = query .. "\009\004\000\000\094\000\004\000" -- Login packet (Collation), then start offsets & lengths (client name, client length)
query = query .. "\102\000\002\000\000\000\000\000" -- Login packet, offsets & lengths = username offset, username length, password offset, password length
query = query .. "\106\000\004\000\114\000\000\000" -- Login packet, offsets & lengths = app name offset, app name length, server name offset, server name length
query = query .. "\000\000\000\000\114\000\003\000" -- Login packet, offsets & lengths = unknown offset, unknown length, library name offset, library name length
query = query .. "\120\000\000\000\120\000\000\000" -- Login packet, offsets & lengths = locale offset, locale length, database name offset, database name length
query = query .. "\000\000\000\000\000\000\000\000" -- Login packet, MAC address + padding
query = query .. "\000\000\000\000\000\000\000\000" -- Login packet, padding
query = query .. "\000\000\000\000\000\000\078\000" -- Login packet, padding + start of client name (N)
query = query .. "\077\000\065\000\080\000\115\000" -- Login packet = rest of client name (MAP) + username (s)
query = query .. "\097\000\078\000\077\000\065\000" -- Login packet = username (a), app name (NMA)
query = query .. "\080\000\078\000\083\000\069\000" -- Login packet = app name (P), library name (NSE)
-- send the packet down the wire
try(socket:send(strbuf.dump(query)))
-- read in any response we might get
stat, resp = socket:receive_bytes(1)
-- successful response to login packet should contain the string "SQL Server"
-- however, the string is UCS2 encoded, so we have to add the \000 characters
if string.match(resp, "S\000Q\000L\000") then
table.insert( outcome, "sa user appears to have blank password" )
strbuf.clear(query)
-- since we have a successful login, send a query that will tell us what version the server is really running
query = query .. "\001\001\000\044\000\000\001\000" -- TDS Query packet
query = query .. "\083\000\069\000\076\000\069\000" -- SELE
query = query .. "\067\000\084\000\032\000\064\000" -- CT @
query = query .. "\064\000\086\000\069\000\082\000" -- @VER
query = query .. "\083\000\073\000\079\000\078\000" -- SION
query = query .. "\013\000\010\000"
-- send the packet down the wire
try(socket:send(strbuf.dump(query)))
-- read in any response we might get
stat, resp = socket:receive_bytes(1)
-- strip out the embedded \000 characters
local banner = string.gsub(resp, "%z", "")
table.insert( outcome, string.match(banner, "(Microsoft.-)\n") )
table.insert( outcome, string.match(banner, "\n.-\n.-\n(.-Build.-)\n") )
end
try(socket:close())
end -- if string.match(response, "^\004")
if #outcome == 0 then
table.insert( outcome, "Could not retrieve actual version information" )
end
return outcome
end -- get_real_version(dst, dstPort)
preaction = function()
local host, port = "255.255.255.255", 1434
-- create the socket used for our connection
local socket = nmap.new_socket("udp")
-- set a reasonable timeout value
socket:set_timeout(5000)
-- do some exception handling / cleanup
local catch = function() socket:close() end
local try = nmap.new_try(catch)
-- send a magic packet
-- details here: http://www.codeproject.com/cs/database/locate_sql_servers.asp
try(socket:sendto(host, port, "\002"))
local output = {}
while(true) do
-- read in any response we might get
local status, response = socket:receive()
if ( not(status) ) then break end
local status, _, _, ip, _ = socket:get_info()
if ( not(status) ) then return end
local result, serverInfo = process_response( response )
for _,instance in ipairs(serverInfo) do
if instance.instanceName ~= nil then
table.insert(result, "Instance name: " .. instance.instanceName)
end
if instance.tcpPort ~= nil then
table.insert(result, "TCP Port: " .. instance.tcpPort)
table.insert(result, get_real_version(ip, instance.tcpPort) )
end
end
if target.ALLOW_NEW_TARGETS then
target.add(ip)
end
result.name = ip
table.insert( output, result )
end
socket:close()
return stdnse.format_output( true, output )
end
scanaction = function( host, port )
-- create the socket used for our connection
local socket = nmap.new_socket()
-- set a reasonable timeout value
socket:set_timeout(5000)
-- do some exception handling / cleanup
local catch = function() socket:close() end
local try = nmap.new_try(catch)
try(socket:connect(host, port, "udp"))
-- send a magic packet
-- details here: http://www.codeproject.com/cs/database/locate_sql_servers.asp
try(socket:send("\002"))
-- read in any response we might get
local status, response = socket:receive()
if (not(status)) then
socket:close()
return
end
local _, _, ip, _ = try(socket:get_info())
local result, serverInfo = process_response( response )
for _,instance in ipairs(serverInfo) do
if instance.instanceName ~= nil then
table.insert(result, "Instance name: " .. instance.instanceName)
end
if instance.tcpPort ~= nil then
table.insert(result, "TCP Port: " .. instance.tcpPort)
table.insert(result, get_real_version(ip, instance.tcpPort) )
end
end
socket:close()
nmap.set_port_state( host, port, "open")
return stdnse.format_output( true, result )
end
-- Function dispatch table
local actions = {
prerule = preaction,
hostrule = scanaction,
portrule = scanaction,
}
function action (...) return actions[SCRIPT_TYPE](...) end

View File

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