diff --git a/CHANGELOG b/CHANGELOG index 61fb69aa9..6f282caee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a stun library and the scripts stun-version and stun-info, which + extract version information and the external NAT:ed address. + [Patrik Karlsson] + o [NSE] Added the script duplicates which attempts to determine duplicate hosts by analyzing information collected by other scripts. [Patrik Karlsson] diff --git a/nselib/stun.lua b/nselib/stun.lua new file mode 100644 index 000000000..4bdc77f61 --- /dev/null +++ b/nselib/stun.lua @@ -0,0 +1,368 @@ +--- +-- A library that implements the basics of the STUN protocol per RFC3489 +-- and RFC5389. +-- +-- @author "Patrik Karlsson " +-- + +module(... or "stun", package.seeall) + +require 'ipOps' +require 'match' + +-- The supported request types +MessageType = { + BINDING_REQUEST = 0x0001, + BINDING_RESPONSE = 0x0101, +} + +-- The header used in both request and responses +Header = { + + -- the header size in bytes + size = 20, + + -- creates a new instance of Header + -- @param type number the request/response type + -- @param trans_id string the 128-bit transaction id + -- @param length number the packet length + new = function(self, type, trans_id, length) + local o = { type = type, trans_id = trans_id, length = length or 0 } + setmetatable(o, self) + self.__index = self + return o + end, + + -- parses an opaque string and creates a new Header instance + -- @param data opaque string + -- @return header new instance of Header + parse = function(data) + local header = Header:new() + local pos + pos, header.type, header.length, header.trans_id = bin.unpack(">SSA16", data) + return header + end, + + -- converts the header to an opaque string + -- @return string containing the header instance + __tostring = function(self) + return bin.pack(">SSA", self.type, self.length, self.trans_id) + end, +} + +Request = { + + -- The binding request + Bind = { + + -- Creates a new Bind request + -- @param trans_id string containing the 128 bit transaction ID + -- @return o new instance of the Bind request + new = function(self, trans_id) + local o = { + header = Header:new(MessageType.BINDING_REQUEST, trans_id), + attributes = {} + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- converts the instance to an opaque string + -- @return string containing the Bind request as string + __tostring = function(self) + local data = "" + for _, attrib in ipairs(self.attributes) do + data = data .. tostring(attrib) + end + self.header.length = #data + return tostring(self.header) .. data + end, + } + +} + +-- The attribute class +Attribute = { + + MAPPED_ADDRESS = 0x0001, + RESPONSE_ADDRESS = 0x0002, + CHANGE_REQUEST = 0x0003, + SOURCE_ADDRESS = 0x0004, + CHANGED_ADDRESS = 0x0005, + USERNAME = 0x0006, + PASSWORD = 0x0007, + MESSAGE_INTEGRITY = 0x0008, + ERROR_CODE = 0x0009, + UNKNOWN_ATTRIBUTES = 0x000a, + REFLECTED_FROM = 0x000b, + SERVER = 0x8022, + + -- creates a new attribute instance + -- @param type number containing the attribute type + -- @param data string containing the attribute value + -- @return o instance of attribute + new = function(self, type, data) + local o = { + type = type, + length = (data and #data or 0), + data = data, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- parses a string and creates an Attribute instance + -- @param data string containing the raw attribute + -- @return o new attribute instance + parse = function(data) + local attr = Attribute:new() + local pos = 1 + + pos, attr.type, attr.length = bin.unpack(">SS", data, pos) + + local function parseAddress(data, pos) + local _, addr = nil, {} + pos, _, addr.family, addr.port, addr.ip = bin.unpack("SSA", self.type, self.length, self.data or "") + end, + +} + +-- Response class container +Response = { + + -- Bind response class + Bind = { + + -- creates a new instance of the Bind response + -- @param trans_id string containing the 128 bit transaction id + -- @return o new Bind instance + new = function(self, trans_id) + local o = { header = Header:new(MessageType.BINDING_RESPONSE, trans_id) } + setmetatable(o, self) + self.__index = self + return o + end, + + -- parses a raw string and creates a new Bind instance + -- @param data string containing the raw data + -- @return resp containing a new Bind instance + parse = function(data) + local resp = Response.Bind:new() + local pos = Header.size + + resp.header = Header.parse(data) + resp.attributes = {} + + while( pos < #data ) do + local attr = Attribute.parse(data:sub(pos)) + table.insert(resp.attributes, attr) + pos = pos + attr.length + 4 + end + return resp + end + } +} + +-- The communication class +Comm = { + + -- creates a new Comm instance + -- @param host table + -- @param port table + -- @param options table, currently supporting: + -- timeout - socket timeout in ms. + -- @param mode containing the mode + -- @return o new instance of Comm + new = function(self, host, port, options, mode) + local o = { + host = host, + port = port, + options = options or { timeout = 10000 }, + socket = nmap.new_socket(), + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- connects the socket to the server + -- @return status true on success, false on failure + -- @return err string containing an error message, if status is false + connect = function(self) + self.socket:set_timeout(self.options.timeout) + return self.socket:connect(self.host, self.port) + end, + + -- sends a request to the server + -- @return status true on success, false on failure + -- @return err string containing an error message, if status is false + send = function(self, data) + return self.socket:send(data) + end, + + -- receives a response from the server + -- @return status true on success, false on failure + -- @return response containing a response instance + -- err string containing an error message, if status is false + recv = function(self) + local status, hdr_data = self.socket:receive_buf(match.numbytes(Header.size)) + if ( not(status) ) then + return false, "Failed to receive response from server" + end + + local header = Header.parse(hdr_data) + if ( not(header) ) then + return false, "Failed to parse response header" + end + + local status, data = self.socket:receive_buf(match.numbytes(header.length)) + if ( header.type == MessageType.BINDING_RESPONSE ) then + local resp = Response.Bind.parse(hdr_data .. data) + return true, resp + end + + return false, "Unknown response message received" + end, + + -- sends the request instance to the server and receives the response + -- @param req request class instance + -- @return status true on success, false on failure + -- @return response containing a response instance + -- err string containing an error message, if status is false + exch = function(self, req) + local status, err = self:send(tostring(req)) + if ( not(status) ) then + return false, "Failed to send request to server" + end + return self:recv() + end, + + -- closes the connection to the server + -- @return status true on success, false on failure + -- @return err string containing an error message, if status is false + close = function(self) + self.socket:close() + end, +} + +-- The Util class +Util = { + + -- creates a random string + -- @param len number containg the length of the generated random string + -- @return str containing the random string + randomString = function(len) + local str = "" + for i=1, len do str = str .. string.char(math.random(255)) end + return str + end + +} + +-- The Helper class +Helper = { + + -- creates a new Helper instance + -- @param host table + -- @param port table + -- @param options table, currently supporting: + -- timeout - socket timeout in ms. + -- @param mode containing the mode container, currently Classic is the only + -- supported container + -- @return o new instance of Comm + new = function(self, host, port, options, mode) + local o = { + mode = mode, + comm = Comm:new(host, port, options, mode), + } + o.mode = stdnse.get_script_args("stun.mode") or "modern" + assert(o.mode == "modern" or o.mode == "classic", "Unsupported mode") + setmetatable(o, self) + self.__index = self + return o + end, + + -- connects to the server + -- @return status true on success, false on failure + -- @return err string containing an error message, if status is false + connect = function(self) + return self.comm:connect() + end, + + -- Get's the external public IP + -- @return status true on success, false on failure + -- @return result containing the IP as tring + getExternalAddress = function(self) + local trans_id + + if ( self.mode == "classic" ) then + trans_id = Util.randomString(16) + else + trans_id = bin.pack("HA","2112A442", Util.randomString(12)) + end + local req = Request.Bind:new(trans_id) + + local status, response = self.comm:exch(req) + if ( not(status) ) then + return false, "Failed to send data to server" + end + + local result + for k, attr in pairs(response.attributes) do + if (attr.type == Attribute.MAPPED_ADDRESS ) then + result = ( attr.addr and attr.addr.ip or "" ) + end + if ( attr.type == Attribute.SERVER ) then + self.cache = self.cache or {} + self.cache.server = attr.server + end + end + + return status, result + end, + + -- Gets the server version if it was returned by the server + -- @return status true on success, false on failure + -- @return version string containing the server product and version + getVersion = function(self) + -- check if the server version was cached + if ( not(self.cache) or not(self.cache.version) ) then + self:getExternalAddress() + end + return true, (self.cache and self.cache.server or "") + end, + + -- closes the connection to the server + -- @return status true on success, false on failure + -- @return err string containing an error message, if status is false + close = function(self) + return self.comm:close() + end, + +} + diff --git a/scripts/script.db b/scripts/script.db index 1ac3cf8ca..a6e0dc985 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -313,6 +313,8 @@ Entry { filename = "ssl-enum-ciphers.nse", categories = { "discovery", "intrusiv Entry { filename = "ssl-google-cert-catalog.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "ssl-known-key.nse", categories = { "discovery", "safe", "vuln", } } Entry { filename = "sslv2.nse", categories = { "default", "safe", } } +Entry { filename = "stun-info.nse", categories = { "discovery", "safe", } } +Entry { filename = "stun-version.nse", categories = { "version", } } Entry { filename = "stuxnet-detect.nse", categories = { "discovery", "intrusive", } } Entry { filename = "svn-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "targets-ipv6-multicast-echo.nse", categories = { "broadcast", "discovery", } } diff --git a/scripts/stun-info.nse b/scripts/stun-info.nse new file mode 100644 index 000000000..bd09fb05b --- /dev/null +++ b/scripts/stun-info.nse @@ -0,0 +1,47 @@ +description = [[ +Retrieves the external IP address of a NAT:ed host using the STUN Classic +protocol. +]] + +--- +-- @usage +-- nmap -sV -PN -sU -p 3478 --script stun-info +-- +-- @output +-- PORT STATE SERVICE +-- 3478/udp open|filtered stun +-- | stun-info: +-- |_ External IP: 80.216.42.106 +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require 'shortport' +require 'stun' + +portrule = shortport.port_or_service(3478, "stun", "udp") + +local function fail(err) return ("\n ERROR: %s"):format(err or "") end + +action = function(host, port) + local helper = stun.Helper:new(host, port) + local status = helper:connect() + if ( not(status) ) then + return fail("Failed to connect to server") + end + + local status, result = helper:getExternalAddress() + if ( not(status) ) then + return fail("Failed to retrieve external IP") + end + + port.version.name = "stun" + nmap.set_port_state(host, port, "open") + nmap.set_port_version(host, port, "hardmatched") + + if ( result ) then + return "\n External IP: " .. result + end +end \ No newline at end of file diff --git a/scripts/stun-version.nse b/scripts/stun-version.nse new file mode 100644 index 000000000..89c81f55b --- /dev/null +++ b/scripts/stun-version.nse @@ -0,0 +1,39 @@ +description = [[ +Sends a binding request to the server and attempts to extract version +information from the response, if the server attribute is present. +]] + +--- +-- @output +-- PORT STATE SERVICE VERSION +-- 3478/udp open stun Vovida.org 0.96 +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"version"} + +require 'shortport' +require 'stun' + +portrule = shortport.port_or_service(3478, "stun", "udp") + +local function fail(err) return ("\n ERROR: %s"):format(err or "") end + +action = function(host, port) + local helper = stun.Helper:new(host, port) + local status = helper:connect() + if ( not(status) ) then + return fail("Failed to connect to server") + end + + local status, result = helper:getVersion() + if ( not(status) ) then + return fail("Failed to retrieve external IP") + end + + port.version.name = "stun" + port.version.product = result + nmap.set_port_state(host, port, "open") + nmap.set_port_version(host, port, "hardmatched") +end \ No newline at end of file