Drupal 6 CCK Module Allows Arbitrary PHP Injection

30 November -0001

Abusing Drupal 6 CCK to Inject PHP

Drupal 6 does a rather good job of preventing unauthorized users from injecting PHP into content in order to take control of the web server. Unlike Drupal 5, Drupal 6 does not have a devault PHP input type, which is a huge leap forward in preventing users from crafting PHP. This helps protect the web server from compromise should someone gain Drupal credentials. The Drupal site touts this new feature

PHP format secured The "PHP input format" is now an "opt-in" core module, rather than being enabled by default. Safer for you.

Unfortunately the CCK module for Drupal 6 (arguably the most popular Drupal 6 module available) contains functionality that allows malicious users who have access to create content types to inject arbitrary PHP through the CCK module. Doing so is a bit tricky and requires a rudimentary understanding of the Drupal API, but it is possible.

In order to exploit this vulnerability CCK must be installed and the attacker must be logged in with privileges to content module's "Use PHP input field settings", which understandably (and laudably) is labeled with "dangerous - grant with care." It would be nice if this functionality could be disabled altogether, but as long as it remains in the Drupal 6 user permissions list it is exploitable.

Once logged in the malicious user needs to create a new content type first then create new content of that type. To do the attacker can:

  1. Either navigate through Administer -> Content types -> 'Add a new Content type' or browse to the URL admin/content/types/add.
  2. Fill in arbitrary values for the name, type, and description of the new type and click 'Save content type'
  3. Next the user must click the 'manage fields' link for the new content type.
  4. For this proof of concept the attacker fills out a 'New field' entering aribrary values for Label and field_, but selecting Text for 'Type of data to store.' and 'Text area' for 'Form element to edit the data.
  5. Next click 'Save' to save the new field wich brings up the settngs page for the new field
  6. Expand the 'Default value' link, then expand the 'PHP code' link
  7. In the 'Code:' texarea enter:

    return array(
      0 => isset($_GET['cmd']) ? array('value' => system($_GET['cmd'])) : array('value' => 1)
    );
    

  8. Click save to save the new field.
  9. Click the 'Create content' link or browse to /node/add
  10. Click the new content type under the 'Create content' heading
  11. Append to the URL of the resulting page, for instance, if the resulting create content page URL is http://192.168.0.2/drupal6/node/add/poc change this to append a URL GET variable for 'cmd' like so: http://192.168.0.2/drupal6/node/add/poc?cmd=pwd
  12. Submit the new URL and observe the default value for the new field has changed to list the output of the command.
  13. At this point the attacker has control over the web server process and can issue arbitrary PHP.

Mitigation Strategies

Obviously the "Use PHP input field settings" permission should be granted with extreme care. However, in many scenarios we must consider the danger posed by compromised user accounts. This means that even though permission to utilize CCK's functionality may be limited, we want to be able to mitigate the threat posed by a compromised account of a legitimate user with these permissions to create arbitrary PHP.

It would be ideal if the functionality could be disabled entirely with no possible way to re-enable it. It is possible to hide the UI from the admin interface, but hacking the back end means that upgrades will undo this protection measure and it will have to be repeated on each upgrade (and unfortunately the Drupal upgrade cycle is fairly frequent).

A better strategy would be to detect when a new content type was created that utilized PHP functionality. In this way administrators could be alerted to any use of the functionality, especially rogue use or in situations where you do not expect any users to utilize the functionality.

When a new content type is created a new table is created in the Drupal 6 database. For example, if a new content type called 'poc' was created this could create a new table in the database 'content_type_poc':

mysql> desc content_type_poc;
+------------------+------------------+------+-----+---------+-------+
| Field            | Type             | Null | Key | Default | Extra |
+------------------+------------------+------+-----+---------+-------+
| vid              | int(10) unsigned | NO   | PRI | 0       |       |
| nid              | int(10) unsigned | NO   | MUL | 0       |       |
| field_test_value | longtext         | YES  |     | NULL    |       |
+------------------+------------------+------+-----+---------+-------+
3 rows in set

The values for the PHP code, however, are stored in the content_node_field_instance table. A typical entry for our proof of concept above would be:

mysql> select * from content_node_field_instance where type_name = 'poc';
+------------+-----------+--------+-------+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+---------------+
| field_name | type_name | weight | label | widget_type   | widget_settings                                                                                                                                                                                                | display_settings                                                                                                                                                                                                                                         | description | widget_module | widget_active |
+------------+-----------+--------+-------+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+---------------+
| field_test | poc       |      1 | test  | text_textarea | a:4:{s:4:"rows";s:1:"5";s:4:"size";i:60;s:13:"default_value";N;s:17:"default_value_php";s:108:"return array(
  0 => isset($_GET['cmd']) ? array('value' => system($_GET['cmd'])) : array('value' => 1)
);";} | a:4:{s:5:"label";a:2:{s:6:"format";s:5:"above";s:7:"exclude";i:0;}s:6:"teaser";a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}s:4:"full";a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}i:4;a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}} |             | text          |             1 |
+------------+-----------+--------+-------+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+---------------+

It is easy to spot the PHP entry by monitoring the content_node_field_instance, specifically looking at the widget_settings. New fields that are not defined with a default PHP value will contain the following string in the widget_settings table:

default_value_php";N;

As opposed to a field that did have a default PHP value which would look something like:

"default_value_php";s:108:"return array(
  0 => isset($_GET['cmd']) ? array('value' => system($_GET['cmd'])) : array('value' =gt; 1)
);";} 

Monitoring this table regularly and reporting on rows that do not contain the value 'default_value_php";N;' can easily discover entries where PHP has been used. The SQL query:

mysql> SELECT field_name, type_name, widget_settings FROM content_node_field_instance WHERE widget_settings NOT LIKE '%default_value_php";N%';

Should identify instances where PHP values have been specified.