diff --git a/CHANGELOG b/CHANGELOG index 998c0f1cf..a426f8b7d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a natpmp library and the script nat-pmp-mapport that allows + NAT mapping of external TCP and UDP ports to internal addresses. [Patrik] + o [NSE] Added the script riak-http-info that lists version and statistics information from the Basho Riak distributed database. [Patrik] @@ -10,8 +13,8 @@ o [NSE] Added the script memcached-info that lists version and statistics o [NSE] Added the script redis-info that lists version and statistic information gathered from the Redis network key-value store. [Patrik] -o [NSE] Added the script redis-brute that performs brute force password - guessing against the Redis network key-value store. [Patrik] +o [NSE] Added the redis library and the script redis-brute that performs brute + force password guessing against the Redis network key-value store. [Patrik] o [NSE] Added the script http-proxy-brute that performs brute force password guessing against HTTP proxy servers. [Patrik] diff --git a/nselib/natpmp.lua b/nselib/natpmp.lua new file mode 100644 index 000000000..3ad0213cf --- /dev/null +++ b/nselib/natpmp.lua @@ -0,0 +1,220 @@ +--- +-- This library implements the basics of NAT-PMP as described in the +-- NAT Port Mapping Protocol (NAT-PMP) draft: +-- o http://tools.ietf.org/html/draft-cheshire-nat-pmp-03 +-- +-- +-- @author "Patrik Karlsson " +-- +module(... or "natpmp", package.seeall) + +require 'bin' +require 'ipOps' + +local ResultCode = { + SUCCESS = 0, + UNSUPPORTED_VERSION = 1, + NOT_AUTHORIZED = 2, + NETWORK_FAILURE = 3, + OUT_OF_RESOURCES = 4, + UNSUPPORTED_OPCODE = 5, +} + +local ErrorMessage = { + [ResultCode.UNSUPPORTED_VERSION] = "The device did not support the protocol version", + [ResultCode.NOT_AUTHORIZED] = "The operation was not authorized", + [ResultCode.NETWORK_FAILURE] = "Network failure", + [ResultCode.OUT_OF_RESOURCES] = "The device is out of resources", + [ResultCode.UNSUPPORTED_OPCODE] = "The requested operation was not supported", +} + + +Request = { + + GetWANIP = { + + new = function(self) + local o = { version = 0, op = 0 } + setmetatable(o, self) + self.__index = self + return o + end, + + __tostring = function(self) + return bin.pack(">CC", self.version, self.op) + end, + + }, + + MapPort = { + + new = function(self, pubport, privport, proto, lifetime) + assert(proto == "udp" or proto == "tcp", "Unsupported protocol") + local o = { + version = 0, + pubport = pubport, + privport = privport, + proto = proto, + lifetime = lifetime or 3600 + } + setmetatable(o, self) + self.__index = self + return o + end, + + __tostring = function(self) + return bin.pack(">CCSSSI", + self.version, + (self.proto=="udp" and 1 or 2), + 0, -- reserved + self.privport, self.pubport, + self.lifetime) + end, + + } + +} + +Response = { + + GetWANIP = { + + new = function(self, data) + local o = { data = data } + setmetatable(o, self) + self.__index = self + if ( o:parse() ) then + return o + end + end, + + parse = function(self) + if ( #self.data ~= 12 ) then + return + end + + local pos + pos, self.version, self.op, self.rescode = bin.unpack("ISSI", self.data, pos) + return true + end, + } + + +} + + + + + +Helper = { + + new = function(self, host, port) + local o = { host = host, port = port } + setmetatable(o, self) + self.__index = self + return o + end, + + exchPacket = function(self, data) + local socket = nmap.new_socket("udp") + socket:set_timeout(5000) + + local status = socket:sendto(self.host, self.port, data) + if ( not(status) ) then + socket:close() + return false, "Failed to send request to device" + end + + local response + status, response = socket:receive() + socket:close() + if ( not(status) ) then + return false, "Failed to receive response from router" + end + return true, response + end, + + --- Gets the WAN ip of the router + getWANIP = function(self) + local packet = Request.GetWANIP:new() + local status, response = self:exchPacket(tostring(packet)) + if ( not(status) ) then + return status, response + end + + response = Response.GetWANIP:new(response) + if ( not(response) ) then + return false, "Failed to parse response from router" + end + + return true, response + end, + + --- Maps a public port to a private port + -- @param pubport number containing the public external port to map + -- @param privport number containing the private internal port to map + -- @param protocol string containing the protocol to map (udp|tcp) + -- @param lifetime [optional] number containing the lifetime in seconds + mapPort = function(self, pubport, privport, protocol, lifetime) + local packet = Request.MapPort:new(pubport, privport, protocol, lifetime) + local status, response = self:exchPacket(tostring(packet)) + if ( not(status) ) then + return status, response + end + + response = Response.MapPort:new(response) + if ( not(response) ) then + return false, "Failed to parse response from router" + end + + print(response.privport, response.pubport, response.lifetime) + + return true, response + end, + + unmapPort = function(self, pubport, privport) + return self:mapPort(pubport, privport, 0) + end, + + unmapAllPorts = function(self) + return self.mapPort(0, 0, 0) + end, + +} diff --git a/scripts/nat-pmp-info.nse b/scripts/nat-pmp-info.nse index a243fd76e..00b49adc4 100644 --- a/scripts/nat-pmp-info.nse +++ b/scripts/nat-pmp-info.nse @@ -1,104 +1,35 @@ description = [[ -Queries a NAT-PMP service for its external address. +Get's the routers WAN IP using the NAT Port Mapping Protocol (NAT-PMP). +The NAT-PMP protocol is supported by a broad range of routers including: + - Apple AirPort Express + - Apple AirPort Extreme + - Apple Time Capsule + - DD-WRT + - OpenWrt v8.09 or higher, with MiniUPnP daemon + - pfSense v2.0 + - Tarifa (firmware) (Linksys WRT54G/GL/GS) + - Tomato Firmware v1.24 or higher. (Linksys WRT54G/GL/GS and many more) + - Peplink Balance ]] ---- --- @usage --- nmap -sU --script nat-pmp-info -p 5351 --- --- @output --- PORT STATE SERVICE REASON --- 5351/udp open unknown udp-response --- | nat-pmp-info: --- |_ External ip: 1.2.3.4 --- --- --- The implementation is based on the following documentation: --- http://files.dns-sd.org/draft-cheshire-nat-pmp.txt --- - --- --- Version 0.1 --- Created 09/15/2010 - v0.1 - created by Patrik Karlsson --- - author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" -categories = {"default", "safe", "discovery"} +categories = {"default", "discovery", "safe"} -require "stdnse" -require "shortport" +require 'shortport' +require 'natpmp' -portrule = shortport.port_or_service(5351, "nat-pmp", "udp") +portrule = shortport.port_or_service(5351, "nat-pmp", {"udp"} ) -process_response = function( data ) - - -- - -- Make sure we received exactly 12 bytes: - -- - -- 0 1 2 3 - -- 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - -- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -- | Vers = 0 | OP = 128 + 0 | Result Code | - -- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -- | Seconds Since Start of Epoch | - -- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -- | External IP Address (a.b.c.d) | - -- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -- +action = function(host, port) + local helper = natpmp.Helper:new(host, port) + local status, response = helper:getWANIP() - if ( #data ~= 12 ) then return false, "Invalid length" end - local pos, version, op, result, time = bin.unpack("CCSI", data ) - - -- Make sure the result code is zero (OK) - if ( result ~= 0 ) then - return false, ("Non-zero (%d) result code returned"):format(result) + if ( status ) then + nmap.set_port_state(host, port, "open") + port.version.name = "nat-pmp" + nmap.set_port_version(host, port, "hardmatched") + + return stdnse.format_output(true, ("WAN IP: %s"):format(response.ip)) end - - local _, o1, o2, o3, o4 = bin.unpack("CCCC", data, pos ) - return true, ("%d.%d.%d.%d"):format(o1,o2,o3,o4) - -end - -action = function( host, port ) - - local socket = nmap.new_socket() - local status = socket:connect( host, port, "udp" ) - - socket:set_timeout(5000) - - -- 0 1 - -- 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 - -- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -- | Vers = 0 | OP = 0 | - -- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -- - -- Layout of the query for external IP packet - -- - local packet = string.char( 0, 0 ) - - status = socket:send( packet ) - if( not(status) ) then - stdnse.print_debug(3, "ERROR: Failed to send data") - return - end - - local data - status, data = socket:receive_bytes(12) - if( not(status) ) then - stdnse.print_debug(3, "ERROR: Failed to receive data") - return - end - - local external_ip - status, external_ip = process_response( data ) - if ( not(status) ) then stdnse.print_debug(3, external_ip) end - - -- set port to open - nmap.set_port_state(host, port, "open") - port.version.name = "nat-pmp" - nmap.set_port_version(host, port, "hardmatched") - - return (" \n External ip: %s"):format( external_ip ) - end diff --git a/scripts/nat-pmp-mapport.nse b/scripts/nat-pmp-mapport.nse new file mode 100644 index 000000000..de2dde20b --- /dev/null +++ b/scripts/nat-pmp-mapport.nse @@ -0,0 +1,114 @@ +description = [[ +Maps a WAN port on the router to a local port on the client. +The script uses the NAT Port Mapping Protocol (NAT-PMP) to do so and supports the following operations: +o map - maps a new external port on the router to an internal port of the requesting IP +o unmap - unmaps a previously mapped port for the requesting IP +o unmapall - unmaps all previously mapped ports for the requesting IP +]] + +--- +-- @usage +-- nmap -sU -p 5351 --script nat-pmp-mapport --script-args='op=map,pubport=8080,privport=8080,protocol=tcp' +-- nmap -sU -p 5351 --script nat-pmp-mapport --script-args='op=unmap,pubport=8080,privport=8080,protocol=tcp' +-- nmap -sU -p 5351 --script nat-pmp-mapport --script-args='op=unmapall,protocol=tcp' +-- +-- @output +-- PORT STATE SERVICE +-- 5351/udp open nat-pmp +-- | nat-pmp-mapport: +-- |_ Successfully mapped tcp 1.2.3.4:8080 -> 192.168.0.100:80 +-- +-- @args nat-pmp-mapport.op operation, can be either map, unmap or unmap all +-- o map allows you to map an external port to an internal port of the calling IP +-- o unmap removes the external port mapping for the specified ports and protocol +-- o unmapall removes all mappings for the specified protocol and calling IP +-- +-- @args nat-pmp-mapport.pubport the external port to map on the router. The +-- specified port is treated as the requested port. If the port is available +-- it will be allocated to the caller, otherwise the router will simply +-- choose another port, create the mapping and return the resulting port. +-- +-- @args nat-pmp-mapport.privport the internal port of the calling IP to map requests +-- to. This port will recieve all requests coming in to the external port on the +-- router. +-- +-- @args nat-pmp-mapport.protocol the protocol to map, can be either tcp or udp. +-- +-- @args nat-pmp-mapport.lifetime the lifetime of the mapping in seconds (default: 3600) +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require 'shortport' +require 'natpmp' +require 'tab' + +portrule = shortport.port_or_service(5351, "nat-pmp", {"udp"} ) + +local arg_pubport = stdnse.get_script_args(SCRIPT_NAME .. ".pubport") +local arg_privport= stdnse.get_script_args(SCRIPT_NAME .. ".privport") +local arg_protocol= stdnse.get_script_args(SCRIPT_NAME .. ".protocol") +local arg_lifetime= stdnse.get_script_args(SCRIPT_NAME .. ".lifetime") or 3600 +local arg_op = stdnse.get_script_args(SCRIPT_NAME .. ".op") or "map" + +local function fail(str) return "\n ERROR: " .. str end + +action = function(host, port) + + local op = arg_op:lower() + + if ( "map" ~= op and "unmap" ~= op and "unmapall" ~= op ) then + return fail("Operation must be either \"map\", \"unmap\" or \"unmapall\"") + end + + if ( ("map" == op or "unmap" == op ) and + ( not(arg_pubport) or not(arg_privport) or not(arg_protocol) ) ) then + return fail("The arguments pubport, privport and protocol are required") + elseif ( "unmapall" == op and not(arg_protocol) ) then + return fail("The argument protocol is required") + end + + local helper = natpmp.Helper:new(host, port) + + if ( "unmap" == op or "unmapall" == op ) then + arg_lifetime = 0 + end + if ( "unmapall" == op ) then + arg_pubport, arg_privport = 0, 0 + end + + local status, response = helper:getWANIP() + if ( not(status) ) then + return fail("Failed to retrieve WAN IP") + end + + local wan_ip = response.ip + local lan_ip = (nmap.get_interface_info(host.interface)).address + + local status, response = helper:mapPort(arg_pubport, arg_privport, arg_protocol, arg_lifetime) + + if ( not(status) ) then + return fail(response) + end + + local output + if ( "unmap" == op ) then + output = ("Successfully unmapped %s %s:%d -> %s:%d"):format( + arg_protocol, wan_ip, response.pubport, lan_ip, response.privport ) + elseif ( "unmapall" == op ) then + output = ("Sucessfully unmapped all %s NAT mappings for %s"):format(arg_protocol, lan_ip) + else + output = ("Successfully mapped %s %s:%d -> %s:%d"):format( + arg_protocol, wan_ip, response.pubport, lan_ip, response.privport ) + + if ( tonumber(arg_pubport) ~= tonumber(response.pubport) ) then + output = { output } + table.insert(output, "WARNING: Requested public port could not be allocated") + end + end + + return stdnse.format_output(true, output) + +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index d0d399b8f..590a85625 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -184,6 +184,7 @@ Entry { filename = "mysql-info.nse", categories = { "default", "discovery", "saf Entry { filename = "mysql-users.nse", categories = { "auth", "intrusive", } } Entry { filename = "mysql-variables.nse", categories = { "discovery", "intrusive", } } Entry { filename = "nat-pmp-info.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "nat-pmp-mapport.nse", categories = { "discovery", "safe", } } Entry { filename = "nbstat.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "ncp-enum-users.nse", categories = { "auth", "safe", } } Entry { filename = "ncp-serverinfo.nse", categories = { "default", "discovery", "safe", } }