Privilege Escalation Vulnerability in Quttera Web Malware Scanner
One of the big problems we see in trying to improve security is that so often security companies are promoting product and services that they claim will protect websites, but really only try to deal with the after effects of them being hacked. What seems like could explain a lot of that is that most of those companies don’t know or care about security and they are just trying to make a buck with little to no concern whether they are providing anything of value in exchange for that money. One of the things that seems to back that up is how often security companies fail to handle basic security when it comes to their own websites and product/services.
The latest example of that was something we ran across while discussing an example of security companies’ frequent misleading to outright false claims made about their products and services. As discussed over at our main blog the makers of the plugin Quttera Web Malware Scanner had recently claimed that the plugin had over 400,000 installations despite it actually only having 10,000+ active install according to wordpress.org. After running across that we started to take a quick look at the plugin’s security and immediately found it was failing to take some basic security measures.
The plugin makes a number of functions available through WordPress’ AJAX functionality to anyone logged in to WordPress:
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | add_action( 'wp_ajax_scanner-run_scan', 'CQtrAjaxHandler::RunExternalScan' ); /* * setup action @scanner-run_internal_scan mapped to callback qtr_wm_scanner_ajax_run_internal_scan * wp_ajax_ prefix used only for logged in users */ add_action( 'wp_ajax_scanner-run_internal_scan', 'CQtrAjaxHandler::RunInternalScan' ); add_action( 'wp_ajax_scanner-is_internal_scan_running', 'CQtrAjaxHandler::IsInternalScanNowRunning' ); add_action( 'wp_ajax_scanner-get_log_lines', 'CQtrAjaxHandler::GetLogLines' ); add_action( 'wp_ajax_scanner-clean_log', 'CQtrAjaxHandler::CleanLogLines' ); add_action( 'wp_ajax_scanner-get_stats', 'CQtrAjaxHandler::GetStats' ); add_action( 'wp_ajax_scanner-stop_internal_scan', 'CQtrAjaxHandler::StopInternalScan' ); add_action( 'wp_ajax_scanner-get_detected_threats', 'CQtrAjaxHandler::GetDetectedThreatsReport' ); add_action( 'wp_ajax_scanner-get_ignored_threats', 'CQtrAjaxHandler::GetIgnoredThreatsReport' ); add_action( 'wp_ajax_scanner-ignore_threat', 'CQtrAjaxHandler::IgnoreThreat' ); add_action( 'wp_ajax_scanner-get_file_report', 'CQtrAjaxHandler::ScannerReport' ); /* * return threat back to report */ add_action( 'wp_ajax_scanner-unignore_threat', 'CQtrAjaxHandler::RemoveFromIgnoreList' ); add_action( 'wp_ajax_scanner-clean_ignore_list', 'CQtrAjaxHandler::CleanIgnoreList'); add_action( 'wp_ajax_scanner-whitelist_threat','CQtrAjaxHandler::WhiteListThreat' ); add_action( 'wp_ajax_scanner-clean_threats_whitelist', 'CQtrAjaxHandler::CleanThreatsWhiteList'); add_action( 'wp_ajax_scanner-whitelist_file', 'CQtrAjaxHandler::WhiteListFile'); add_action( 'wp_ajax_scanner-clean_files_whitelist', 'CQtrAjaxHandler::CleanFilesWhiteList'); |
The plugin’s admin pages, where at least most of those are intended to be accessed from, is only accessible to those with the “activate_plugins” capability, which would normally be only Administrator-level users. So there should be a check to make sure that users requesting those have that capability.
The first of the functions RunExternalScan() was restricted to those with the “manage_options” capability, which is also normally a capability only Administrator-level users have:
32 33 34 35 36 37 | public static function RunExternalScan() { if(!current_user_can('manage_options')) { wp_die(__('You do not have sufficient permissions to access this page.') ); } |
But that was missing from other functions. For example the GetLogLines() contains no check, so anyone logged in to WordPress could access it:
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | public static function GetLogLines() { $index = 0; $logger = new CQtrLogger(); if( isset( $_GET['start_line']) ) { $index = intval( $_GET['start_line']); } else if( isset( $_POST['start_line']) ) { $index = intval( $_POST['start_line']); } // $lines = $logger->GetFromLine($index); $lines = $logger->GetAllLines(); echo json_encode($lines); exit(); } |
In that case of that function, it would lead to a full path disclosure since it will display the full path to files that have been scanned.
What also was missing in all the functions we looked at was protection against cross-site request forgery (CSRF), so an attacker could cause someone logged in to WordPress to access the various functions without intending it.
After we notified the developer they released version 3.0.9.1, which partially resolves this.
The new version introduces a function __can_access() that ends the running of the code using exit() if the user doesn’t have the “manage_options” capability:
32 33 34 35 36 | private static function __can_access(){ if(!current_user_can('manage_options')){ wp_die(__('You do not have sufficient permissions to access this page.') ); } } |
That function is called first when those AJAX accessible functions mentioned above are run. Here, for example, is that with GetLogLines():
231 232 233 | public static function GetLogLines() { self::__can_access(); |
There still is a lack of protection against CSRF.
Proof of Concept
The following proof of concept will show the last few log lines from the plugin’s scanner, when logged in to WordPress.
Make sure to replace “[path to WordPress]” with the location of WordPress.
http://[path to WordPress]/wp-admin/admin-ajax.php?action=scanner-get_log_lines
Timeline
- June 7, 2018 – Developer notified.
- June 7, 2018 – Developer responds.
- June 8, 2018 – Version 3.0.9.1 released, which partially fixes the issue.