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

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..

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.

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.

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.

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 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.

can we control the $file paramter? 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.

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:

time - just the current time plus 30 minutes ip - the ip of the client connecting to the webserver 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.