Security

Deep Dive: Stored XSS Vulnerability in LiteSpeed Cache Plugin for WordPress (CVE-2024-47374)

An in-depth look at the CVE-2024-47374 vulnerability affecting LiteSpeed Cache plugin for WordPress, its impact, and a technical breakdown of our detection method.

Thu 10 October 2024

Introduction

CVE-2024-47374 is a high-severity vulnerability affecting the LiteSpeed Cache plugin for WordPress, an all-in-one site acceleration plugin that helps improve the performance of a website by storing frequently accessed data in a cache. This stored cross-site scripting (XSS) vulnerability impacts all versions of the plugin up to and including 6.5.0.2. The flaw, if exploited, could allow malicious actors to execute arbitrary JavaScript code.

Technical Details

The vulnerability stems from the way the plugin handles the X-LSCACHE-VARY-VALUE HTTP header. This header value is parsed without adequate sanitization and output escaping, allowing for the injection of javascript code. The flaw is specifically related to the plugin's CSS optimization features.

Vulnerable Components

The CCSS and UCSS generation functions, _ccss() and _load(), accept the necessary parameters and HTTP headers to generate and store the data. The queue is created using the following lines of code:

  • _ccss() in litespeed-cache/src/css.cls.php
private function _ccss()
{
    global $wp;
    $request_url = home_url($wp->request);

    $filepath_prefix = $this->_build_filepath_prefix('ccss');
    $url_tag = $this->_gen_ccss_file_tag($request_url);
    $vary = $this->cls('Vary')->finalize_full_varies();

    ===============  Skip ================

    // Store it to prepare for cron
    Core::comment('QUIC.cloud CCSS in queue');
    $this->_queue = $this->load_queue('ccss');

    if (count($this->_queue) > 500) {
        self::debug('CCSS Queue is full - 500');
        return null;
    }

    $queue_k = (strlen($vary) > 32 ? md5($vary) : $vary) . ' ' . $url_tag;
    $this->_queue[$queue_k] = array(
        'url' => apply_filters('litespeed_ccss_url', $request_url),
        'user_agent' => substr($ua, 0, 200),
        'is_mobile' => $this->_separate_mobile(),
        'is_webp' => $this->cls('Media')->webp_support() ? 1 : 0,
        'uid' => $uid,
        'vary' => $vary,
        'url_tag' => $url_tag,
    ); // Current UA will be used to request
    $this->save_queue('ccss', $this->_queue);
  • load() in litespeed-cache/src/ucss.cls.php
public function load($request_url, $dry_run = false)
{
    ===============  Skip ================

    $vary = $this->cls('Vary')->finalize_full_varies();

    ===============  Skip ================

    // Store it for cron
    $this->_queue = $this->load_queue('ucss');

    if (count($this->_queue) > 500) {
        self::debug('UCSS Queue is full - 500');
        return false;
    }

    $queue_k = (strlen($vary) > 32 ? md5($vary) : $vary) . ' ' . $url_tag;
    $this->_queue[$queue_k] = array(
        'url' => apply_filters('litespeed_ucss_url', $request_url),
        'user_agent' => substr($ua, 0, 200),
        'is_mobile' => $this->_separate_mobile(),
        'is_webp' => $this->cls('Media')->webp_support() ? 1 : 0,
        'uid' => $uid,
        'vary' => $vary,
        'url_tag' => $url_tag,
    ); // Current UA will be used to request
    $this->save_queue('ucss', $this->_queue);

Note that the $vary variable is stored using the $this->save_queue function. The $vary variable is constructed by the finalize_full_varies function which includes data from the X-LSCACHE-VARY-VALUE HTTP header.

public function finalize_full_varies()
{
    $vary = $this->_finalize_curr_vary_cookies(true);
    $vary .= $this->finalize_default_vary(get_current_user_id());
    $vary .= $this->get_env_vary();
    return $vary;
}

/**
    * Get request environment Vary
    *
    * @since  4.0
    */
public function get_env_vary()
{
    $env_vary = isset($_SERVER['LSCACHE_VARY_VALUE']) ? $_SERVER['LSCACHE_VARY_VALUE'] : false;
    if (!$env_vary) {
        $env_vary = isset($_SERVER['HTTP_X_LSCACHE_VARY_VALUE']) ? $_SERVER['HTTP_X_LSCACHE_VARY_VALUE'] : false;
    }
    return $env_vary;
}

When displaying the queue in the admin panel, the Vary Group information (derived from $vary) is printed directly without any sanitization or escaping on the Admin page.

According to the plugin vendor's website, the Vary Group functionality merges the ideas of cache varies and user roles. The vulnerability arises because users can provide the Vary Group through an HTTP header, which is then displayed on the admin page without proper sanitization.

Here's the relevant code snippet from litespeed-cache/tpl/page_optm/settings_css.tpl.php:

<?php if (!empty($ucss_queue)) : ?>
    <div class="litespeed-callout notice notice-warning inline">
        <h4>
            <?php echo sprintf(__('URL list in %s queue waiting for cron', 'litespeed-cache'), 'UCSS'); ?> ( <?php echo count($ucss_queue); ?> )
            <a href="<?php echo Utility::build_url(Router::ACTION_UCSS, UCSS::TYPE_CLEAR_Q); ?>" class="button litespeed-btn-warning litespeed-right">Clear</a>
        </h4>
        <p>
            <?php $i = 0;
            foreach ($ucss_queue as $k => $v) : ?>
                <?php if ($i++ > 20) : ?>
                    <?php echo '...'; ?>
                    <?php break; ?>
                <?php endif; ?>
                <?php if (!is_array($v)) continue; ?>
                <?php if (!empty($v['_status'])) : ?><span class="litespeed-success"><?php endif; ?>
                    <?php echo esc_html($v['url']); ?>
                    <?php if (!empty($v['_status'])) : ?></span><?php endif; ?>
                <?php if ($pos = strpos($k, ' ')) echo ' (' . __('Vary Group', 'litespeed-cache') . ':' . substr($k, 0, $pos) . ')'; ?>
                <?php if ($v['is_mobile']) echo ' <span data-balloon-pos="up" aria-label="mobile">📱</span>'; ?>
                <?php if (!empty($v['is_webp'])) echo ' WebP'; ?>
                <br />
            <?php endforeach; ?>
        </p>
    </div>

This lack of sanitization allows an attacker to inject malicious scripts into the admin panel.

Exploitation Requirements

For the exploit to be successful, two specific Page Optimization settings must be enabled:

  1. CSS Combine: ON
  2. Generate UCSS: ON

Impact of CVE-2024-47374

The vulnerability's impact is significant due to several factors:

High Severity: With a CVSS score of 7.2, this vulnerability is classified as high-severity.

Wide Attack Surface: LiteSpeed Cache is a popular WordPress plugin installed on over 6 million WordPress websites.

Admin Account Takeover: If an administrator's session is hijacked, the attacker could gain complete control over the WordPress site. This is the most severe scenario, as it allows for:

  • Installing malicious plugins or themes
  • Modifying site content
  • Accessing and exfiltrating sensitive data
  • Using the compromised site as a pivot for further attacks

Session Hijacking: The vulnerability could be used to hijack authenticated user sessions, allowing attackers to perform actions on behalf of the victim.

The stored XSS vulnerability in LiteSpeed Cache allows attackers to inject and execute arbitrary JavaScript code. Our detection script focuses on identifying vulnerable instances by checking the plugin version. Let's take a look at it:

VULNERABLE_VERSIONS = [
    r"(?P<major_0_to_5>[0-5])\.(?P<minor>[0-9]+)(\.(?P<patch>[0-9]+))?",
    r"(?P<major_6_0_to_4>6)\.(?P<minor_0_to_4>[0-4])(\.(?P<patch>[0-9]+))?",
    r"(?P<major_6_5>6)\.5\.0(\.(?P<patch_0_to_2>[0-2]))$",
]

class CVE202447374Exploit(webexploit.WebExploit):
    accept_request = definitions.Request(
        method="GET", path="/wp-content/plugins/litespeed-cache/readme.txt"
    )
    check_request = definitions.Request(
        method="GET", path="/wp-content/plugins/litespeed-cache/readme.txt"
    )
    accept_pattern = [re.compile("LiteSpeed Cache")]
    match_pattern = [
        re.compile(f"Stable tag: {version}") for version in VULNERABLE_VERSIONS
    ]

Version Identification Analysis

The VULNERABLE_VERSIONS list contains three regular expressions, each designed to match specific ranges of vulnerable versions:

r"(?P<major_0_to_5>[0-5])\.(?P<minor>[0-9]+)(\.(?P<patch>[0-9]+))?":

  • This regex matches all versions from 0.0.0 to 5.x.x.
  • (?P<major_0_to_5>[0-5]) captures the major version number from 0 to 5.
  • \.(?P<minor>[0-9]+) matches the minor version number.
  • (\.(?P<patch>[0-9]+))? optionally matches a patch version, as some versions might not have it.

r"(?P<major_6_0_to_4>6)\.(?P<minor_0_to_4>[0-4])(\.(?P<patch>[0-9]+))?":

  • This regex covers versions 6.0.x to 6.4.x.
  • (?P<major_6_0_to_4>6) specifically matches major version 6.
  • \.(?P<minor_0_to_4>[0-4]) matches minor versions 0 to 4.
  • The patch version is again optional.

r"(?P<major_6_5>6)\.5\.0(\.(?P<patch_0_to_2>[0-2]))$":

  • This regex is highly specific, targeting only versions 6.5.0, 6.5.0.1, and 6.5.0.2.
  • (?P<major_6_5>6)\.5\.0 matches exactly version 6.5.0.
  • (\.(?P<patch_0_to_2>[0-2]))$ ensures that only patch versions 0, 1, or 2 are matched, and nothing else.

This precise version matching is crucial because the vulnerability affects all versions up to and including 6.5.0.2. By using these carefully crafted regular expressions, the script can accurately identify vulnerable versions while excluding the patched version 6.5.1 and any future releases.

Target Identification and Vulnerability Check

The script uses two key components for identifying the target and checking for vulnerability:

  1. Target Identification: python accept_pattern = [re.compile("LiteSpeed Cache")] This pattern checks for the presence of LiteSpeed Cache in the readme.txt file. It's a simple yet effective way to confirm that the LiteSpeed Cache plugin is installed on the target WordPress site.

  2. Vulnerability Check: python match_pattern = [ re.compile(f"Stable tag: {version}") for version in VULNERABLE_VERSIONS ] This approach creates a list of regex patterns, each looking for the Stable tag: line in the readme.txt file with a version number matching one of the vulnerable versions. By iterating through these patterns, the script can quickly determine if the installed version is vulnerable.

File Request Strategy

The script targets the readme.txt file located at /wp-content/plugins/litespeed-cache/readme.txt. This file is chosen because:

  1. It's typically publicly accessible in WordPress installations.
  2. It contains the version information in a standardized format (Stable tag: X.X.X).
  3. Accessing this file is non-intrusive and doesn't risk modifying or damaging the target system.

By combining these elements, the script provides a robust, accurate, and safe method for detecting instances of LiteSpeed Cache vulnerable to CVE-2024-47374.

Mitigation

To mitigate this vulnerability, website administrators should update the LiteSpeed Cache plugin to version 6.5.1 or later. This version was released on September 25, 2024, and addresses the CVE-2024-47374 vulnerability.

Testing for the Vulnerability Using OXO

If you're concerned your instance might be vulnerable, follow these steps to run a test using the OXO tool:

Install OXO via pip:

pip install -U ostorlab

Install the asteroid agent from the OXO agent store:

oxo agent install agent/ostorlab/asteroid

Run the scan using the asteroid agent with the following command:

oxo scan run --agent agent/ostorlab/asteroid link --url <target-URL> --method GET