{"id":"CVE-2026-45578","aliases":["GHSA-xw67-cg5f-4m2r"],"url":"https://o3.security/vulnerability/CVE-2026-45578","summary":"WWBN AVideo Live: OS command injection in on_publish.php execAsync via unescaped m3u8 URL","details":"## Summary\n\n**Type:** Classic shell-metacharacter injection. The YPTSocket notification branch in `plugin/Live/on_publish.php` builds an `execAsync()` command line by string concatenation, single-quoting each argument but never calling `escapeshellarg()`. A `'` in any of the three interpolated values (`$users_id`, `$m3u8`, `$obj->liveTransmitionHistory_id`) closes the quoted token and lets the attacker append arbitrary commands.\n**File:** `plugin/Live/on_publish.php`, line 267.\n**Root cause:** the developer wrapped each variable in literal single quotes (`'$users_id'`, `'$m3u8'`, `'$obj->liveTransmitionHistory_id'`) believing this provides shell-quoting. PHP single-quoted-into-shell is not safe quoting; it is just two literal quote characters that the shell pairs greedily. Any embedded `'` closes the outer string and resumes interpretation in the shell. The rest of the AVideo codebase already calls `escapeshellarg()` (137 call sites across the project) for ffmpeg invocations, so the safe primitive is well-known to the project; it was simply omitted from this branch. The endpoint is web-reachable (no `.htaccess` rule restricts `on_publish.php`, no `REMOTE_ADDR` check), so the trigger is a direct HTTP POST without going through nginx-rtmp.\n\n## Affected Code\n\n**File:** `plugin/Live/on_publish.php`, lines 256-271.\n\n```php\nif (AVideoPlugin::isEnabledByName('YPTSocket')) {\n    $array = setLiveKey($lth->getKey(), $lth->getLive_servers_id());\n    @ob_clean();\n    _ob_start();\n    $lth = new LiveTransmitionHistory($obj->liveTransmitionHistory_id);\n    $m3u8 = Live::getM3U8File($lth->getKey(), false, true);          // value-carrying URL: contains the stream key verbatim\n    $users_id = $obj->row['users_id'];\n    $liveTransmitionHistory_id = $obj->liveTransmitionHistory_id;\n    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {\n        include \"{$global['systemRootPath']}plugin/Live/on_publish_socket_notification.php\";\n    } else {\n        $command = get_php(). \" {$global['systemRootPath']}plugin/Live/on_publish_socket_notification.php '$users_id' '$m3u8' '{$obj->liveTransmitionHistory_id}'\";  // <-- BUG: literal quotes, no escapeshellarg\n        $pid = execAsync($command);                                  // sink: shell exec\n    }\n}\n```\n\n`Live::getM3U8File($key, false, true)` (`Live.php:1337-1350` -> `Live.php:4845-4889`) returns `\"{$playerServer}{$uuid}.m3u8\"` (or `\"{$playerServer}{$uuid}/index.m3u8\"`) where `$uuid = $this->getKeyWithIndex(...)` is the stream key string read straight out of the `live_transmitions` table. There is no character normalisation between database read and command construction.\n\n**Why it's wrong:** `'$m3u8'` is not shell quoting. PHP interpolates `$m3u8` into the string between two literal `'` characters. The shell then tokenises the result. If `$m3u8` contains `'` itself, the shell sees `'…'` followed by `<attacker bytes>` followed by another `'`, which forms two adjacent quoted strings concatenated with whatever the attacker put between them. Embedded `;`, backticks, `$()`, `&&`, `|`, or `\\n` then run as shell commands. The fix is `escapeshellarg()`, which AVideo already uses 137 times in ffmpeg invocations (e.g. `getVideos.php:1069`, `videos.json.php`, `aVideoEncoder.json.php`); this branch simply forgot it.\n\n## Exploit Chain\n\n1. Attacker authenticates and arranges for one of the command variables to contain `'`. Under the current code the readily available primitive is a `canStream` user supplying a stream key via the persistence path (`saveLive.php`'s `$_REQUEST['key']` is written verbatim to `live_transmitions.key`). State: a row exists with `key = \"evilkey';id>/tmp/pwn;#\"`.\n2. Attacker POSTs directly to `https://target/plugin/Live/on_publish.php` (the file is web-served, no IP restriction) with body:\n   ```\n   name=evilkey';id>/tmp/pwn;#\n   p=<md5(attacker_password)>\n   tcurl=rtmp://target/live\n   addr=1.2.3.4\n   ```\n   `on_publish.php:117` runs `preg_replace(\"/[&=]/\", '', $_POST['name'])` — only `&`/`=` are stripped, so `';id>/tmp/pwn;#` survives. Lines 143-163 confirm `$_GET['p'] === $user->getPassword()` (the attacker is themself, knows their own MD5), persist a `LiveTransmitionHistory` row with the poisoned key, and set `$obj->error = false`. State: authorisation gate passed.\n3. Line 261 calls `Live::getM3U8File($lth->getKey(), false, true)`, returning `\"https://server/live/evilkey';id>/tmp/pwn;#.m3u8\"`. State: `$m3u8` carries the injection payload.\n4. Line 267 builds the command string by concatenation:\n   ```\n   php /var/www/AVideo/plugin/Live/on_publish_socket_notification.php '7' 'https://server/live/evilkey';id>/tmp/pwn;#.m3u8' '42'\n   ```\n   Shell tokenisation sees: `php`, `…/on_publish_socket_notification.php`, `'7'`, `'https://server/live/evilkey'` (the attacker's `'` closed the second quote), then operator `;`, then command-2 `id>/tmp/pwn`, then `;`, then `#.m3u8' '42'` (everything after `#` is a comment). State: the shell has parsed two real commands.\n5. Line 269 `execAsync($command)` spawns the shell, which runs the secondary command `id>/tmp/pwn` as the AVideo PHP-FPM/Apache user. State: arbitrary OS command execution with the privileges of the web-server runtime user.\n6. Final state: the attacker reads `/tmp/pwn`, swaps the payload for a reverse shell, exfiltrates `videos/configuration.php` (database password and root URL), drops a webshell into the upload tree, or pivots to other plugin credentials (PayPal/Stripe API keys, AWS keys for the CDN plugin, OpenAI key for the AI plugin).\n\n## Security Impact\n\n**Severity:** sec-high. Pre-auth-friendly remote code execution: the only prerequisite is that the attacker can place a `'` into one of the three command-line variables, which on a streaming platform means a single low-privilege account.\n**Attacker capability:** with one `canStream` account and two HTTP requests, the attacker executes arbitrary shell commands as the AVideo runtime user. From there: read database credentials, exfiltrate user data, write a webshell into a publicly-served path, pivot to plugin credentials, persist via cron, or escalate via any local sudoers entries.\n**Preconditions:** AVideo deployment with `Live` and `YPTSocket` plugins enabled (the standard live-streaming bundle); attacker can reach `/plugin/Live/on_publish.php` over the network; a value containing `'` is reachable into `users_id`, `m3u8`, or `liveTransmitionHistory_id` (the current code lets `canStream` users supply such a value via the stream-key persistence path).\n**Differential:** source-inspection-verified end-to-end. The shell-tokenising behaviour of `'…'…'…'` is reproducible offline:\n\n```sh\n$ s=\"php /a/b.php '7' 'https://s/live/evilkey';id>/tmp/pwn;#.m3u8' '42'\"\n$ rm -f /tmp/pwn; bash -c \"$s\" 2>/dev/null; ls -l /tmp/pwn\n-rw-r--r-- 1 user user N <date> /tmp/pwn        # injected `id` ran, output captured\n```\n\nThe patched build (with the suggested `escapeshellarg()` fix below applied) constructs `php /a/b.php '7' 'https://s/live/evilkey'\\''id>/tmp/pwn;#.m3u8' '42'`, which the shell parses as a single argument containing the literal characters; the second command never runs.\n\n## Suggested Fix\n\nUse `escapeshellarg()` on every variable interpolated into the command string. This matches established project conventions (137 other call sites for ffmpeg invocations).\n\n```diff\n--- a/plugin/Live/on_publish.php\n+++ b/plugin/Live/on_publish.php\n@@ -264,7 +264,11 @@\n         if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {\n             include \"{$global['systemRootPath']}plugin/Live/on_publish_socket_notification.php\";\n         } else {\n-            $command = get_php(). \" {$global['systemRootPath']}plugin/Live/on_publish_socket_notification.php '$users_id' '$m3u8' '{$obj->liveTransmitionHistory_id}'\";\n+            $command = get_php()\n+                . ' ' . escapeshellarg($global['systemRootPath'] . 'plugin/Live/on_publish_socket_notification.php')\n+                . ' ' . escapeshellarg((string) $users_id)\n+                . ' ' . escapeshellarg((string) $m3u8)\n+                . ' ' . escapeshellarg((string) $obj->liveTransmitionHistory_id);\n             _error_log(\"NGINX Live::on_publish YPTSocket start  ($command)\");\n             $pid = execAsync($command);\n         }\n```\n\nDefence-in-depth: `on_publish.php` is the nginx-rtmp webhook and should not be reachable from the public Internet. Add an `.htaccess`/nginx `location` rule restricting the file to `127.0.0.1` and any configured RTMP server IPs. That blocks the trigger path independently of the sanitisation work.","published":"2026-05-29T13:14:02.663Z","modified":"2026-08-12T03:51:37.785608702Z","cvss":{"score":8.8,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"},"epss":{"score":0.00318,"percentile":0.23959,"asOf":"2026-08-24"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"WWBN/AVideo","fixedVersion":null}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45578.json"},{"type":"ADVISORY","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-xw67-cg5f-4m2r"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45578"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:37.785608702Z"}}