Files
metasploit-gs/lib/msf/core/exploit/smtp_deliver.rb
T
2016-12-16 16:06:02 -06:00

277 lines
8.4 KiB
Ruby

# -*- 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 RuntimeError.new '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 RuntimeError.new '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
#
# Sends an email message, connecting to the server first if a connection is
# not already established.
#
def send_message(data)
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: <#{datastore['MAILFROM']}>\r\n", nsock)
res = raw_send_recv("RCPT TO: <#{datastore['MAILTO']}>\r\n", nsock)
if 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
send_status = raw_send_recv("#{full_msg}\r\n.\r\n", nsock)
end
else
print_error "Server refused to send to <#{datastore['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
nsock.put(cmd)
res = nsock.get_once
# 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