You are here

June 2023

Insecure Deserialisation and IDOR, oh my!

A few years ago I found quite an interesting vulnerability in a contributed Drupal module called tablefield.

The module allows Drupal entities to hold tabular data, and the vulnerability was a combination of Insecure Deserialisation and a type of Insecure Direct Object Reference (IDOR).

The fix was released over 4 years ago so sufficient time has passed for me to share some more details.

The module has a hook_menu page callback (Drupal 7's equivalent of a route) that looks like this:

    (
      'tablefield/export/%/%/%/%/%' => array(
      'page callback' => 'tablefield_export_csv',
      'page arguments' => array(2, 3, 4, 5, 6),
      'title' => 'Export Table Data',
      'access arguments' => array('export tablefield'),
    ),

https://git.drupalcode.org/project/tablefield/-/blob/7.x-3.3/tablefield....

The callback function that would pass requests to looked like this:

/**
 * Menu callback to export a table as a CSV.
 *
 * @param string $entity_type
 *   The type of entity, e.g. node.
 * @param string $entity_id
 *   The id of the entity.
 * @param string $field_name
 *   The machine name of the field to load.
 * @param string $langcode
 *   The language code specified.
 * @param string $delta
 *   The field delta to load.
 */
function tablefield_export_csv($entity_type, $entity_id, $field_name, $langcode, $delta) {
  $filename = sprintf('%s_%s_%s_%s_%s.csv', $entity_type, $entity_id, $field_name, $langcode, $delta);
  $uri = 'temporary://' . $filename;
 
  // Attempt to load the entity.
  $ids = array($entity_id);
  $entity = entity_load($entity_type, $ids);
  $entity = array_pop($entity);
 
  // Ensure that the data is available and that we can load a
  // temporary file to stream the data.
  if (isset($entity->{$field_name}[$langcode][$delta]['value']) && $fp = fopen($uri, 'w+')) {
    $table = unserialize($entity->{$field_name}[$langcode][$delta]['value']);
 
...snip...

https://git.drupalcode.org/project/tablefield/-/blob/7.x-3.3/tablefield....

So this page callback takes several parameters from the URL and uses them to load the value of a specific field on a given entity.

It is assumed that this will be a tablefield, and that its content will be serialised tablular data.

The callback passes the value of the field to PHP's unserialize() in order to reconstruct the tabular data in order to export it to a CSV file.

Can you spot a problem here?

It's possible for an attacker to pass any combination of the parameters $entity_type, $entity_id, $field_name, $langcode, $delta in order to load an arbitrary field from the database, and the value stored in that field will be passed to unserialize(). Oops.

In order for this to be an exploitable vulnerability on a site, two conditions need to be met. The attacker must be able to:

  • Store their payload in an entity's field.
  • Access the "Export Table Data" callback (in other words they need to have the "export tablefield" permission).

It's good that the callback is protected by its own permission, but it seems likely that this permission may be granted to fairly low privileged or even anonymous users.

As for storing a payload in an entity field, this too is something that low privileged or even anonymous users are quite often able to do on Drupal sites.

If a user can post a comment, for example, that comment is stored as an entity field. A comment field could be passed to tablefield's callback with a URL like this:

$ curl http:// example.com /tablefield/export/comment/1/comment_body/und/0

In my original report of this vulnerability I gave the following as an example payload:

O:11:"Archive_Tar":1:{s:13:"_temp_tarname";s:23:"/tmp/now_you_see_me.txt";}

This is a serialised object of Drupal 7's Archive_Tar class, which is based on PEAR's Archive_Tar.

At the time of the report, the class destructor would "clean up" by calling unlink() on the value of _temp_tarname if it was set.

    public function __destruct()
    {
        $this->_close();
        // ----- Look for a local copy to delete
        if ($this->_temp_tarname != '') {
            @unlink($this->_temp_tarname);
        }
    }

https://github.com/pear/Archive_Tar/blob/19bb8e95490d3e3ad92fcac95500ca8...

So when this example payload was stored in a comment, then loaded by the callback and passed to unserialize() the Archive_Tar object would briefly be reanimated, then the destructor would be called and the file specified in the payload would be deleted.

This could be used, for example, to delete a .htaccess file protecting a sensitive directory, or preventing PHP uploads from being executed. An attacker with an appetite for destruction might simply try to delete settings.php

Other gadget chains may be available on a given site, but Drupal 7 core does not have many classes that make good candidates for this.

Comments weren't the only viable vector, but they were probably the most likely entity field that an attacker might be able to write to.

After this report was submitted to the Drupal Security Team, the maintainers of the module responded almost immediately and were exemplary in the way they handled the issue and worked on releasing the fix. We also co-ordinated with the Backdrop Security team.

https://www.drupal.org/sa-contrib-2019-045 was rated as Critical 16/25. The fix we agreed on was simply that the callback should check that the given entity field was actually managed by tablefield before passing its value to unserialize():

  // Ensure this is a tablefield.
  $field_info = field_info_field($field_name);
  if (!$field_info || $field_info['type'] != 'tablefield') {
    return drupal_not_found();
  }

https://git.drupalcode.org/project/tablefield/-/compare/7.x-3.4...7.x-3....

Since then, I got a PR merged upstream to harden the destructor in Archive_Tar which means the example payload would no longer delete any arbitrary file.