CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/msf/core/auxiliary/redis.rb
Views: 11784
1
# -*- coding: binary -*-
2
module Msf
3
###
4
#
5
# This module provides methods for working with redis
6
#
7
###
8
module Auxiliary::Redis
9
include Msf::Exploit::Remote::Tcp
10
include Auxiliary::Scanner
11
include Auxiliary::Report
12
13
#
14
# Initializes an instance of an auxiliary module that interacts with Redis
15
#
16
def initialize(info = {})
17
super
18
register_options(
19
[
20
Opt::RPORT(6379),
21
OptString.new('PASSWORD', [false, 'Redis password for authentication test', 'foobared'])
22
]
23
)
24
25
register_advanced_options(
26
[
27
OptInt.new('READ_TIMEOUT', [true, 'Seconds to wait while reading redis responses', 2])
28
]
29
)
30
end
31
32
def read_timeout
33
datastore['READ_TIMEOUT']
34
end
35
36
def report_redis(version)
37
report_service(
38
host: rhost,
39
port: rport,
40
proto: 'tcp',
41
name: 'redis',
42
info: "version #{version}"
43
)
44
end
45
46
def redis_command(*commands)
47
command_string = printable_redis_response(commands.join(' '))
48
unless (command_response = send_redis_command(*commands))
49
vprint_error("No response to '#{command_string}'")
50
return
51
end
52
if (match = authentication_required?(command_response))
53
auth_response = match[:auth_response]
54
55
fail_with(::Msf::Module::Failure::BadConfig, "#{peer} requires authentication but Password unset") unless datastore['Password']
56
vprint_status("Requires authentication (#{printable_redis_response(auth_response, false)})")
57
58
if (auth_response = send_redis_command('AUTH', datastore['PASSWORD']))
59
unless auth_response =~ Rex::Proto::Redis::Base::Constants::OKAY
60
vprint_error("Authentication failure: #{printable_redis_response(auth_response)}")
61
return
62
end
63
vprint_status("Authenticated")
64
unless (command_response = send_redis_command(*commands))
65
vprint_error("No response to '#{command_string}'")
66
return
67
end
68
else
69
vprint_status("Authentication failed; no response")
70
return
71
end
72
end
73
74
vprint_status("Redis command '#{command_string}' got '#{printable_redis_response(command_response)}'")
75
command_response
76
end
77
78
def parse_redis_response(response)
79
parser = RESPParser.new(response)
80
parser.parse
81
end
82
83
def printable_redis_response(response_data, convert_whitespace = true)
84
Rex::Text.ascii_safe_hex(response_data, convert_whitespace)
85
end
86
87
private
88
89
# Verifies whether the response indicates if authentication is required
90
# @return [RESPParser] Returns a matched response if a hit is there; otherwise nil.
91
def authentication_required?(response)
92
response.match(Rex::Proto::Redis::Base::Constants::AUTHENTICATION_REQUIRED) ||
93
response.match(Rex::Proto::Redis::Version6::Constants::AUTHENTICATION_REQUIRED)
94
end
95
96
def redis_proto(command_parts)
97
return if command_parts.blank?
98
command = "*#{command_parts.length}\r\n"
99
command_parts.each do |c|
100
command << "$#{c.length}\r\n#{c}\r\n"
101
end
102
command
103
end
104
105
def send_redis_command(*command_parts)
106
sock.put(redis_proto(command_parts))
107
command_response = sock.get(read_timeout)
108
return unless command_response
109
command_response.strip
110
end
111
112
class RESPParser
113
114
LINE_BREAK = "\r\n"
115
116
def initialize(data)
117
@raw_data = data
118
@counter = 0
119
end
120
121
def parse
122
@counter = 0
123
parse_next
124
end
125
126
def data_at_counter
127
@raw_data[@counter..-1]
128
end
129
130
def parse_resp_array
131
# Read array length
132
unless /\A\*(?<arr_len>\d+)(\r|$)/ =~ data_at_counter
133
raise "RESP parsing error in array"
134
end
135
136
@counter += (1 + arr_len.length)
137
138
if data_at_counter.start_with?(LINE_BREAK)
139
@counter += LINE_BREAK.length
140
end
141
142
arr_len = arr_len.to_i
143
144
result = []
145
for index in 1..arr_len do
146
element = parse_next
147
result.append(element)
148
end
149
result
150
end
151
152
def parse_simple_string
153
str_end = data_at_counter.index(LINE_BREAK)
154
str_end = str_end.to_i
155
result = data_at_counter[1..str_end - 1]
156
@counter += str_end
157
@counter += 2 # Skip over next CLRF
158
result
159
end
160
161
def parse_bulk_string
162
unless /\A\$(?<str_len>[-\d]+)(\r|$)/ =~ data_at_counter
163
raise "RESP parsing error in bulk string"
164
end
165
166
@counter += (1 + str_len.length)
167
str_len = str_len.to_i
168
169
if data_at_counter.start_with?(LINE_BREAK)
170
@counter += LINE_BREAK.length
171
end
172
173
result = nil
174
if str_len != -1
175
result = data_at_counter[0..str_len - 1]
176
@counter += str_len
177
@counter += 2 # Skip over next CLRF
178
end
179
result
180
end
181
182
183
def parse_next
184
case data_at_counter[0]
185
when "*"
186
parse_resp_array
187
when "+"
188
parse_simple_string
189
when "$"
190
parse_bulk_string
191
else
192
raise "RESP parsing error: " + data_at_counter
193
end
194
end
195
end
196
end
197
end
198
199