# -*- coding: binary -*-
module Msf

require 'rex/mime'

###
#
# This module exposes methods that may be useful to exploits that send email
# messages via SMTP.
#
###

module Exploit::Remote::SMTPDeliver

  include Exploit::Remote::Tcp

  #
  # Creates an instance of an exploit that delivers messages via SMTP
  #
  def initialize(info = {})
    super

    # Register our options, overriding the RHOST/RPORT from TCP
    register_options(
      [
        OptAddress.new("RHOST", [ true, "The SMTP server to send through" ]),
        OptPort.new("RPORT", [ true, "The SMTP server port (e.g. 25, 465, 587, 2525)", 25 ]),
        OptString.new('DATE', [false, 'Override the DATE: field with this value', '']),
        OptString.new('MAILFROM', [ true, 'The FROM address of the e-mail', 'random@example.com' ]),
        OptString.new('MAILTO', [ true, 'The TO address of the email' ]),
        OptString.new('SUBJECT', [ true, 'Subject line of the email' ]),
        OptString.new('USERNAME', [ false, 'SMTP Username for sending email', '' ]),
        OptString.new('PASSWORD', [ false, 'SMTP Password for sending email', '' ]),
        OptString.new('DOMAIN', [false, 'SMTP Domain to EHLO to', '']),
        OptString.new('VERBOSE', [ false, 'Display verbose information' ]),
      ], Msf::Exploit::Remote::SMTPDeliver)
    register_autofilter_ports([ 25, 465, 587, 2525, 25025, 25000])
    register_autofilter_services(%W{ smtp smtps })

    @connected = false
  end

  def connected?
    (@connected)
  end

  #
  # Establish an SMTP connection to host and port specified by the RHOST and
  # RPORT options, respectively.  After connecting, the banner message is
  # read in and stored in the +banner+ attribute.
  #
  # This method does NOT perform an EHLO, it only connects.
  #
  def connect(global = true)
    fd = super

    if fd
      @connected = true
      # Wait for a banner to arrive...
      self.banner = fd.get_once(-1, 30)
    end
    fd
  end

  #
  # Connect to the remote SMTP server, send EHLO, start TLS if the server
  # asks for it, and authenticate if we've got creds (specified in +USERNAME+
  # and +PASSWORD+ datastore options).
  #
  # This method currently only knows about PLAIN authentication.
  #
  def connect_login(global = true)
    if datastore['DOMAIN'] && datastore['DOMAIN'] != ''
      domain = datastore['DOMAIN']
    else
      domain = Rex::Text.rand_text_alpha(rand(32)+1)
    end

    nsock, res = connect_ehlo(global, domain)

    if res =~ /STARTTLS/
      print_status("Starting tls")
      smtp_send_recv("STARTTLS\r\n", nsock)

      [:high, :medium, :default].each do |level|
        begin
          swap_sock_plain_to_ssl(nsock, level)
          break
        rescue OpenSSL::SSL::SSLError
          # Perform manual fallback for servers that can't
          print_status 'Could not negotiate SSL, falling back to older ciphers'
          nsock.close
          nsock, res = connect_ehlo(global)
          smtp_send_recv("STARTTLS\r\n", nsock)
          raise if level == :default
        end
      end

      res = smtp_send_recv("EHLO #{domain}\r\n", nsock)
    end

    unless datastore['PASSWORD'].empty? and datastore["USERNAME"].empty?
      # TODO: other auth methods
      if res =~ /AUTH .*PLAIN/
        if datastore["USERNAME"] and not datastore["USERNAME"].empty?
          # Have to double the username.  SMTP auth is weird
          user = "#{datastore["USERNAME"]}\0" * 2
          auth = Rex::Text.encode_base64("#{user}#{datastore["PASSWORD"]}")
          res = smtp_send_recv("AUTH PLAIN #{auth}\r\n", nsock)
          unless res[0..2] == '235'
            print_error("Authentication failed, quitting")
            disconnect(nsock)
            raise 'Could not authenticate to SMTP server'
          end
        else
          print_status("Server requested auth and no creds given, trying to continue anyway")
        end
      elsif res =~ /AUTH .*LOGIN/
        if datastore["USERNAME"] and not datastore["USERNAME"].empty?
          user = Rex::Text.encode_base64("#{datastore["USERNAME"]}")
          auth = Rex::Text.encode_base64("#{datastore["PASSWORD"]}")
          smtp_send_recv("AUTH LOGIN\r\n", nsock)
          smtp_send_recv("#{user}\r\n", nsock)
          res = smtp_send_recv("#{auth}\r\n", nsock)
          unless res[0..2] == '235'
            print_error("Authentication failed, quitting")
            disconnect(nsock)
            raise 'Could not authenticate to SMTP server'
          end
        else
          print_status("Server requested auth and no creds given, trying to continue anyway")
        end
      elsif res =~ /AUTH/
        print_error("Server doesn't accept any supported authentication, trying to continue anyway")
      else
        if datastore['PASSWORD'] and datastore["USERNAME"] and not datastore["USERNAME"].empty?
          # Let the user know their creds are going unused
          vprint_status("Server didn't ask for authentication, skipping")
        end
      end
    end

    return nsock
  end

  def connect_ehlo(global = true, domain)
    vprint_status("Connecting to SMTP server #{rhost}:#{rport}...")
    nsock = connect(global)

    [nsock, smtp_send_recv("EHLO #{domain}\r\n", nsock)]
  end

  def bad_address(address)
    address.bytesize > 2048 || /[\r\n]/ =~ address
  end

  #
  # Sends an email message, connecting to the server first if a connection is
  # not already established.
  #
  def send_message(data)
    mailfrom = datastore['MAILFROM'].strip
    if bad_address(mailfrom)
      print_error "Bad from address, not sending: #{mailfrom}"
      return nil
    end

    mailto = datastore['MAILTO'].strip
    if bad_address(mailto)
      print_error "Bad to address, not sending: #{mailto}"
      return nil
    end

    send_status = nil

    already_connected = connected?
    if already_connected
      print_status("Already connected, reusing")
      nsock = self.sock
    else
      nsock = connect_login(false)
    end

    smtp_send_recv("MAIL FROM: <#{mailfrom}>\r\n", nsock)
    res = smtp_send_recv("RCPT TO: <#{mailto}>\r\n", nsock)
    if res && res[0..2] == '250'
      resp = smtp_send_recv("DATA\r\n", nsock)

      # If the user supplied a Date field, use that, else use the current
      # DateTime in the proper RFC2822 format.
      if datastore['DATE'].present?
        date = "Date: #{datastore['DATE']}\r\n"
      else
        date = "Date: #{DateTime.now.rfc2822}\r\n"
      end

      # If the user supplied a Subject field, use that
      subject = nil
      if datastore['SUBJECT'].present?
        subject = "Subject: #{datastore['SUBJECT']}\r\n"
      end

      # Avoid sending tons of data and killing the connection if the server
      # didn't like us.
      if not resp or not resp[0,3] == '354'
        print_error("Server refused our mail")
      else
        full_msg = ''
        full_msg << date unless data =~ /date: /i
        full_msg << subject unless subject.nil? || data =~ /subject: /i
        full_msg << data
        # Escape leading dots in the mail messages so there are no false EOF
        full_msg.gsub!(/(?m)^\./, '..')
        send_status = smtp_send_recv("#{full_msg}\r\n.\r\n", nsock)
      end
    else
      print_error "Server refused to send to <#{mailto}>"
    end

    if not already_connected
      vprint_status("Closing the connection...")
      disconnect(nsock)
    end

    send_status
  rescue SMTPCommunicationError => e
    print_error(e.message)
    if already_connected
      print_error("Closing and reconnecting...")
      disconnect(nsock)
      connect_login
    end
  end

  def disconnect(nsock=self.sock)
    begin
      smtp_send_recv("QUIT\r\n", nsock)
    rescue SMTPCommunicationError => _e
    end
    super
    @connected = false
  end

  # Send and receive a single command using SMTP protocol
  # allowing for response continuation
  def smtp_send_recv(cmd, nsock=self.sock)
    return false if not nsock
    if cmd =~ /AUTH PLAIN/
      # Don't print the user's plaintext password
      vprint_status("C: AUTH PLAIN ...")
    else
      # Truncate because this will include a full email and we don't want
      # to dump it all.
      vprint_status("C: #{((cmd.length > 120) ? cmd[0,120] + "..." : cmd).strip}")
    end
    begin
      nsock.put(cmd)
      res = nsock.get_once
      while !(res =~ /(^|\r\n)\d{3}( .*|)\r\n$/)
        chunk = nsock.get_once
        break unless chunk
        if res
          res += chunk
        else
          res = chunk
        end
      end
      raise SMTPCommunicationError.new("SMTP response is incomplete or contains extra data") unless res =~ /(^|\r\n)\d{3}( .*|)\r\n$/
    rescue EOFError
      return nil
    end
    # Don't truncate the server output because it might be helpful for
    # debugging.
    vprint_status("S: #{res.strip}") if res

    return res
  end


  # The banner received after the initial connection to the server.  This should look something like:
  #   220 mx.google.com ESMTP s5sm3837150wak.12
  attr_reader :banner

protected
  attr_writer :banner #:nodoc:

  #
  # Create a new SSL session on the existing socket.  Used for STARTTLS
  # support.
  #
  def swap_sock_plain_to_ssl(nsock=self.sock, security=:high)
    ctx = generate_ssl_context(security)
    ssl = OpenSSL::SSL::SSLSocket.new(nsock, ctx)

    ssl.connect

    nsock.extend(Rex::Socket::SslTcp)
    nsock.sslsock = ssl
    nsock.sslctx  = ctx
  end

  def generate_ssl_context(security=:high)
    case security
    when :high
      ctx = OpenSSL::SSL::SSLContext.new(:SSLv23)
      ctx.ciphers = "ALL:!ADH:!EXPORT:!SSLv2:!SSLv3:+HIGH:+MEDIUM"
      ctx
    when :medium
      OpenSSL::SSL::SSLContext.new(:TLSv1)
    when :default
      OpenSSL::SSL::SSLContext.new
    end
  end

  class SMTPCommunicationError < RuntimeError
    # private error class for detecting protocol errors
  end
end

end
