summaryrefslogtreecommitdiffstats
path: root/scripts/redis-info.nse
blob: fcfd4b7af32b54b46b408f2bc78e877b45dbbd7e (plain)
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