vBulletin is a commercial forum and blog platform developed by vBulletin Solutions, Inc. It was created over 10 years ago and is written in PHP. It is the world’s most popular forum platform, powering ~78% out of the forums in the top 100K web-sites. Currently there are estimated to be over 40,000 live sites using vBulletin.
A month ago, Check Point privately reported a critical unauthenticated RCE vulnerability to vBulletin support. This vulnerability was independently discovered by Netanel Rubin, and assigned CVE-2015-7808.
When exploited, the vulnerability allows an attacker to execute PHP code on any vBulletin server without requiring user authentication. It does not require any themes or modules other than the ones installed by default.
As widely reported, the main vBulletin.org forum was compromised earlier this week and an exploit for a vBulletin 0-day was up for sale in online markets.
A patch later released by vBulletin fixes the vulnerability reported, but fails to neither credit any reporting nor mention the appropriate CVE number.
As the vulnerability is now fixed and an exploit exists in the wild with public analyses, we follow with the technical description as submitted to vBulletin.
If you administer any vBulletin web site, we urge you to apply the patch as soon as possible, as exploitation risk is imminent.
It is important to note the analyzed public exploit shows a different chain than the one we used for our PoC; therefore we cannot link the attacks directly to our report.
|Oct 4 2015||First contact with vBulletin|
|Oct 5 2015||First response received, asked for PGP key to securely transfer report|
|Oct 6 2015||PGP request denied: “I will hide the response so it’s not publicly available”|
|Oct 10 2015||Sent complete report unencrypted as attached PDF|
|Oct 11 2015||“We cannot accept attachments in our ticket system –
please upload this to your server and provide a link to download this.”
|Oct 11 2015||Uploaded report to a public server and sent a link. Updated with CVE-2015-7808 assignment.|
|Oct 14 2015||Asked vBulletin for confirmation/update|
|Oct 14 2015||“We’re still working to establish the issues and identify any fixes that may be required.”|
|Oct 27 2015||Asked vBulletin for confirmation/update|
|Oct 27 2015||“We’re still working to establish the issues and identify any fixes that may be required.”|
|Nov 2 2015||Patch released by vBulletin|
|Nov 5 2015||Public disclosure|
vBulletin handles a lot of the heavy lifting through an internal API. Unfortunately parts of that API are also used as a gate for Ajax requests, and as a result this API is also accessible through the regular CGI.
This API does not validate the origin of the request, allowing us to access any part of its interface. That interface, in fact, allows us to call any public method in any class that inherits from ‘vB_Api’ and located in ‘/core/vb/api/’. This folder contains dozens of classes and hundreds of methods for us to use as an attack surface, and as noted, some of them considered internal methods. Moreover, we can call these methods with any arguments we’d like, as the API doesn’t have any sort of argument white listing.
Our findings begin with the vulnerable method ‘vB_Api_Hook::decodeArguments()’, which, again, requires no authentication, and at its first line of code contains an ‘unserialize’ call. This can be seen in the following code:
Because we control the ‘$arguments’ parameter we can also control what goes into the ‘unserialize()’ call. That means we can actually craft and inject any object we’d like into the ‘$args’ variable.
PHP objects share several ‘magic methods’ that get called automatically whenever specific events occur. Such a trigger event, for example, is the destruction of an object (the ‘__destruct()’ method gets called), or an attempt to use an object as a string, triggering its ‘__toString()’ method.
That means we can now trigger a call to any ‘__destruct()’ method for any defined class. We only set our object to be of that type, and when the ‘decodeArguments()’ function returns, that object will get destructed.
Examine the following interesting destructor for vB_vURL:
This method takes the ‘tmpfile’ property and treats it as a file name that, if exists, gets deleted.
This can obviously be exploited to remove any file accessible to the web server, most likely rendering the server inoperable, or even causing permanent data loss. However, we are after higher goals. We can leverage this code to gain more access to protected methods in objects. If we insert another object into ‘tmpfile’, an attempt to convert it into a string will happen once the ‘file_exists()’ call is reached. In effect, this now added all ‘__toString()’ methods to our widening attacking surface.
Take a look at vB_View’s ‘__toString()’:
This ‘__toString()’ make a call to vB_View’s ‘render()’ method right off the bat. Unfortunately, this base ‘render()’ method doesn’t do anything of interest for our exploitation purposes. Consider, however, the ‘vB_View_AJAXHTML’ class, inheriting from vB_View, and implementing a very different ‘render()’ method:
As seen above, this ‘render()’ calls another ‘render()’, for the object under the ‘content’ property. Since we can control that one, too, we can now target our ‘render()’ call at any class (not only vB_View objects).
This leads us to the ‘vB5_Template’ class, which implements its own ‘render()’ method:
Basically, this method loads a template from the cache or DB using the template name specified in the ‘template’ property. Afterwards, it evals or includes it based on the template type.
Because the template name is only used inside a (properly escaped) SQL query, we can’t use it for a LFI attempt. Still, this allows us to load any template we want from the DB.
Let’s look at the ‘widget_php’ template code:
The ‘code’ value in the ‘$widgetConfig’ dictionary is the argument for a function which (ultimately) evals its content. If we could control this variable, we would be able to execute any PHP code we’d like on the server.
Let’s see how we do exactly that.
The ‘render()’ method at the ‘vB5_Template’ class call the ‘extract()’ function with the ‘$this->registered’ as an argument. As some readers may observe, ’extract()’ receives a key-value dictionary as an argument and adds the key-value pairs as variables and respective values to the current scope. Because we control ‘$this->registered’, we can add any variable and value to the current scope.
Finally, we add the ‘$widgetConfig’ variable to the ‘registered’ property, set its value as a dictionary containing ‘code’ as a key and our exploit PHP code as its value.