diff --git a/CHANGELOG b/CHANGELOG index 849b11dca..8215c12a4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ #Nmap Changelog ($Id$); -*-text-*- +o [NSE] memcached-info can now gather information from the UDP memcached + service in addition to the TCP service. The UDP service is frequently used as + a DDoS reflector and amplifier. [Daniel Miller] + o [NSE][GH#1129] Changed url.absolute() behavior with respect to dot and dot-dot path segments to comply with RFC 3986, section 5.2. [nnposter] diff --git a/scripts/memcached-info.nse b/scripts/memcached-info.nse index 11ede3a6f..9bacc11ae 100644 --- a/scripts/memcached-info.nse +++ b/scripts/memcached-info.nse @@ -1,8 +1,10 @@ local nmap = require "nmap" local match = require "match" +local math = require "math" local shortport = require "shortport" local stdnse = require "stdnse" -local tab = require "tab" +local string = require "string" +local table = require "table" description = [[ Retrieves information (including system architecture, process ID, and @@ -14,29 +16,40 @@ server time) from distributed memory object caching system memcached. -- nmap -p 11211 --script memcached-info -- -- @output --- 11211/tcp open unknown +-- 11211/udp open unknown -- | memcached-info: --- | Process ID 18568 --- | Uptime 6950 seconds --- | Server time Sat Dec 31 14:16:10 2011 --- | Architecture 64 bit --- | Used CPU (user) 0.172010 --- | Used CPU (system) 0.200012 --- | Current connections 10 --- | Total connections 78 --- | Maximum connections 1024 --- | TCP Port 11211 --- | UDP Port 11211 --- |_ Authentication no +-- | Process ID: 18568 +-- | Uptime: 6950 seconds +-- | Server time: Sat Dec 31 14:16:10 2011 +-- | Architecture: 64 bit +-- | Used CPU (user): 0.172010 +-- | Used CPU (system): 0.200012 +-- | Current connections: 10 +-- | Total connections: 78 +-- | Maximum connections: 1024 +-- | TCP Port: 11211 +-- | UDP Port: 11211 +-- |_ Authentication: no -- +-- @xmloutput +-- 17307 +-- 10662 seconds +-- 2018-03-01T16:46:59 +-- 64 bit +-- 0.212809 +-- 0.157151 +-- 5 +-- 11 +-- 1024 +-- 11211 +-- 11211 +-- no author = "Patrik Karlsson" license = "Same as Nmap--See https://nmap.org/book/man-legal.html" categories = {"discovery", "safe"} - --- currently, we only support the TCP, text based protocol -portrule = shortport.port_or_service(11211, "memcached", "tcp") +portrule = shortport.port_or_service(11211, "memcached", {"tcp", "udp"}) local filter = { @@ -70,61 +83,96 @@ local function mergetab(tab1, tab2) return tab1 end -local function recvResponse(socket) - local kvs = {} - repeat - local status, response = socket:receive_buf(match.pattern_limit("\r\n", 2048), false) - if ( not(status) ) then - return false, "Failed to receive response from server" +local Comm = { + new = function(self, host, port, options) + local o = { host = host, port = port, options = options or {}} + self.protocol = port.protocol + self.req_id = math.random(0,0xfff) + setmetatable(o, self) + self.__index = self + return o + end, + connect = function(self) + self.socket = nmap.new_socket(self.protocol) + self.socket:set_timeout(self.options.timeout or stdnse.get_timeout(self.host)) + return self.socket:connect(self.host, self.port) + end, + exchange = function(self, data) + local req_id = self.req_id + self.req_id = req_id + 1 + if self.protocol == "udp" then + data = string.pack(">I2 I2 I2 I2", + req_id, -- request ID + 0, -- sequence number + 1, -- number of datagrams + 0 -- reserved, must be 0 + ) .. data end - local k,v = response:match("^STAT ([^%s]*) (.*)$") - if ( k and v ) then - kvs[k] = v + local status = self.socket:send(data) + if not status then + return false, "Failed to send request to server" + end + if self.protocol == "udp" then + local msgs = {} + local dgrams = 0 + repeat + local status, response = self.socket:receive_bytes(8) + if not status then return false, "Failed to receive entire response" end + local resp_id, seq, ndgrams, pos = string.unpack(">I2 I2 I2 xx", response) + if resp_id == req_id then + dgrams = ndgrams + msgs[seq+1] = string.sub(response, pos) + end + until #msgs >= dgrams + return true, table.concat(msgs) end - until ( "END" == response or "ERROR" == response ) - return true, kvs + -- pattern matches ERR or ERROR at the beginning of a string or after a newline + return self.socket:receive_buf(match.pattern_limit("%f[^\n\0]E[NR][DR]O?R?\r\n", 2048), true) + end, +} + +local function parseResponse(response, expected) + local kvs = {} + for k, v in response:gmatch(("%%f[^\n\0]%s ([^%%s]*) (.-)\r\n"):format(expected)) do + stdnse.debug1("k=%s, v=%s", k, v) + kvs[k] = v + end + return kvs end action = function(host, port) - local socket = nmap.new_socket() - socket:set_timeout(10000) - local status = socket:connect(host, port) + local client = Comm:new(host, port) + local status = client:connect() if ( not(status) ) then return fail("Failed to connect to server") end - status = socket:send("stats\r\n") + local status, response = client:exchange("stats\r\n") if ( not(status) ) then - return fail("Failed to send request to server") + return fail(("Failed to send request to server: %s"):format(response)) end - local status, kvs = recvResponse(socket) - if( not(status) ) then - return fail(kvs) - end + local kvs = parseResponse(response, "STAT") - status = socket:send("stats settings\r\n") + local status, response = client:exchange("stats settings\r\n") if ( not(status) ) then - return fail("Failed to send request to server") + return fail(("Failed to send request to server: %s"):format(response)) end - local status, kvs2 = recvResponse(socket) - if( not(status) ) then - return fail(kvs2) - end + local kvs2 = parseResponse(response, "STAT") kvs = mergetab(kvs, kvs2) - local result = tab.new(2) + local result = stdnse.output_table() for _, item in ipairs(order) do if ( kvs[item] ) then local name = filter[item].name local val = ( filter[item].func and filter[item].func(kvs[item]) or kvs[item] ) - tab.addrow(result, name, val) + result[name] = val end end - return stdnse.format_output(true, tab.dump(result)) + return result end