Skip to content

Commit 3ddf848

Browse files
committed
Merge pull request #1 from hmoore-r7/smtp_ntlm_domain
Module cleanup, error handling, and reporting
2 parents 8306d73 + 99a23ad commit 3ddf848

File tree

2 files changed

+70
-27
lines changed

2 files changed

+70
-27
lines changed

lib/msf/core/exploit/smtp.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def initialize(info = {})
2424
[
2525
Opt::RHOST,
2626
Opt::RPORT(25),
27-
OptString.new('MAILFROM', [ true, 'FROM address of the e-mail', '[email protected]']),
28-
OptString.new('MAILTO', [ true, 'TO address of the e-mail', '[email protected]']),
27+
OptString.new('MAILFROM', [ true, 'FROM address of the e-mail', '[email protected]']),
28+
OptString.new('MAILTO', [ true, 'TO address of the e-mail', '[email protected]']),
2929
], Msf::Exploit::Remote::Smtp)
3030
register_autofilter_ports([ 25, 465, 587, 2525, 25025, 25000])
3131
register_autofilter_services(%W{ smtp smtps})

modules/auxiliary/scanner/smtp/smtp_ntlm_domain.rb

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ class Metasploit3 < Msf::Auxiliary
1414
def initialize
1515
super(
1616
'Name' => 'SMTP NTLM Domain Extraction',
17-
'Description' => 'Extract the Windows domain name given an NTLM challenge.',
17+
'Description' => 'Extract the Windows domain name from a SMTP NTLM challenge.',
1818
'References' => [ ['URL', 'http://msdn.microsoft.com/en-us/library/cc246870.aspx' ] ],
19-
'Author' => [ 'Rich Whitcroft <rwhitcroft@digitalboundary.net>' ],
19+
'Author' => [ 'Rich Whitcroft <rwhitcroft[at]digitalboundary.net>' ],
2020
'License' => MSF_LICENSE
2121
)
2222

@@ -33,48 +33,91 @@ def run_host(ip)
3333
begin
3434
domain = nil
3535
connect
36-
print_status("Connected to #{ip}:#{datastore['RPORT']}")
3736

38-
# send a EHLO and parse the extensions returned
37+
unless banner
38+
vprint_error("#{rhost}:#{rport} No banner received, aborting...")
39+
return
40+
end
41+
42+
vprint_status("#{rhost}:#{rport} Connected: #{banner.strip.inspect}")
43+
44+
# Report the last line of the banner as services information (typically the interesting one)
45+
report_service(host: rhost, port: rport, name: 'smtp', proto: 'tcp', info: banner.strip.split("\n").last)
46+
47+
# Send a EHLO and parse the extensions returned
3948
sock.puts("EHLO " + datastore['EHLO_DOMAIN'] + "\r\n")
40-
exts = sock.get_once.split(/\n/)
4149

42-
# loop through all returned extensions related to NTLM
43-
exts.grep(/NTLM/).each do |ext|
50+
# Find all NTLM references in the EHLO response
51+
exts = sock.get_once.to_s.split(/\n/).grep(/NTLM/)
52+
if exts.length == 0
53+
vprint_error("#{rhost}:#{rport} No NTLM extensions found")
54+
return
55+
end
56+
57+
exts.each do |ext|
4458

45-
# extract the reply minus the first 4 chars (response code + dash)
59+
# Extract the reply minus the first 4 chars (response code + dash)
4660
e = ext[4..-1].chomp
4761

48-
# try the usual AUTH NTLM approach if possible, otherwise echo the extension back to server
62+
# Try the usual AUTH NTLM approach if possible, otherwise echo the extension back to server
4963
if e =~ /AUTH.*NTLM/
5064
sock.puts("AUTH NTLM\r\n")
65+
vprint_status("#{rhost}:#{rport} Sending AUTH NTLM")
5166
else
5267
sock.puts(e + "\r\n")
68+
vprint_status("#{rhost}:#{rport} Sending #{e}")
5369
end
5470

55-
# we expect a "334" code to go ahead with NTLM auth
56-
reply = sock.get_once
57-
if reply.include?("334")
58-
# send the NTLM AUTH blob to tell the server we're ready to auth
71+
# We expect a "334" code to go ahead with NTLM auth
72+
reply = sock.get_once.to_s
73+
if reply !~ /^334\s+/m
74+
vprint_status("#{rhost}:#{rport} Expected a 334 response, received #{reply.strip.inspect} aborting...")
75+
break
76+
else
77+
# Send the NTLM AUTH blob to tell the server we're ready to auth
5978
blob = "TlRMTVNTUAABAAAAt4II4gAAAAAAAAAAAAAAAAAAAAAFAs4OAAAADw=="
6079
sock.puts(blob + "\r\n")
6180

62-
# capture the challenge sent by server
63-
challenge = sock.get_once.split.last
64-
65-
# and extract the domain out of it
66-
domain = Rex::Proto::NTLM::Message.parse(Rex::Text.decode_base64(challenge))[:target_name].value().gsub(/\0/, '')
67-
print_good("Domain: #{domain}")
68-
else
69-
print_error("Error in response: expected '334', aborting")
81+
# Capture the challenge sent by server
82+
challenge = sock.get_once.to_s.split(/\s+/).last
83+
84+
if challenge.length == 0
85+
vprint_status("#{rhost}:#{rport} Empty challenge response, aborting...")
86+
break
87+
end
88+
89+
begin
90+
# Extract the domain out of the NTLM response
91+
ntlm_reply = Rex::Proto::NTLM::Message.parse(Rex::Text.decode_base64(challenge))
92+
if ! ntlm_reply && ntlm_reply.has_key?(:target_name)
93+
vprint_status("#{rhost}:#{rport} Invalid challenge response, aborting...")
94+
break
95+
end
96+
97+
# TODO: Extract the server name from :target_info as well
98+
domain = ntlm_reply[:target_name].value.to_s.gsub(/\x00/, '')
99+
if domain.to_s.length == 0
100+
vprint_status("#{rhost}:#{rport} Invalid target name in challenge response, aborting...")
101+
break
102+
end
103+
104+
print_good("#{rhost}:#{rport} Domain: #{domain}")
105+
report_note(host: rhost, port: rport, proto: 'tcp', type: 'smtp.ntlm_auth_info', data: { domain: domain })
106+
break
107+
108+
rescue ::Rex::ArgumentError
109+
vprint_status("#{rhost}:#{rport} Invalid challenge response message, aborting...")
110+
break
111+
end
70112
end
71113
end
72114

73-
print_error("#{ip}: No NTLM extensions found") if domain.nil? or domain.empty?
115+
if ! domain
116+
vprint_error("#{rhost}:#{rport} No NTLM domain found")
117+
end
74118

75-
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
76-
rescue Timeout::Error => err
77-
print_error(err.message)
119+
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Timeout::Error
120+
# Ignore common networking and response timeout errors
78121
ensure
79122
disconnect
80123
end

0 commit comments

Comments
 (0)