Refactor and fix some specs, avoid sleeping in time-based shared examples

This commit is contained in:
Niboucha Redouane 2020-07-27 03:15:16 +02:00
parent 854df7e93b
commit 89fef9f9fe
4 changed files with 124 additions and 71 deletions

View File

@ -5,5 +5,5 @@ RSpec.describe Msf::Exploit::SQLi::MySQLi::Common do
sqli_obj = Msf::Exploit::SQLi::MySQLi::Common.new({}, {}, {}) {}
expect(sqli_obj.send(:hex_encode_strings, query)).to eql encoded_query
end
it_should_behave_like "Msf::Exploit::SQLi::Common", described_class
it_should_behave_like 'Msf::Exploit::SQLi::Common', described_class
end

View File

@ -0,0 +1,3 @@
RSpec.describe Msf::Exploit::SQLi::MySQLi::TimeBasedBlind do
it_should_behave_like 'TimeBasedBlind', described_class
end

View File

@ -4,7 +4,7 @@ require 'msf/core'
require 'msf/core/module'
require 'msf/core/exploit/sqli'
RSpec.shared_examples "Msf::Exploit::SQLi::Common" do|sqli_class|
RSpec.shared_examples 'Msf::Exploit::SQLi::Common' do |sqli_class|
let(:common_class) do
sqli_class
end
@ -14,89 +14,136 @@ RSpec.shared_examples "Msf::Exploit::SQLi::Common" do|sqli_class|
end
context 'Without opts' do
it 'Should return a mask of (known bits common to all bytes, number of bits to guess)' do
range = (0b01011010..0b01011110) # common bits are 01011***
sqli_obj = common_class.new({}, {}, {}) {}
known_bits, to_guess = sqli_obj.send(:get_bitmask, range)
expect([known_bits, to_guess]).to eql [0b01011000, 3] # 3 changing bits, upper fixed bits = 01011
let(:sqli_obj) do
common_class.new({}, {}, {}) do |payload|
payload[/'(.+?)'/, 1] || ''
end
it 'should yield the query to the block, and return its return value' do
end
context('#run_sql') do
queries = ["select concat(username,':',password) from users", "select 'hello'", 'select 1234 from users']
sqli_obj = common_class.new({}, {}, {}) do |payload|
payload
query_results = [ ':', 'hello', '' ]
queries.each_with_index do |query, i|
it 'Should call vprint_status on run_sql' do
expect(sqli_obj).to receive(:vprint_status).once
expect(sqli_obj.run_sql(query)).to eql query_results[i]
end
queries.each do |query|
expect(sqli_obj.run_sql(query)).to eql query
end
end
it 'Should return true if the block returns what the SQL statement should return' do
sqli_obj1 = common_class.new({}, {}, {}) { |payload| payload[/'(.+?)'/, 1] }
sqli_obj2 = common_class.new({}, {}, {}) { '<div id="articles"></div>' }
expect(sqli_obj1.test_vulnerable).to eql true
expect(sqli_obj2.test_vulnerable).to eql false
context('#test_vulnerable') do
it 'Should detect if the sqli object is expected to perform SQLi successfully' do
expect(sqli_obj.test_vulnerable).to eql true
allow(sqli_obj).to receive(:run_sql).and_return '<div id="articles"></div>'
expect(sqli_obj.test_vulnerable).to eql false
end
it 'Should call vprint_status when running queries' do
sqli_obj = common_class.new({}, {}, {}) { |payload| payload[/'(.+?)'/, 1] }
expect(sqli_obj).to receive(:vprint_status).once
sqli_obj.run_sql("select '1'")
end
it 'Query should have a limit on the number of rows returned' do
context('#dump_table_fields') do
let(:query_proc) do
sqli_obj.instance_variable_get(:@query_proc)
end
result_limit = rand(1..26)
results = result_limit.times.map { |i| ('a'.ord + i).chr }
sqli_obj = common_class.new({}, {}, {}) do |payload|
expect(payload).to match /limit #{result_limit}/i
results.join(',')
common_query = /^select.*password.*from.*maindb\.users\s*;?\s*?(?:#|--)?$/mi
condition_query = /^select.*password.*from.*maindb\.users\s+where/mi
limit_query = /^select.*password.*from.*maindb\.users\s+limit/mi
# query without condition and limit
it 'Should yield valid queries' do
expect(query_proc).to receive(:call).with(common_query).and_call_original
sqli_obj.dump_table_fields('maindb.users', %w[password])
end
# query with condition string
it 'Should yield valid queries when the user adds a condition' do
expect(query_proc).to receive(:call).with(condition_query).and_call_original
sqli_obj.dump_table_fields('maindb.users', %w[password], "username='admin'")
end
# query with limit
it 'Should yield valid queries when the user adds a limit number' do
expect(query_proc).to receive(:call).with(limit_query).and_call_original
sqli_obj.dump_table_fields('maindb.users', %w[password], '', result_limit)
end
expect(sqli_obj.dump_table_fields('information_schema.tables', %w[table_name], '', result_limit)).to(eql(results.map { |name| [name] }))
end
end
context 'truncation_length set' do
let(:opts) do
{ truncation_length: rand(1..20) }
end
it 'Should call truncated_query if truncation_length is set, and should parse its return value correctly' do
sqli_obj = common_class.new({}, {}, {}, opts) { 'a,b,c' }
expect(sqli_obj).to receive(:truncated_query).with(/\^OFFSET\^/).and_return('a,b,c')
expect(sqli_obj.enum_table_names).to eql %w[a b c]
end
it 'Should concatenate slices of the output to produce output similar to that of run_sql' do
dump_data = 'articles,users,purchases,roles,sessions'.each_char.each_slice(opts[:truncation_length]).to_a
i = -1
sqli_obj = common_class.new({}, {}, {}, opts) { dump_data[i += 1] || '' }
expect(sqli_obj.send(:truncated_query, '')).to eql dump_data.join
let(:sqli_obj) do
common_class.new({}, {}, {}, opts) do |payload|
payload[/'(.+?)'/, 1] || ''
end
end
context 'builtin encoder set' do
let(:query_proc) do
sqli_obj.instance_variable_get(:@query_proc)
end
context '#truncated_query should act like run_sql' do
let(:query_result) do
'e951a99943ebe29c6fc425c7df2a0544,028cad8c0961163ef8401d3573b41d8e,b090e41c61a321c99bca94bdb26d1788'
end
let(:sqli_obj) do
i = 0
common_class.new({}, {}, {}, opts) do |_payload|
slice = query_result[i, opts[:truncation_length]]
i += opts[:truncation_length]
if slice.empty?
i = 0
''
else
slice
end
end
end
it 'Should concatenate the slices and return output like run_sql' do
expect(sqli_obj.send(:truncated_query, 'select substr(username,^OFFSET^,' \
"#{opts[:truncation_length]}) from users")).to eql query_result
end
end
end
context 'with builtin encoder set' do
let(:opts) do
{ encoder: common_class::ENCODERS.keys.sample }
end
it 'Should set the encoder correctly' do
let(:sqli_obj) do
common_class.new({}, {}, {}, opts) {}
end
context '#initialize' do
it 'should set the encoder correctly' do
next if common_class::ENCODERS.empty?
current_encoder = common_class::ENCODERS[opts[:encoder]]
sqli_obj = common_class.new({}, {}, {}, opts) {}
object_encoder = sqli_obj.instance_variable_get(:@encoder)
expect(object_encoder).to eql current_encoder
end
end
end
context 'custom encoder set' do
let(:opts) do
{ encoder: { encode: 'reverse(^DATA^)', decode: :reverse.to_proc } }
end
it 'Should yield a query that calls reverse(), and call reverse from Ruby on the results' do
dump_data = %w[articles users purchases roles sessions]
function_call = /reverse\(/i
sqli_obj = common_class.new({}, {}, {}, opts) do |payload|
expect(payload).to match function_call
let(:dump_data) do
%w[
ALL_PLUGINS APPLICABLE_ROLES CHARACTER_SETS CHECK_CONSTRAINTS COLLATIONS
COLLATION_CHARACTER_SET_APPLICABILITY COLUMNS COLUMN_PRIVILEGES ENABLED_ROLES
]
end
let(:sqli_obj) do
common_class.new({}, {}, {}, opts) do |_payload|
# the server response should be encoded
dump_data.map(&:reverse).join(',')
end
end
context '#initialize' do
it 'should set the custom encoder' do
expect(sqli_obj.instance_variable_get(:@encoder)).to eql opts[:encoder]
end
end
context '#enum_table_names' do
function_call = /reverse\(/i
it 'Should apply the encoder correctly in the query, and use the decoder to retrieve decoded results' do
expect(sqli_obj.instance_variable_get(:@query_proc)).to receive(:call).with(function_call).and_call_original
expect(sqli_obj.enum_table_names).to eql dump_data
end
end
end
end

View File

@ -1,26 +1,29 @@
# -*- coding:binary -*-
require 'msf/core'
require 'msf/core/module'
require 'msf/core/exploit/sqli'
require 'base64'
RSpec.shared_examples "TimeBasedBlind" do|sqli_class|
RSpec.shared_examples 'TimeBasedBlind' do |sqli_class|
let(:common_class) do
sqli_class
end
let(:datastore) do
{ 'SqliDelay' => 1.0 }
end
it "Should return true if the block takes more than datastore['SqliDelay'] to run" do
sqli_obj = described_class.new(datastore, {}, {}) do|payload|
sleep(payload[/\d+\.?\d*/].to_f)
let(:sqli_obj) do
creation_time = Time.now
common_class.new(datastore, {}, {}) do |payload|
delay = payload[/\d+(?:.?\d*)?/].to_f
allow(Time).to receive(:now).and_return(creation_time + delay)
end
end
context '#blind_request' do
it "Should return true if the block takes more than datastore['SqliDelay'] to run" do
expect(sqli_obj.send(:blind_request, 'sleep(1.3)')).to eql true
expect(sqli_obj.send(:blind_request, 'sleep(0.5)')).to eql false
expect(sqli_obj.send(:blind_request, 'sleep(0)')).to eql false
end
end
RSpec.describe Msf::Exploit::SQLi::MySQLi::TimeBasedBlind do
it_should_behave_like "TimeBasedBlind", described_class
end
end