The dangers of type coercion and remote management plugins

Remote management plugins are a popular tool among WordPress site administrators. They allow the user to perform the same action on multiple sites at once, e.g. updating to the latest release or installing a plugin. However, in order to perform these actions the client plugins need to provide a lot of power to a remote user. Therefore it is very important that the communication between the management server and the client plugin is secure and cannot be forged by an attacker. I decided to take a look at some of the available plugins for weaknesses that would allow an attacker to completely compromise a site running one of these plugins.

ManageWP, InfiniteWP, and CMS Commander

These three services share the same code base for the client plugin (I believe it’s originally authored by ManageWP and used by the other two with some tweaks) and so they were all vulnerable to a signature bypass that would allow remote code execution.

Instead of requiring the user to provide administrator credentials, the management server registers a secret key with the client plugin which is used to compute a MAC for each message. A MAC is calculated by taking a message and passing it through a MAC algorithm along with a shared secret key. The message is then sent with the MAC attached. Finally, the recipient can recalculated the MAC of the sent message and check it against the MAC that was attached. MACs are used to verify the authenticity and integrity of a message, i.e. it was sent by the management server and has not been changed in transit. This is a good approach to securing communications, but implementation flaws in the client plugin for these three services lead to a critical security vulnerability.

An incoming message is authenticated by helper.class.php like this:

// $signature is the MAC sent with the message  
// $data is part of the message  
if (md5($data . $this->get_random_signature()) == $signature) {  
    // valid message  
}

The use of non-strict equality means that type juggling occurs before comparison. The output of the md5() function is always a string, but if $signature is an integer then the type coercion can make it relatively easy to forge a matching MAC. For example, if the true MAC starts with “0″ or a non-numeric character then the integer zero will match, if it starts “1x” (where “x” is non-numeric) then the integer 1 will match, and so on.

var_dump('abcdefg' == 0); // true  
var_dump('1abcdef' == 1); // true  
var_dump('2abcdef' == 2); // true

Unfortunately, an attacker can provide an integer as a signature. In init.php incoming requests are parsed by using base64_decode() and then unserializing the result. The use of unserialize() means that an attacker can provide types for the input data. An example of a forged, serialized message is:

a:4:{s:9:"signature";i:0;s:2:"id";i:100000;s:6:"action";s:16:"execute_php_code";s:6:"params";a:2:{s:8:"username";s:5:"admin";s:4:"code";s:25:"exec('touch /tmp/owned');";}}

This message provides the signature as the integer 0 and uses the execute_php_code action provided by the plugin to execute arbitrary PHP code.

$signature = 0;  
// $data is the action concatenated with the message ID  
$data = 'execute_php_code' . 100000;  
if (md5($data . $this->get_random_signature()) == $signature) {  
    // valid message if the output of  
    // md5() doesn't start with a digit  
}

This example forgery is not guaranteed to work straight away. First of all the id key needs to have a value greater than previous legitimate messages (the use of increasing message IDs is being used to prevent replay attacks). Secondly, a matching integer is required for the signature. These two requirements can be bruteforced relatively quickly:

for i from 100,000 to 100,500:
    for j from 0 to 9:
        submit request with id i and signature j

This pseudocode will attempt to send fake messages with very large IDs and for each ID that is attempted ten single digit signatures are used to try and get a match.

This flaw can be fixed by using strict equality (===) and performing other sanity checks on incoming signatures. Fixes were released only changing to strict equality by ManageWP on 19th February, InfiniteWP on 21st February, and CMS Commander on 28th February.

A couple of other problems were reported, but they have not yet been acted on. First, the secret suffix MAC construction used (the secret key is appended to $data and then hashed) has some weaknesses and HMAC should be used instead. Secondly, only the action and message ID are used to create a signature. This means that an active network attacker could change the parameters in a message and the signature would still be valid (e.g. change an execute_php_code message to execute malicious code). To prevent this the MAC should cover the entire message.

(Note that the MD5 based MAC algorithm is a fallback and these client plugins attempt to use openssl_verify() where it is available.)

Worpit

Worpit is another remote management service, but it uses a client plugin built from scratch. It is also vulnerable to a type coercion bug that allows an attacker to log in with administrator privileges.

This client plugin provides a method of remote administrator login “using temporary tokens only configurable by the Worpit package delivery system”. The plugin checks that the token provided in the request matches the one stored in the database:

if ( $_GET['token'] != $oWpHelper->getTransient( 'worpit_login_token' ) ) {  
    die( 'WorpitError: Invalid token' );  
}

The token is deleted from the database once used. This means that most of the time there is no token in the database. Therefore the call to the getTransient() method is likely to return false. Non-strict comparison is used which means that any ‘falsey’ value, such as the string 0, will be treated as a valid token. An example URL to login as administrator:

http://victim/?worpit_api=1&m=login&token=0

From here the site is completely compromised as the attacker has full privileges to install malicious plugins or edit existing ones.

Again the fix is to use strict equality (!==) and to perform other sanity checks on the provided token and the one retrieved from the database. A fix was released on 10th February.

Conclusion

Always remember to check that user input is of the expected type and use strict comparison in security critical functions, such as checking authentication tokens.

This entry was posted on 01 March 2013.