297 lines
8.9 KiB
Ruby
Executable File
297 lines
8.9 KiB
Ruby
Executable File
# -*- 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
|