metasploit-framework/lib/msf/core/exploit/mssql.rb

599 lines
13 KiB
Ruby
Raw Normal View History

require 'msf/core'
module Msf
###
#
# This module exposes methods for querying a remote MSSQL service
#
###
module Exploit::Remote::MSSQL
include Exploit::Remote::Udp
include Exploit::Remote::Tcp
#
# Creates an instance of a MSSQL exploit module.
#
def initialize(info = {})
super
# Register the options that all MSSQL exploits may make use of.
register_options(
[
Opt::RHOST,
Opt::RPORT(1433),
OptString.new('MSSQL_USER', [ false, 'The username to authenticate as', 'sa']),
OptString.new('MSSQL_PASS', [ false, 'The password for the specified username', ''])
], Msf::Exploit::Remote::MSSQL)
end
#
# This method sends a UDP query packet to the server and
# parses out the reply packet into a hash
#
def mssql_ping(timeout=5)
data = { }
ping_sock = Rex::Socket::Udp.create(
'PeerHost' => rhost,
'PeerPort' => 1434,
'Context' =>
{
'Msf' => framework,
'MsfExploit' => self,
})
ping_sock.put("\x02")
resp, saddr, sport = ping_sock.recvfrom(65535, timeout)
ping_sock.close
return data if not resp
return data if resp.length == 0
var = nil
return mssql_ping_parse(resp)
end
#
# Parse a 'ping' response and format as a hash
#
def mssql_ping_parse(data)
res = {}
var = nil
idx = data.index('ServerName')
return res if not idx
data[idx, data.length-idx].split(';').each do |d|
if (not var)
var = d
else
if (var.length > 0)
res[var] = d
var = nil
end
end
end
return res
end
#
# Send and receive using TDS
#
def mssql_send_recv(req, timeout=15)
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(not (head and head.length == 8))
return false
end
# Is this the last buffer?
if(head[1,1] == "\x01")
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 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
#
# 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='')
disconnect if self.sock
connect
pkt = ""
idx = 0
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)
info = {:errors => []}
info = mssql_parse_reply(resp,info)
info[:login_ack] ? true : false
end
=begin
def mssql_login_obsolete(user='sa', pass='')
disconnect if self.sock
connect
pkt = ''
host = Rex::Text.rand_text_alpha(rand(8)+1)
proc = Rex::Text.rand_text(rand(8)+1)
appname = Rex::Text.rand_text_alpha(rand(12)+1)
proname = Rex::Text.rand_text_alpha(rand(8)+1)
provers = rand(100)
srvname = ""
rempass = ""
lang = ""
pkt << [host, host.length].pack('Z30C')
pkt << [user, user.length].pack('Z30C')
pkt << [pass, pass.length].pack('Z30C')
pkt << [proc, proc.length].pack('Z8C')
pkt << Rex::Text.rand_text(6)
pkt << [
0x03, # Int2
0x00, # Int4
0x06, # Ascii
0x05, # Fload
0x00, # UseDB
0x00, # DumpLoad
0x00, # Interface
0x00, # Type
0x00 # SSPI required
].pack('C*')
pkt << [appname, appname.length].pack('Z30C')
pkt << [srvname, srvname.length].pack('Z30C')
pkt << [rempass, rempass.length].pack('Z255C')
pkt << [0x04020000].pack('N')
pkt << [proname, proname.length].pack('Z10C')
pkt << [provers].pack('N')
pkt << [lang, lang.length].pack('Z30C')
pkt << [0x00].pack('C') # set lang
pkt << [1024].pack('N') # pkt size
pkt << Rex::Text.rand_text(rand(8)+1) # Padding
pkt = "\x02\x01" + [pkt.length + 8].pack('n') + [rand(0x100)].pack('n') + [rand(0x100)].pack('C') + "\x00" + pkt
sock.put(pkt)
resp = sock.get_once
if (resp and resp.length > 10 and resp[8,1].unpack('C')[0] == 0xe3)
return true
end
return false
end
=end
def mssql_login_datastore(db='')
mssql_login(datastore['MSSQL_USER'], datastore['MSSQL_PASS'], db)
end
def mssql_query(sqla, doprint=false, opts={})
info = { :sql => sqla }
opts[:timeout] ||= 15
pkts = []
idx = 0
bsize = 4096 - 8
chan = 0
@cnt ||= 0
@cnt += 1
sql = Rex::Text.to_unicode(sqla)
while(idx < sql.length)
buf = sql[idx, bsize]
flg = buf.length < bsize ? "\x01" : "\x00"
pkts << "\x01" + flg + [buf.length + 8].pack('n') + [chan].pack('n') + [@cnt].pack('C') + "\x00" + buf
idx += bsize
end
resp = mssql_send_recv(pkts.join, opts[:timeout])
mssql_parse_reply(resp, info)
mssql_print_reply(info) if doprint
info
end
def mssql_xpcmdshell(cmd, doprint=false, opts={})
mssql_query("xp_cmdshell '#{cmd}'", doprint, opts)
end
def mssql_print_reply(info)
print_status("SQL Query: #{info[:sql]}")
if(info[:done] and info[:done][:rows].to_i > 0)
print_status("Row Count: #{info[:done][:rows]} (Status: #{info[:done][:status]} Command: #{info[:done][:cmd]})")
end
if(info[:errors] and not info[:errors].empty?)
info[:errors].each do |err|
print_error(err)
end
end
if(info[:rows] and not info[:rows].empty?)
tbl = Rex::Ui::Text::Table.new(
'Indent' => 1,
'Header' => "",
'Columns' => info[:colnames]
)
info[:rows].each do |row|
tbl << row
end
print_line(tbl.to_s)
end
end
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,173,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
def mssql_parse_reply(data, info)
info[:errors] = []
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
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
def mssql_parse_ret(data, info)
ret = data.slice!(0,4).unpack('N')[0]
info[:ret] = ret
info
end
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
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
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
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
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
end
end