local os = require "os"
local datetime = require "datetime"
local nmap = require "nmap"
local match = require "match"
local math = require "math"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
description = [[
Retrieves information (including system architecture, process ID, and
server time) from distributed memory object caching system memcached.
]]
---
-- @usage
-- nmap -p 11211 --script memcached-info
--
-- @output
-- 11211/udp open unknown
-- | memcached-info:
-- | Process ID: 18568
-- | Uptime: 6950 seconds
-- | Server time: 2018-03-02T03:35:09
-- | 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"}
portrule = shortport.port_or_service(11211, "memcached", {"tcp", "udp"})
local filter = {
["pid"] = { name = "Process ID" },
["uptime"] = { name = "Uptime", func = function(v) return ("%d seconds"):format(v) end },
["time"] = { name = "Server time", func = datetime.format_timestamp },
["pointer_size"] = { name = "Architecture", func = function(v) return v .. " bit" end },
["rusage_user"] = { name = "Used CPU (user)" },
["rusage_system"] = { name = "Used CPU (system)"},
["curr_connections"] = { name = "Current connections"},
["total_connections"] = { name = "Total connections"},
["maxconns"] = { name = "Maximum connections" },
["tcpport"] = { name = "TCP Port" },
["udpport"] = { name = "UDP Port" },
["auth_enabled_sasl"] = { name = "Authentication" }
}
local order = {
"pid", "uptime", "time", "pointer_size", "rusage_user", "rusage_system",
"curr_connections", "total_connections", "maxconns", "tcpport", "udpport",
"auth_enabled_sasl"
}
local function fail(err) return stdnse.format_output(false, err) end
local function mergetab(tab1, tab2)
for k, v in pairs(tab2) do
tab1[k] = v
end
return tab1
end
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 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
-- 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 client = Comm:new(host, port)
local status = client:connect()
if ( not(status) ) then
return fail("Failed to connect to server")
end
local request_time = os.time()
local status, response = client:exchange("stats\r\n")
if ( not(status) ) then
return fail(("Failed to send request to server: %s"):format(response))
end
local kvs = parseResponse(response, "STAT")
if kvs.time then
datetime.record_skew(host, kvs.time, request_time)
end
local status, response = client:exchange("stats settings\r\n")
if ( not(status) ) then
return fail(("Failed to send request to server: %s"):format(response))
end
local kvs2 = parseResponse(response, "STAT")
kvs = mergetab(kvs, kvs2)
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] )
result[name] = val
end
end
return result
end