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
|