Attacker Value
High
(1 user assessed)
Exploitability
Very High
(1 user assessed)
User Interaction
None
Privileges Required
None
Attack Vector
Network
1

CVE-2023-25135

Disclosure Date: February 03, 2023
Add MITRE ATT&CK tactics and techniques that apply to this CVE.
Initial Access
Techniques
Validation
Validated

Description

vBulletin before 5.6.9 PL1 allows an unauthenticated remote attacker to execute arbitrary code via a crafted HTTP request that triggers deserialization. This occurs because verify_serialized checks that a value is serialized by calling unserialize and then checking for errors. The fixed versions are 5.6.7 PL1, 5.6.8 PL1, and 5.6.9 PL1.

Add Assessment

1
Ratings
Technical Analysis

Overview

This is a pretty cool vulnerability in vBulletin version 5.6.9, 5.6.8 and 5.6.7 prior to PL1 for each respective version. The vulnerability occurs due to improper handing of non-scalar data in vBulletin’s Object-Relational Mapper (ORM), which leads to deserialization of user input without appropriate validation.

There is a great writeup on this bug at https://www.ambionics.io/blog/vbulletin-unserializable-but-unreachable but I’ll try summarize some of the important points here; I’d recommend reading the writeup though as its fairly short and to the point; something that is rare for a technical post of this nature.

The gist of what is going on here is that vBulletin stores non-scalar data in its database in a serialized format using functions like serialize(), and then will call unserialize() to unserialize that data when it needs to retrieve it from the database and use it. Each item to be serialized or deserialized will declare a 3 field structure stating its class, whether its a required field for that class, and a function to verify that the value is correct and modify it if necessary.

The researchers found that the searchprefs property of the vB_DataManager_User class is verified by the verify_serialized() function and is of type vB_Cleaner::TYPE_NOCLEAN, meaning it has no type restricitons. Looking at verify_serialized() we see the following code:

function verify_serialized(&$data)
{
    if ($data === '')
    {
        $data = serialize(array());
        return true;
    }
    else
    {
        if (!is_array($data))
        {
            $data = unserialize($data); // <--------- PROBLEM HERE!!!!
            if ($data === false)
            {
                return false;
            }
        }

        $data = serialize($data);
    }

    return true;
}

The problem in this code is that to verify that the data is actually serialized, we take the untrusted user data in the $data variable and just check that its not an array or a blank string, and if we pass this criteria then we blindly pass it into an unserialize() call, before checking that unserialize() didn’t return false. If unserialize() didn’t return false then its assumed everything went okay and we reserialize the data using serialize(), save that into $data and return true.

The issue here is that no validation is actually done to ensure the serialized data is using expected classes and isn’t just a malicious serialized object. Additionally as we will see later on, just checking that unserialize() doesn’t return false isn’t sufficient; we should also be checking that the object returned is of the expected type.

Whilst the researchers tried to exploit this vulnerability using their normal methods of gadget chains and abusing the applications code, they found this was an issue as vBulletin has a lot of vB_Trait_NoSerialize traits on objects, making them impossible to deserialize without raising an exception. Additionally the one library that they did find some gadget chains in isn’t loaded by default, since the googlelogin package isn’t enabled by default in vBulletin despite it being installed, so they couldn’t use they normal Monolog chain without a bit of tweaking.

What tweaking you may ask? Well it turns out that unserialize() has some interesting behavior. If the class name it receives isn’t valid, then it will return a __PHP_Incomplete_Class object. This will not cause the above code to fail though since it isn’t a false value. Noticing this, the researchers then took a look into the autoloader behavior in vBulletin. The code for the autoloader can be seen below:

spl_autoload_register(array('vB', 'autoload'));

class vB
{
    public static function autoload($classname, $load_map = false, $check_file = true)
    {
        $fclassname = strtolower($classname); // [0]
        $segments = explode('_', $fclassname); // [0-1]

        switch($segments[0]) // [1]
        {
            case 'vb':
                $vbPath = true;
                $filename = VB_PATH; // ./vb/
                break;
            case 'vb5':
                $vbPath = true;
                $filename = VB5_PATH; // ./vb5/
                break;
            default:
                $vbPath = false;
                $filename = VB_PKG_PATH; // ./packages/
                break;
        }

        if (sizeof($segments) > ($vbPath ? 2 : 1))
        {
            $filename .= implode('/', array_slice($segments, ($vbPath ? 1 : 0), -1)) . '/'; // [2]
        }

        $filename .= array_pop($segments) . '.php'; // [3]

        if(file_exists($filename))
            require($filename); // [4]
    }
}

