Merge pull request #11 from rapid7/feature/login_scanner/mssql
Add the MSSQL LoginScanner class MSP-9679 #landbug/bundler_fix
commit
d0d9100802
|
@ -5,6 +5,8 @@
|
|||
.sublime-project
|
||||
# RVM control file, keep this to avoid backdooring Metasploit
|
||||
.rvmrc
|
||||
.ruby-version
|
||||
.ruby-gemset
|
||||
# YARD cache directory
|
||||
.yardoc
|
||||
# Mac OS X files
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
metasploit-framework
|
|
@ -1 +0,0 @@
|
|||
1.9.3-p484
|
|
@ -0,0 +1,60 @@
|
|||
require 'metasploit/framework/mssql/client'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
require 'metasploit/framework/login_scanner/ntlm'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This is the LoginScanner class for dealing with Microsoft SQL Servers.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results
|
||||
class MSSQL
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
include Metasploit::Framework::LoginScanner::NTLM
|
||||
include Metasploit::Framework::MSSQL::Client
|
||||
|
||||
# @!attribute windows_authentication
|
||||
# @return [Boolean] Whether to use Windows Authentication instead of SQL Server Auth.
|
||||
attr_accessor :windows_authentication
|
||||
|
||||
validates :windows_authentication,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
def attempt_login(credential)
|
||||
result_options = {
|
||||
credential: credential
|
||||
}
|
||||
|
||||
begin
|
||||
if mssql_login(credential.public, credential.private, '', credential.realm)
|
||||
result_options[:status] = :success
|
||||
else
|
||||
result_options[:status] = :failed
|
||||
end
|
||||
rescue ::Rex::ConnectionError
|
||||
result_options[:status] = :connection_error
|
||||
end
|
||||
|
||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_sane_defaults
|
||||
self.max_send_size = 0 if self.max_send_size.nil?
|
||||
self.send_delay = 0 if self.send_delay.nil?
|
||||
self.send_lm = true if self.send_lm.nil?
|
||||
self.send_ntlm = true if self.send_ntlm.nil?
|
||||
self.send_spn = true if self.send_spn.nil?
|
||||
self.use_ntlm2_session = true if self.use_ntlm2_session.nil?
|
||||
self.use_ntlmv2 = true if self.use_ntlmv2.nil?
|
||||
self.windows_authentication = false if self.windows_authentication.nil?
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
require 'metasploit/framework/login_scanner'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This Concern provides the basic accessors and validations
|
||||
# for protocols that require the use of NTLM for Authentication.
|
||||
module NTLM
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveModel::Validations
|
||||
|
||||
included do
|
||||
# @!attribute send_lm
|
||||
# @return [Boolean] Whether or not to always send the LANMAN response(except if using NTLM2 Session)
|
||||
attr_accessor :send_lm
|
||||
|
||||
# @!attribute send_ntlm
|
||||
# @return [Boolean] Whether or not to use NTLM responses
|
||||
attr_accessor :send_ntlm
|
||||
|
||||
# @!attribute send_spn
|
||||
# @return [Boolean] Whether or not to support SPN for newer Windows OSes
|
||||
attr_accessor :send_spn
|
||||
|
||||
# @!attribute send_lm
|
||||
# @return [Boolean] Whether or not to force the use of NTLM2 session
|
||||
attr_accessor :use_ntlm2_session
|
||||
|
||||
# @!attribute send_lm
|
||||
# @return [Boolean] Whether or not to force the use of NTLMv2 instead of NTLM2 Session
|
||||
attr_accessor :use_ntlmv2
|
||||
|
||||
validates :send_lm,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
validates :send_ntlm,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
validates :send_spn,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
validates :use_ntlm2_session,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
validates :use_ntlmv2,
|
||||
inclusion: { in: [true, false] }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,728 @@
|
|||
require 'metasploit/framework/tcp/client'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module MSSQL
|
||||
|
||||
module Client
|
||||
include Metasploit::Framework::Tcp::Client
|
||||
|
||||
NTLM_CRYPT = Rex::Proto::NTLM::Crypt
|
||||
NTLM_CONST = Rex::Proto::NTLM::Constants
|
||||
NTLM_UTILS = Rex::Proto::NTLM::Utils
|
||||
NTLM_XCEPT = Rex::Proto::NTLM::Exceptions
|
||||
|
||||
# Encryption
|
||||
ENCRYPT_OFF = 0x00 #Encryption is available but off.
|
||||
ENCRYPT_ON = 0x01 #Encryption is available and on.
|
||||
ENCRYPT_NOT_SUP = 0x02 #Encryption is not available.
|
||||
ENCRYPT_REQ = 0x03 #Encryption is required.
|
||||
|
||||
# Packet Type
|
||||
TYPE_SQL_BATCH = 1 # (Client) SQL command
|
||||
TYPE_PRE_TDS7_LOGIN = 2 # (Client) Pre-login with version < 7 (unused)
|
||||
TYPE_RPC = 3 # (Client) RPC
|
||||
TYPE_TABLE_RESPONSE = 4 # (Server) Pre-Login Response ,Login Response, Row Data, Return Status, Return Parameters,
|
||||
# Request Completion, Error and Info Messages, Attention Acknowledgement
|
||||
TYPE_ATTENTION_SIGNAL = 6 # (Client) Attention
|
||||
TYPE_BULK_LOAD = 7 # (Client) SQL Command with binary data
|
||||
TYPE_TRANSACTION_MANAGER_REQUEST = 14 # (Client) Transaction request manager
|
||||
TYPE_TDS7_LOGIN = 16 # (Client) Login
|
||||
TYPE_SSPI_MESSAGE = 17 # (Client) Login
|
||||
TYPE_PRE_LOGIN_MESSAGE = 18 # (Client) pre-login with version > 7
|
||||
|
||||
# Status
|
||||
STATUS_NORMAL = 0x00
|
||||
STATUS_END_OF_MESSAGE = 0x01
|
||||
STATUS_IGNORE_EVENT = 0x02
|
||||
STATUS_RESETCONNECTION = 0x08 # TDS 7.1+
|
||||
STATUS_RESETCONNECTIONSKIPTRAN = 0x10 # TDS 7.3+
|
||||
|
||||
#
|
||||
# This method connects to the server over TCP and attempts
|
||||
# to authenticate with the supplied username and password
|
||||
# The global socket is used and left connected after auth
|
||||
#
|
||||
def mssql_login(user='sa', pass='', db='', domain_name='')
|
||||
|
||||
disconnect if self.sock
|
||||
connect
|
||||
|
||||
# Send a prelogin packet and check that encryption is not enabled
|
||||
if mssql_prelogin() != ENCRYPT_NOT_SUP
|
||||
print_error("Encryption is not supported")
|
||||
return false
|
||||
end
|
||||
|
||||
if windows_authentication
|
||||
idx = 0
|
||||
pkt = ''
|
||||
pkt_hdr = ''
|
||||
pkt_hdr = [
|
||||
TYPE_TDS7_LOGIN, #type
|
||||
STATUS_END_OF_MESSAGE, #status
|
||||
0x0000, #length
|
||||
0x0000, # SPID
|
||||
0x01, # PacketID (unused upon specification
|
||||
# but ms network monitor stil prefer 1 to decode correctly, wireshark don't care)
|
||||
0x00 #Window
|
||||
]
|
||||
|
||||
pkt << [
|
||||
0x00000000, # Size
|
||||
0x71000001, # TDS Version
|
||||
0x00000000, # Dummy Size
|
||||
0x00000007, # Version
|
||||
rand(1024+1), # PID
|
||||
0x00000000, # ConnectionID
|
||||
0xe0, # Option Flags 1
|
||||
0x83, # Option Flags 2
|
||||
0x00, # SQL Type Flags
|
||||
0x00, # Reserved Flags
|
||||
0x00000000, # Time Zone
|
||||
0x00000000 # Collation
|
||||
].pack('VVVVVVCCCCVV')
|
||||
|
||||
cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
|
||||
aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name
|
||||
sname = Rex::Text.to_unicode( rhost )
|
||||
dname = Rex::Text.to_unicode( db )
|
||||
|
||||
ntlm_options = {
|
||||
:signing => false,
|
||||
:usentlm2_session => use_ntlm2_session,
|
||||
:use_ntlmv2 => use_ntlmv2,
|
||||
:send_lm => send_lm,
|
||||
:send_ntlm => send_ntlm
|
||||
}
|
||||
|
||||
ntlmssp_flags = NTLM_UTILS.make_ntlm_flags(ntlm_options)
|
||||
workstation_name = Rex::Text.rand_text_alpha(rand(8)+1)
|
||||
|
||||
ntlmsspblob = NTLM_UTILS::make_ntlmssp_blob_init(domain_name, workstation_name, ntlmssp_flags)
|
||||
|
||||
idx = pkt.size + 50 # lengths below
|
||||
|
||||
pkt << [idx, cname.length / 2].pack('vv')
|
||||
idx += cname.length
|
||||
|
||||
pkt << [0, 0].pack('vv') # User length offset must be 0
|
||||
pkt << [0, 0].pack('vv') # Password length offset must be 0
|
||||
|
||||
pkt << [idx, aname.length / 2].pack('vv')
|
||||
idx += aname.length
|
||||
|
||||
pkt << [idx, sname.length / 2].pack('vv')
|
||||
idx += sname.length
|
||||
|
||||
pkt << [0, 0].pack('vv') # unused
|
||||
|
||||
pkt << [idx, aname.length / 2].pack('vv')
|
||||
idx += aname.length
|
||||
|
||||
pkt << [idx, 0].pack('vv') # locales
|
||||
|
||||
pkt << [idx, 0].pack('vv') #db
|
||||
|
||||
# ClientID (should be mac address)
|
||||
pkt << Rex::Text.rand_text(6)
|
||||
|
||||
# NTLMSSP
|
||||
pkt << [idx, ntlmsspblob.length].pack('vv')
|
||||
idx += ntlmsspblob.length
|
||||
|
||||
pkt << [idx, 0].pack('vv') # AtchDBFile
|
||||
|
||||
pkt << cname
|
||||
pkt << aname
|
||||
pkt << sname
|
||||
pkt << aname
|
||||
pkt << ntlmsspblob
|
||||
|
||||
# Total packet length
|
||||
pkt[0,4] = [pkt.length].pack('V')
|
||||
|
||||
pkt_hdr[2] = pkt.length + 8
|
||||
|
||||
pkt = pkt_hdr.pack("CCnnCC") + pkt
|
||||
|
||||
# Rem : One have to set check_status to false here because sql server sp0 (and maybe above)
|
||||
# has a strange behavior that differs from the specifications
|
||||
# upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header
|
||||
# is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification
|
||||
resp = mssql_send_recv(pkt,15, false)
|
||||
|
||||
# Get default data
|
||||
begin
|
||||
blob_data = NTLM_UTILS.parse_ntlm_type_2_blob(resp)
|
||||
# a domain.length < 3 will hit this
|
||||
rescue NTLM_XCEPT::NTLMMissingChallenge
|
||||
return false
|
||||
end
|
||||
|
||||
challenge_key = blob_data[:challenge_key]
|
||||
server_ntlmssp_flags = blob_data[:server_ntlmssp_flags] #else should raise an error
|
||||
#netbios name
|
||||
default_name = blob_data[:default_name] || ''
|
||||
#netbios domain
|
||||
default_domain = blob_data[:default_domain] || ''
|
||||
#dns name
|
||||
dns_host_name = blob_data[:dns_host_name] || ''
|
||||
#dns domain
|
||||
dns_domain_name = blob_data[:dns_domain_name] || ''
|
||||
#Client time
|
||||
chall_MsvAvTimestamp = blob_data[:chall_MsvAvTimestamp] || ''
|
||||
|
||||
spnopt = {:use_spn => send_spn, :name => self.rhost}
|
||||
|
||||
resp_lm, resp_ntlm, client_challenge, ntlm_cli_challenge = NTLM_UTILS.create_lm_ntlm_responses(user, pass, challenge_key,
|
||||
domain_name, default_name, default_domain,
|
||||
dns_host_name, dns_domain_name, chall_MsvAvTimestamp,
|
||||
spnopt, ntlm_options)
|
||||
|
||||
ntlmssp = NTLM_UTILS.make_ntlmssp_blob_auth(domain_name, workstation_name, user, resp_lm, resp_ntlm, '', ntlmssp_flags)
|
||||
|
||||
# Create an SSPIMessage
|
||||
idx = 0
|
||||
pkt = ''
|
||||
pkt_hdr = ''
|
||||
pkt_hdr = [
|
||||
TYPE_SSPI_MESSAGE, #type
|
||||
STATUS_END_OF_MESSAGE, #status
|
||||
0x0000, #length
|
||||
0x0000, # SPID
|
||||
0x01, # PacketID
|
||||
0x00 #Window
|
||||
]
|
||||
|
||||
pkt_hdr[2] = ntlmssp.length + 8
|
||||
|
||||
pkt = pkt_hdr.pack("CCnnCC") + ntlmssp
|
||||
|
||||
resp = mssql_send_recv(pkt)
|
||||
|
||||
|
||||
#SQL Server Authentification
|
||||
else
|
||||
idx = 0
|
||||
pkt = ''
|
||||
pkt << [
|
||||
0x00000000, # Dummy size
|
||||
|
||||
0x71000001, # TDS Version
|
||||
0x00000000, # Size
|
||||
0x00000007, # Version
|
||||
rand(1024+1), # PID
|
||||
0x00000000, # ConnectionID
|
||||
0xe0, # Option Flags 1
|
||||
0x03, # Option Flags 2
|
||||
0x00, # SQL Type Flags
|
||||
0x00, # Reserved Flags
|
||||
0x00000000, # Time Zone
|
||||
0x00000000 # Collation
|
||||
].pack('VVVVVVCCCCVV')
|
||||
|
||||
|
||||
cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
|
||||
uname = Rex::Text.to_unicode( user )
|
||||
pname = mssql_tds_encrypt( pass )
|
||||
aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
|
||||
sname = Rex::Text.to_unicode( rhost )
|
||||
dname = Rex::Text.to_unicode( db )
|
||||
|
||||
idx = pkt.size + 50 # lengths below
|
||||
|
||||
pkt << [idx, cname.length / 2].pack('vv')
|
||||
idx += cname.length
|
||||
|
||||
pkt << [idx, uname.length / 2].pack('vv')
|
||||
idx += uname.length
|
||||
|
||||
pkt << [idx, pname.length / 2].pack('vv')
|
||||
idx += pname.length
|
||||
|
||||
pkt << [idx, aname.length / 2].pack('vv')
|
||||
idx += aname.length
|
||||
|
||||
pkt << [idx, sname.length / 2].pack('vv')
|
||||
idx += sname.length
|
||||
|
||||
pkt << [0, 0].pack('vv')
|
||||
|
||||
pkt << [idx, aname.length / 2].pack('vv')
|
||||
idx += aname.length
|
||||
|
||||
pkt << [idx, 0].pack('vv')
|
||||
|
||||
pkt << [idx, dname.length / 2].pack('vv')
|
||||
idx += dname.length
|
||||
|
||||
# The total length has to be embedded twice more here
|
||||
pkt << [
|
||||
0,
|
||||
0,
|
||||
0x12345678,
|
||||
0x12345678
|
||||
].pack('vVVV')
|
||||
|
||||
pkt << cname
|
||||
pkt << uname
|
||||
pkt << pname
|
||||
pkt << aname
|
||||
pkt << sname
|
||||
pkt << aname
|
||||
pkt << dname
|
||||
|
||||
# Total packet length
|
||||
pkt[0,4] = [pkt.length].pack('V')
|
||||
|
||||
# Embedded packet lengths
|
||||
pkt[pkt.index([0x12345678].pack('V')), 8] = [pkt.length].pack('V') * 2
|
||||
|
||||
# Packet header and total length including header
|
||||
pkt = "\x10\x01" + [pkt.length + 8].pack('n') + [0].pack('n') + [1].pack('C') + "\x00" + pkt
|
||||
|
||||
resp = mssql_send_recv(pkt)
|
||||
|
||||
end
|
||||
|
||||
info = {:errors => []}
|
||||
info = mssql_parse_reply(resp,info)
|
||||
|
||||
return false if not info
|
||||
info[:login_ack] ? true : false
|
||||
end
|
||||
|
||||
#
|
||||
# Parse an "environment change" TDS token
|
||||
#
|
||||
def mssql_parse_env(data, info)
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
buff = data.slice!(0,len)
|
||||
type = buff.slice!(0,1).unpack('C')[0]
|
||||
|
||||
nval = ''
|
||||
nlen = buff.slice!(0,1).unpack('C')[0] || 0
|
||||
nval = buff.slice!(0,nlen*2).gsub("\x00", '') if nlen > 0
|
||||
|
||||
oval = ''
|
||||
olen = buff.slice!(0,1).unpack('C')[0] || 0
|
||||
oval = buff.slice!(0,olen*2).gsub("\x00", '') if olen > 0
|
||||
|
||||
info[:envs] ||= []
|
||||
info[:envs] << { :type => type, :old => oval, :new => nval }
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse a "ret" TDS token
|
||||
#
|
||||
def mssql_parse_ret(data, info)
|
||||
ret = data.slice!(0,4).unpack('N')[0]
|
||||
info[:ret] = ret
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse a "done" TDS token
|
||||
#
|
||||
def mssql_parse_done(data, info)
|
||||
status,cmd,rows = data.slice!(0,8).unpack('vvV')
|
||||
info[:done] = { :status => status, :cmd => cmd, :rows => rows }
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse an "error" TDS token
|
||||
#
|
||||
def mssql_parse_error(data, info)
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
buff = data.slice!(0,len)
|
||||
|
||||
errno,state,sev,elen = buff.slice!(0,8).unpack('VCCv')
|
||||
emsg = buff.slice!(0,elen * 2)
|
||||
emsg.gsub!("\x00", '')
|
||||
|
||||
info[:errors] << "SQL Server Error ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse an "information" TDS token
|
||||
#
|
||||
def mssql_parse_info(data, info)
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
buff = data.slice!(0,len)
|
||||
|
||||
errno,state,sev,elen = buff.slice!(0,8).unpack('VCCv')
|
||||
emsg = buff.slice!(0,elen * 2)
|
||||
emsg.gsub!("\x00", '')
|
||||
|
||||
info[:infos]||= []
|
||||
info[:infos] << "SQL Server Info ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse a "login ack" TDS token
|
||||
#
|
||||
def mssql_parse_login_ack(data, info)
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
buff = data.slice!(0,len)
|
||||
info[:login_ack] = true
|
||||
end
|
||||
|
||||
#
|
||||
# Parse individual tokens from a TDS reply
|
||||
#
|
||||
def mssql_parse_reply(data, info)
|
||||
info[:errors] = []
|
||||
return if not data
|
||||
until data.empty?
|
||||
token = data.slice!(0,1).unpack('C')[0]
|
||||
case token
|
||||
when 0x81
|
||||
mssql_parse_tds_reply(data, info)
|
||||
when 0xd1
|
||||
mssql_parse_tds_row(data, info)
|
||||
when 0xe3
|
||||
mssql_parse_env(data, info)
|
||||
when 0x79
|
||||
mssql_parse_ret(data, info)
|
||||
when 0xfd, 0xfe, 0xff
|
||||
mssql_parse_done(data, info)
|
||||
when 0xad
|
||||
mssql_parse_login_ack(data, info)
|
||||
when 0xab
|
||||
mssql_parse_info(data, info)
|
||||
when 0xaa
|
||||
mssql_parse_error(data, info)
|
||||
when nil
|
||||
break
|
||||
else
|
||||
info[:errors] << "unsupported token: #{token}"
|
||||
end
|
||||
end
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse a raw TDS reply from the server
|
||||
#
|
||||
def mssql_parse_tds_reply(data, info)
|
||||
info[:errors] ||= []
|
||||
info[:colinfos] ||= []
|
||||
info[:colnames] ||= []
|
||||
|
||||
# Parse out the columns
|
||||
cols = data.slice!(0,2).unpack('v')[0]
|
||||
0.upto(cols-1) do |col_idx|
|
||||
col = {}
|
||||
info[:colinfos][col_idx] = col
|
||||
|
||||
col[:utype] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:flags] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:type] = data.slice!(0,1).unpack('C')[0]
|
||||
|
||||
case col[:type]
|
||||
when 48
|
||||
col[:id] = :tinyint
|
||||
|
||||
when 52
|
||||
col[:id] = :smallint
|
||||
|
||||
when 56
|
||||
col[:id] = :rawint
|
||||
|
||||
when 61
|
||||
col[:id] = :datetime
|
||||
|
||||
when 34
|
||||
col[:id] = :image
|
||||
col[:max_size] = data.slice!(0,4).unpack('V')[0]
|
||||
col[:value_length] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:value] = data.slice!(0, col[:value_length] * 2).gsub("\x00", '')
|
||||
|
||||
when 36
|
||||
col[:id] = :string
|
||||
|
||||
when 38
|
||||
col[:id] = :int
|
||||
col[:int_size] = data.slice!(0,1).unpack('C')[0]
|
||||
|
||||
when 127
|
||||
col[:id] = :bigint
|
||||
|
||||
when 165
|
||||
col[:id] = :hex
|
||||
col[:max_size] = data.slice!(0,2).unpack('v')[0]
|
||||
|
||||
when 173
|
||||
col[:id] = :hex # binary(2)
|
||||
col[:max_size] = data.slice!(0,2).unpack('v')[0]
|
||||
|
||||
when 231,175,167,239
|
||||
col[:id] = :string
|
||||
col[:max_size] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:codepage] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:cflags] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:charset_id] = data.slice!(0,1).unpack('C')[0]
|
||||
|
||||
else
|
||||
col[:id] = :unknown
|
||||
end
|
||||
|
||||
col[:msg_len] = data.slice!(0,1).unpack('C')[0]
|
||||
|
||||
if(col[:msg_len] and col[:msg_len] > 0)
|
||||
col[:name] = data.slice!(0, col[:msg_len] * 2).gsub("\x00", '')
|
||||
end
|
||||
info[:colnames] << (col[:name] || 'NULL')
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Parse a single row of a TDS reply
|
||||
#
|
||||
def mssql_parse_tds_row(data, info)
|
||||
info[:rows] ||= []
|
||||
row = []
|
||||
|
||||
info[:colinfos].each do |col|
|
||||
|
||||
if(data.length == 0)
|
||||
row << "<EMPTY>"
|
||||
next
|
||||
end
|
||||
|
||||
case col[:id]
|
||||
when :hex
|
||||
str = ""
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
if(len > 0 and len < 65535)
|
||||
str << data.slice!(0,len)
|
||||
end
|
||||
row << str.unpack("H*")[0]
|
||||
|
||||
when :string
|
||||
str = ""
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
if(len > 0 and len < 65535)
|
||||
str << data.slice!(0,len)
|
||||
end
|
||||
row << str.gsub("\x00", '')
|
||||
|
||||
when :datetime
|
||||
row << data.slice!(0,8).unpack("H*")[0]
|
||||
|
||||
when :rawint
|
||||
row << data.slice!(0,4).unpack('V')[0]
|
||||
|
||||
when :bigint
|
||||
row << data.slice!(0,8).unpack("H*")[0]
|
||||
|
||||
when :smallint
|
||||
row << data.slice!(0, 2).unpack("v")[0]
|
||||
|
||||
when :smallint3
|
||||
row << [data.slice!(0, 3)].pack("Z4").unpack("V")[0]
|
||||
|
||||
when :tinyint
|
||||
row << data.slice!(0, 1).unpack("C")[0]
|
||||
|
||||
when :image
|
||||
str = ''
|
||||
len = data.slice!(0,1).unpack('C')[0]
|
||||
str = data.slice!(0,len) if (len and len > 0)
|
||||
row << str.unpack("H*")[0]
|
||||
|
||||
when :int
|
||||
len = data.slice!(0, 1).unpack("C")[0]
|
||||
raw = data.slice!(0, len) if (len and len > 0)
|
||||
|
||||
case len
|
||||
when 0,255
|
||||
row << ''
|
||||
when 1
|
||||
row << raw.unpack("C")[0]
|
||||
when 2
|
||||
row << raw.unpack('v')[0]
|
||||
when 4
|
||||
row << raw.unpack('V')[0]
|
||||
when 5
|
||||
row << raw.unpack('V')[0] # XXX: missing high byte
|
||||
when 8
|
||||
row << raw.unpack('VV')[0] # XXX: missing high dword
|
||||
else
|
||||
info[:errors] << "invalid integer size: #{len} #{data[0,16].unpack("H*")[0]}"
|
||||
end
|
||||
else
|
||||
info[:errors] << "unknown column type: #{col.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
info[:rows] << row
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
#this method send a prelogin packet and check if encryption is off
|
||||
#
|
||||
def mssql_prelogin(enc_error=false)
|
||||
|
||||
pkt = ""
|
||||
pkt_hdr = ""
|
||||
pkt_data_token = ""
|
||||
pkt_data = ""
|
||||
|
||||
|
||||
pkt_hdr = [
|
||||
TYPE_PRE_LOGIN_MESSAGE, #type
|
||||
STATUS_END_OF_MESSAGE, #status
|
||||
0x0000, #length
|
||||
0x0000, # SPID
|
||||
0x00, # PacketID
|
||||
0x00 #Window
|
||||
]
|
||||
|
||||
version = [0x55010008,0x0000].pack("Vv")
|
||||
encryption = ENCRYPT_NOT_SUP # off
|
||||
instoptdata = "MSSQLServer\0"
|
||||
|
||||
threadid = "\0\0" + Rex::Text.rand_text(2)
|
||||
|
||||
idx = 21 # size of pkt_data_token
|
||||
pkt_data_token << [
|
||||
0x00, # Token 0 type Version
|
||||
idx , # VersionOffset
|
||||
version.length, # VersionLength
|
||||
|
||||
0x01, # Token 1 type Encryption
|
||||
idx = idx + version.length, # EncryptionOffset
|
||||
0x01, # EncryptionLength
|
||||
|
||||
0x02, # Token 2 type InstOpt
|
||||
idx = idx + 1, # InstOptOffset
|
||||
instoptdata.length, # InstOptLength
|
||||
|
||||
0x03, # Token 3 type Threadid
|
||||
idx + instoptdata.length, # ThreadIdOffset
|
||||
0x04, # ThreadIdLength
|
||||
|
||||
0xFF
|
||||
].pack("CnnCnnCnnCnnC")
|
||||
|
||||
pkt_data << pkt_data_token
|
||||
pkt_data << version
|
||||
pkt_data << encryption
|
||||
pkt_data << instoptdata
|
||||
pkt_data << threadid
|
||||
|
||||
pkt_hdr[2] = pkt_data.length + 8
|
||||
|
||||
pkt = pkt_hdr.pack("CCnnCC") + pkt_data
|
||||
|
||||
resp = mssql_send_recv(pkt)
|
||||
|
||||
idx = 0
|
||||
|
||||
while resp and resp[0,1] != "\xff" and resp.length > 5
|
||||
token = resp.slice!(0,5)
|
||||
token = token.unpack("Cnn")
|
||||
idx -= 5
|
||||
if token[0] == 0x01
|
||||
|
||||
idx += token[1]
|
||||
break
|
||||
end
|
||||
end
|
||||
if idx > 0
|
||||
encryption_mode = resp[idx,1].unpack("C")[0]
|
||||
else
|
||||
#force to ENCRYPT_NOT_SUP and hope for the best
|
||||
encryption_mode = ENCRYPT_NOT_SUP
|
||||
end
|
||||
|
||||
if encryption_mode != ENCRYPT_NOT_SUP and enc_error
|
||||
raise RuntimeError,"Encryption is not supported"
|
||||
end
|
||||
encryption_mode
|
||||
end
|
||||
|
||||
#
|
||||
# Send and receive using TDS
|
||||
#
|
||||
def mssql_send_recv(req, timeout=15, check_status = true)
|
||||
sock.put(req)
|
||||
|
||||
# Read the 8 byte header to get the length and status
|
||||
# Read the length to get the data
|
||||
# If the status is 0, read another header and more data
|
||||
|
||||
done = false
|
||||
resp = ""
|
||||
|
||||
while(not done)
|
||||
head = sock.get_once(8, timeout)
|
||||
if !(head and head.length == 8)
|
||||
return false
|
||||
end
|
||||
|
||||
# Is this the last buffer?
|
||||
if(head[1,1] == "\x01" or not check_status )
|
||||
done = true
|
||||
end
|
||||
|
||||
# Grab this block's length
|
||||
rlen = head[2,2].unpack('n')[0] - 8
|
||||
|
||||
while(rlen > 0)
|
||||
buff = sock.get_once(rlen, timeout)
|
||||
return if not buff
|
||||
resp << buff
|
||||
rlen -= buff.length
|
||||
end
|
||||
end
|
||||
|
||||
resp
|
||||
end
|
||||
|
||||
#
|
||||
# Encrypt a password according to the TDS protocol (encode)
|
||||
#
|
||||
def mssql_tds_encrypt(pass)
|
||||
# Convert to unicode, swap 4 bits both ways, xor with 0xa5
|
||||
Rex::Text.to_unicode(pass).unpack('C*').map {|c| (((c & 0x0f) << 4) + ((c & 0xf0) >> 4)) ^ 0xa5 }.pack("C*")
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def windows_authentication
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def use_ntlm2_session
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def use_ntlmv2
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def send_lm
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def send_ntlm
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def send_spn
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -113,8 +113,6 @@ module Metasploit
|
|||
self.sock = nil
|
||||
end
|
||||
|
||||
# Remove this socket from the list of sockets created by this exploit
|
||||
remove_socket(nsock)
|
||||
end
|
||||
|
||||
##
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
require 'spec_helper'
|
||||
require 'metasploit/framework/login_scanner/mssql'
|
||||
|
||||
describe Metasploit::Framework::LoginScanner::MSSQL do
|
||||
let(:public) { 'root' }
|
||||
let(:private) { 'toor' }
|
||||
|
||||
let(:pub_blank) {
|
||||
Metasploit::Framework::LoginScanner::Credential.new(
|
||||
paired: true,
|
||||
public: public,
|
||||
private: ''
|
||||
)
|
||||
}
|
||||
|
||||
let(:pub_pub) {
|
||||
Metasploit::Framework::LoginScanner::Credential.new(
|
||||
paired: true,
|
||||
public: public,
|
||||
private: public
|
||||
)
|
||||
}
|
||||
|
||||
let(:pub_pri) {
|
||||
Metasploit::Framework::LoginScanner::Credential.new(
|
||||
paired: true,
|
||||
public: public,
|
||||
private: private
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
subject(:login_scanner) { described_class.new }
|
||||
|
||||
it_behaves_like 'Metasploit::Framework::LoginScanner::Base'
|
||||
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
|
||||
it_behaves_like 'Metasploit::Framework::LoginScanner::NTLM'
|
||||
|
||||
it { should respond_to :windows_authentication }
|
||||
|
||||
context 'validations' do
|
||||
context '#windows_authentication' do
|
||||
it 'is not valid for the string true' do
|
||||
login_scanner.windows_authentication = 'true'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:windows_authentication]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is not valid for the string false' do
|
||||
login_scanner.windows_authentication = 'false'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:windows_authentication]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is valid for true class' do
|
||||
login_scanner.windows_authentication = true
|
||||
expect(login_scanner.errors[:windows_authentication]).to be_empty
|
||||
end
|
||||
|
||||
it 'is valid for false class' do
|
||||
login_scanner.windows_authentication = false
|
||||
expect(login_scanner.errors[:windows_authentication]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context '#attempt_login' do
|
||||
context 'when the is a connection error' do
|
||||
it 'returns a result with the connection_error status' do
|
||||
my_scanner = login_scanner
|
||||
my_scanner.should_receive(:mssql_login).and_raise ::Rex::ConnectionError
|
||||
expect(my_scanner.attempt_login(pub_blank).status).to eq :connection_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the login fails' do
|
||||
it 'returns a result object with a status of :failed' do
|
||||
my_scanner = login_scanner
|
||||
my_scanner.should_receive(:mssql_login).and_return false
|
||||
expect(my_scanner.attempt_login(pub_blank).status).to eq :failed
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the login succeeds' do
|
||||
it 'returns a result object with a status of :success' do
|
||||
my_scanner = login_scanner
|
||||
my_scanner.should_receive(:mssql_login).and_return true
|
||||
expect(my_scanner.attempt_login(pub_blank).status).to eq :success
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,135 @@
|
|||
shared_examples_for 'Metasploit::Framework::LoginScanner::NTLM' do
|
||||
|
||||
subject(:login_scanner) { described_class.new }
|
||||
|
||||
it { should respond_to :send_lm }
|
||||
it { should respond_to :send_ntlm }
|
||||
it { should respond_to :send_spn }
|
||||
it { should respond_to :use_ntlm2_session }
|
||||
it { should respond_to :use_ntlmv2 }
|
||||
|
||||
context 'validations' do
|
||||
|
||||
context '#send_lm' do
|
||||
it 'is not valid for the string true' do
|
||||
login_scanner.send_lm = 'true'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:send_lm]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is not valid for the string false' do
|
||||
login_scanner.send_lm = 'false'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:send_lm]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is valid for true class' do
|
||||
login_scanner.send_lm = true
|
||||
expect(login_scanner.errors[:send_lm]).to be_empty
|
||||
end
|
||||
|
||||
it 'is valid for false class' do
|
||||
login_scanner.send_lm = false
|
||||
expect(login_scanner.errors[:send_lm]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context '#send_ntlm' do
|
||||
it 'is not valid for the string true' do
|
||||
login_scanner.send_ntlm = 'true'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:send_ntlm]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is not valid for the string false' do
|
||||
login_scanner.send_ntlm = 'false'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:send_ntlm]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is valid for true class' do
|
||||
login_scanner.send_ntlm = true
|
||||
expect(login_scanner.errors[:send_ntlm]).to be_empty
|
||||
end
|
||||
|
||||
it 'is valid for false class' do
|
||||
login_scanner.send_ntlm = false
|
||||
expect(login_scanner.errors[:send_ntlm]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context '#send_spn' do
|
||||
it 'is not valid for the string true' do
|
||||
login_scanner.send_spn = 'true'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:send_spn]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is not valid for the string false' do
|
||||
login_scanner.send_spn = 'false'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:send_spn]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is valid for true class' do
|
||||
login_scanner.send_spn = true
|
||||
expect(login_scanner.errors[:send_spn]).to be_empty
|
||||
end
|
||||
|
||||
it 'is valid for false class' do
|
||||
login_scanner.send_spn = false
|
||||
expect(login_scanner.errors[:send_spn]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context '#use_ntlm2_session' do
|
||||
it 'is not valid for the string true' do
|
||||
login_scanner.use_ntlm2_session = 'true'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:use_ntlm2_session]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is not valid for the string false' do
|
||||
login_scanner.use_ntlm2_session = 'false'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:use_ntlm2_session]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is valid for true class' do
|
||||
login_scanner.use_ntlm2_session = true
|
||||
expect(login_scanner.errors[:use_ntlm2_session]).to be_empty
|
||||
end
|
||||
|
||||
it 'is valid for false class' do
|
||||
login_scanner.use_ntlm2_session = false
|
||||
expect(login_scanner.errors[:use_ntlm2_session]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context '#use_ntlmv2' do
|
||||
it 'is not valid for the string true' do
|
||||
login_scanner.use_ntlmv2 = 'true'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:use_ntlmv2]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is not valid for the string false' do
|
||||
login_scanner.use_ntlmv2 = 'false'
|
||||
expect(login_scanner).to_not be_valid
|
||||
expect(login_scanner.errors[:use_ntlmv2]).to include 'is not included in the list'
|
||||
end
|
||||
|
||||
it 'is valid for true class' do
|
||||
login_scanner.use_ntlmv2 = true
|
||||
expect(login_scanner.errors[:use_ntlmv2]).to be_empty
|
||||
end
|
||||
|
||||
it 'is valid for false class' do
|
||||
login_scanner.use_ntlmv2 = false
|
||||
expect(login_scanner.errors[:use_ntlmv2]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue