mirror of
https://github.com/nmap/nmap.git
synced 2025-12-09 06:01:28 +00:00
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]
This commit is contained in:
@@ -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]
|
||||
|
||||
|
||||
368
nselib/stun.lua
Normal file
368
nselib/stun.lua
Normal file
@@ -0,0 +1,368 @@
|
||||
---
|
||||
-- A library that implements the basics of the STUN protocol per RFC3489
|
||||
-- and RFC5389.
|
||||
--
|
||||
-- @author "Patrik Karlsson <patrik@cqure.net>"
|
||||
--
|
||||
|
||||
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("<CCSI", data, pos)
|
||||
addr.ip = ipOps.fromdword(addr.ip)
|
||||
return addr
|
||||
end
|
||||
|
||||
if ( ( attr.type == Attribute.MAPPED_ADDRESS ) or
|
||||
( attr.type == Attribute.RESPONSE_ADDRESS ) or
|
||||
( attr.type == Attribute.SOURCE_ADDRESS ) or
|
||||
( attr.type == Attribute.CHANGED_ADDRESS ) ) then
|
||||
if ( attr.length ~= 8 ) then
|
||||
stdnse.print_debug(2, "Incorrect attribute length")
|
||||
end
|
||||
attr.addr = parseAddress(data, pos)
|
||||
elseif( attr.type == Attribute.SERVER ) then
|
||||
pos, attr.server = bin.unpack("A" .. attr.length-1, data, pos)
|
||||
end
|
||||
|
||||
return attr
|
||||
end,
|
||||
|
||||
-- converts an attribute to string
|
||||
-- @return string containing the serialized attribute
|
||||
__tostring = function(self)
|
||||
return bin.pack(">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:
|
||||
-- <code>timeout</code> - 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:
|
||||
-- <code>timeout</code> - 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 "<unknown>" )
|
||||
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,
|
||||
|
||||
}
|
||||
|
||||
@@ -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", } }
|
||||
|
||||
47
scripts/stun-info.nse
Normal file
47
scripts/stun-info.nse
Normal file
@@ -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 <ip>
|
||||
--
|
||||
-- @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
|
||||
39
scripts/stun-version.nse
Normal file
39
scripts/stun-version.nse
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user