1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
|
local creds = require "creds"
local redis = require "redis"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local stringaux = require "stringaux"
local table = require "table"
local tableaux = require "tableaux"
local ipOps = require "ipOps"
description = [[
Retrieves information (such as version number and architecture) from a Redis key-value store.
]]
---
-- @usage
-- nmap -p 6379 <ip> --script redis-info
--
-- @output
-- PORT STATE SERVICE
-- 6379/tcp open unknown
-- | redis-info:
-- | Version 2.2.11
-- | Architecture 64 bits
-- | Process ID 17821
-- | Used CPU (sys) 2.37
-- | Used CPU (user) 1.02
-- | Connected clients 1
-- | Connected slaves 0
-- | Used memory 780.16K
-- | Role master
-- | Bind addresses:
-- | 192.168.121.101
-- | Active channels:
-- | testChannel
-- | bidChannel
-- | Client connections:
-- | 192.168.171.101
-- |_ 72.14.177.105
--
--
author = {"Patrik Karlsson", "Vasiliy Kulikov"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
dependencies = {"redis-brute"}
portrule = shortport.port_or_service(6379, "redis")
local function fail(err) return stdnse.format_output(false, err) end
local function cb_parse_version(host, port, val)
port.version.version = val
port.version.cpe = port.version.cpe or {}
table.insert(port.version.cpe, 'cpe:/a:redis:redis:' .. val)
nmap.set_port_version(host, port)
return val
end
local function cb_parse_architecture(host, port, val)
val = ("%s bits"):format(val)
port.version.extrainfo = val
nmap.set_port_version(host, port)
return val
end
local filter = {
["redis_version"] = { name = "Version", func = cb_parse_version },
["os"] = { name = "Operating System" },
["arch_bits"] = { name = "Architecture", func = cb_parse_architecture },
["process_id"] = { name = "Process ID"},
["uptime"] = { name = "Uptime", func = function(h, p, v) return ("%s seconds"):format(v) end },
["used_cpu_sys"]= { name = "Used CPU (sys)"},
["used_cpu_user"] = { name = "Used CPU (user)"},
["connected_clients"] = { name = "Connected clients"},
["connected_slaves"] = { name = "Connected slaves"},
["used_memory_human"] = { name = "Used memory"},
["role"] = { name = "Role"}
}
local order = {
"redis_version", "os", "arch_bits", "process_id", "used_cpu_sys",
"used_cpu_user", "connected_clients", "connected_slaves",
"used_memory_human", "role"
}
local extras = {
{
-- https://redis.io/commands/config-get/
"Bind addresses", {"CONFIG", "GET", "bind"}, function (data)
if data[1] ~= "bind" or not data[2] then
return nil
end
local restab = stringaux.strsplit(" ", data[2])
for i, ip in ipairs(restab) do
if ip == '' then restab[i] = '0.0.0.0' end
end
return restab
end
},
{
-- https://redis.io/commands/pubsub-channels/
"Active channels", {"PUBSUB", "CHANNELS"}, function (data)
local channels = {}
local omitted = 0
local limit = nmap.verbosity() <= 1 and 20 or false
for _, channel in ipairs(data) do
if limit and #channels >= limit then
omitted = omitted + 1
else
table.insert(channels, channel)
end
end
if omitted > 0 then
table.insert(channels, ("(omitted %s item(s), use verbose mode -v to show them)"):format(omitted))
end
return #channels > 0 and channels or nil
end
},
{
-- https://redis.io/commands/client-list/
"Client connections", {"CLIENT", "LIST"}, function(data)
if not data then
stdnse.debug1("Failed to parse response from server")
return nil
end
local client_ips = {}
for conn in data:gmatch("[^\n]+") do
local ip = conn:match("%f[^%s\0]addr=%[?([%x:.]+)%]?:%d+%f[%s\0]")
if ip then
local binip = ipOps.ip_to_str(ip)
if binip then
-- prepending length sorts IPv4 and IPv6 separately
client_ips[string.pack("s1", binip)] = binip
end
end
end
local out = {}
local keys = tableaux.keys(client_ips)
table.sort(keys)
for _, packed in ipairs(keys) do
table.insert(out, ipOps.str_to_ip(client_ips[packed]))
end
return #out > 0 and out or nil
end
},
{
-- https://redis.io/commands/cluster-nodes/
"Cluster nodes", {"CLUSTER", "NODES"}, function(data)
if not data then
stdnse.debug1("Failed to parse response from server")
return nil
end
local out = {}
for node in data:gmatch("[^\n]+") do
local ipport, flags = node:match("^%x+%s+([%x.:%[%]]+)@?%d*%s+(%S+)")
if ipport then
table.insert(out, ("%s (%s)"):format(ipport, flags))
else
stdnse.debug1("Unable to parse cluster node info")
end
end
return #out > 0 and out or nil
end
},
}
action = function(host, port)
local helper = redis.Helper:new(host, port)
local status = helper:connect()
if( not(status) ) then
return fail("Failed to connect to server")
end
-- do we have a service password
local c = creds.Credentials:new(creds.ALL_DATA, host, port)
local cred = c:getCredentials(creds.State.VALID + creds.State.PARAM)()
if ( cred and cred.pass ) then
local status, response = helper:reqCmd("AUTH", cred.pass)
if ( not(status) ) then
helper:close()
return fail(response)
end
end
local status, response = helper:reqCmd("INFO")
if ( not(status) ) then
helper:close()
return fail(response)
end
if ( redis.Response.Type.ERROR == response.type ) then
if ( "-ERR operation not permitted" == response.data ) or
( "-NOAUTH Authentication required." == response.data ) then
return fail("Authentication required")
end
return fail(response.data)
end
local restab = stringaux.strsplit("\r\n", response.data)
if ( not(restab) or 0 == #restab ) then
return fail("Failed to parse response from server")
end
local kvs = {}
for _, item in ipairs(restab) do
local k, v = item:match("^([^:]*):(.*)$")
if k ~= nil then
kvs[k] = v
end
end
local result = stdnse.output_table()
for _, item in ipairs(order) do
if kvs[item] then
local name = filter[item].name
local val
if filter[item].func then
val = filter[item].func(host, port, kvs[item])
else
val = kvs[item]
end
result[name] = val
end
end
for i=1, #extras do
local name = extras[i][1]
local cmd = extras[i][2]
local process = extras[i][3]
local status, response = helper:reqCmd(table.unpack(cmd))
if status and redis.Response.Type.ERROR ~= response.type then
result[name] = process(response.data)
end
end
helper:close()
return result
end
|