They noticed that vBulletin has an autoloader at vB::autoload() which will be called whenever an unknown class is attempted to be accessed during deserialization. This code will take in a classname, split it on the _ character, check what the first part of the path contains and will append the appropriate directory name to the beginning of the path, and then takes the rest of the path minus the last segment, and squishes it together, using / to separate each part of the final path name. Finally it takes the last item of the split and uses this as the filename to be accessed, appending .php to the end of it before adding it to the final path name. If a file exists at this resulting path, it is then loaded using a require() statement. No validation is done to see if this is an expected file path or similar though, so as long as the file exists in one of the three expected directories (./vb/, ./vb5/, or ./packages/), it will be possible to load it via require() with this code.

The researchers then realized that they could abuse this to load the autoloader of the Monolog library that they had a gadget chain in, such that the autoloader would be loaded into memory allowing them to use any classes within Monolog since any unknown ones will now be loaded by the Monolog autoloader. Keep in mind this is possible because the plugin is normally disabled but still installed, so all that’s needed to use it is for some code to load some of its initializers into memory so that PHP knows where to find the classes in the deserialization gadget chain. With the Monolog autoloader now in place to help load any Monolog classes that the gadget chain may need that aren’t already in memory, the researchers now had everything they needed to make their Monolog deserialization gadget chain work again.

PoC Code

The final PoC can be seen over at https://github.com/ambionics/vbulletin-exploits/blob/main/vbulletin-rce-cve-2023-25135.py and looks roughly like the following code:

POST /ajax/api/user/save HTTP/1.1
Host: 172.17.0.2
Content-Type: application/x-www-form-urlencoded 
Content-Length: 666

securitytoken=guest
&options=
&adminoptions=
&userfield=
&userid=0
&user[email]=pown@pown.net
&user[username]=toto
&password=password
&user[password]=password
&user[searchprefs]=a:2:{i:0;O:27:"googlelogin_vendor_autoload":0:{}i:1;O:32:"Monolog\\Handler\\SyslogUdpHandler":1:s:9:"\x00*\x00socket";O:29:"Monolog\\Handler\\BufferHandler":7:{s:10:"\x00*\x00handler";r:4;s:13:"\x00*\x00bufferSize";i:-1;s:9:"\x00*\x00buffer";a:1:{i:0;a:2:{i:0;s:[LEN]:"[COMMAND]";s:5:"level";N;}}s:8:"\x00*\x00level";N;s:14:"\x00*\x00initialized";b:1;s:14:"\x00*\x00bufferLimit";i:-1;s:13:"\x00*\x00processors";a:2:i:0;s:7:"current";i:1;s:6:"system";}}}}

Final Notes

I was unfortunately unable to determine what user the code will ultimately end up executing as; I presume it would be the user that vBulletin is running as though. However the fact that this is a unauthenticated deserialization bug that can be remotely exploited with no prior knowledge of the target makes it a pretty severe issue. The one saving grace is that it appears this bug may have been limited to only three editions of vBulletin, however if your running any of these versions its highly advisable to upgrade and to also perform a check to see if you have potentially been compromised by this vulnerability. I expect to see more widespread exploitation of this bug in the future given its ease of exploitation.

CVSS V3 Severity and Metrics
Base Score:
9.8 Critical
Impact Score:
5.9
Exploitability Score:
3.9
Vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Attack Vector (AV):
Network
Attack Complexity (AC):
Low
Privileges Required (PR):
None
User Interaction (UI):
None
Scope (S):
Unchanged
Confidentiality (C):
High
Integrity (I):
High
Availability (A):
High

General Information

Vendors

  • vbulletin

Products

  • vbulletin 5.6.7,
  • vbulletin 5.6.8,
  • vbulletin 5.6.9

Additional Info

Technical Analysis