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

require 'msf/core/exploit/tcp'
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('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)
		print_verbose("Connecting to SMTP server #{rhost}:#{rport}...")
		nsock = connect(global)

		if datastore['DOMAIN'] and not datastore['DOMAIN'] == ''
			domain = datastore['DOMAIN']
		else
			domain = Rex::Text.rand_text_alpha(rand(32)+1)
		end

		res = raw_send_recv("EHLO #{domain}\r\n", nsock)
		if res =~ /STARTTLS/
			print_status("Starting tls")
			raw_send_recv("STARTTLS\r\n", nsock)
			swap_sock_plain_to_ssl
			res = raw_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"]}")
					raw_send_recv("AUTH PLAIN #{auth}\r\n", nsock)
				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"]}")
					raw_send_recv("AUTH LOGIN\r\n",nsock)
					raw_send_recv("#{user}\r\n",nsock)
					raw_send_recv("#{auth}\r\n",nsock)
				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
					print_verbose("Server didn't ask for authentication, skipping")
				end
			end
		end

		return nsock
	end


	#
	# Sends an email message, connecting to the server first if a connection is
	# not already established.
	#
	def send_message(data)

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

		raw_send_recv("MAIL FROM: <#{datastore['MAILFROM']}>\r\n", nsock)
		raw_send_recv("RCPT TO: <#{datastore['MAILTO']}>\r\n", nsock)

		resp = raw_send_recv("DATA\r\n", nsock)

		# 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
			raw_send_recv("#{data}\r\n.\r\n", nsock)
		end

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

	def disconnect(nsock=self.sock)
		raw_send_recv("QUIT\r\n", nsock)
		super
		@connected = false
	end

	def raw_send_recv(cmd, nsock=self.sock)
		return false if not nsock
		if cmd =~ /AUTH PLAIN/
			# Don't print the user's plaintext password
			print_verbose("C: AUTH PLAIN ...")
		else
			# Truncate because this will include a full email and we don't want
			# to dump it all.
			print_verbose("C: #{((cmd.length > 120) ? cmd[0,120] + "..." : cmd).strip}")
		end

		nsock.put(cmd)
		res = nsock.get_once

		# Don't truncate the server output because it might be helpful for
		# debugging.
		print_verbose("S: #{res.strip}") if res

		return res
	end

	def print_verbose(msg)
		if datastore['VERBOSE']
			print_status(msg)
		end
	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)
		ctx = generate_ssl_context()
		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
		ctx = OpenSSL::SSL::SSLContext.new(:SSLv3)
		ctx.key = OpenSSL::PKey::RSA.new(1024){ }

		ctx.session_id_context = Rex::Text.rand_text(16)

		return ctx
	end

end

end
