# -*- 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('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") raw_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) raw_send_recv("STARTTLS\r\n", nsock) raise if level == :default end end 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"]}") res = raw_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"]}") raw_send_recv("AUTH LOGIN\r\n",nsock) raw_send_recv("#{user}\r\n",nsock) res = raw_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, raw_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 raw_send_recv("MAIL FROM: <#{mailfrom}>\r\n", nsock) res = raw_send_recv("RCPT TO: <#{mailto}>\r\n", nsock) if res && res[0..2] == '250' resp = raw_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 = raw_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 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 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 rescue 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 end end