Technical Advisory – playSMS Pre-Authentication Remote Code Execution (CVE-2020-8644)

Vendor: playSMS
Vendor URL:
Versions affected: Before 1.4.3
Systems Affected: All
Author: Lucas Rosevear
Advisory URL / CVE Identifier: CVE-2020-8644
Risk: Critical


PlaySMS is an open source SMS gateway, which has a web management portal written in PHP. PlaySMS supports a custom PHP templating system, called tpl (

PlaySMS double processes a server-side template, resulting in unauthenticated user control of input to the PlaySMS template engine. The template engine’s implementation then permits arbitrary code execution.




An attacker with network connectivity to an instance of PlaySMS can execute arbitrary code.


The vulnerability can be broken into two parts:

  1. A server-side template is processed twice. After the first pass, user
    controlled input is present in the template when it is processed a second time.
  2. The tpl template language is vulnerable to PHP code injection.

These are both detailed below.

Server-side Template Processed Twice

PlaySMS caches the last POST handled by each plugin/route in web/init.php with the following logic:

// save last $_POST in $_SESSION
if ($_POST['X-CSRF-Token']) {
  $_SESSION['tmp']['last_post'][md5(trim(_APP_ . _INC_ . _ROUTE_ . _INC_))] = $_POST;

The login flow in PlaySMS starts by dynamically loading a specified plugin in web/inc/app/main.php. For authentication, the web/plugin/core/auth/login.php file is included. When a user authenticates, a POST request is sent, populating the cache. The authentication plugin then populates a template, found in web/plugin/core/auth/templates/auth_login.html.

This template includes the following:

<td><input type=text class=form-control placeholder="{{ Username or email }}" name=username value='{{ _lastpost('username') }}' maxlength=100></td>

This template gets processed within the authentication plugin, executing the _lastpost method, which returns a value from the POST cache, populating the username field with the last submitted username.

At this point, if a user submitted a custom template string, such as {{1+1}}, this would not be processed, but would now be present in this HTML blob.

Subsequently, back in web/inc/app/main.php, the following logic is executed:

$content = ob_get_clean();


The $content variable contains the HTML with user-controlled input present from the authentication plugin. The themes_apply function ultimately ends up executing the following template:

  $tpl = array(
    'name' => $themes_layout,
    'vars' => array(
      'CONTENT' => $content,
<snip ... />
  $content = tpl_apply($tpl, array(

This logic maps the user-controlled $content to the CONTENT variable in the theme template, as seen in web/plugin/themes/common/templates/themes_layout.html

    <div id="main-content">
      {{ CONTENT }}

Template remote code execution

The vulnerable logic in the tpl templating system begins with variable substitution:

    // check static replaces
    if ($this->vars) {
      foreach ($this->vars as $key => $val) {
        $this->_setString($key, $val);

After this logic executes, the attacker controlled input is directly present in the template that is being processed. Subsequently, the following snippet is executed:

    $pattern = "{{(.*?)}}";
    preg_match_all("/" . $pattern . "/", $this->_result, $matches, PREG_SET_ORDER);

This looks for a pattern of form {{input}}. The input ultimately gets concatenated to a PHP string:

    foreach ($matches as $block) {
      $chunk = $block[0];
      $codes = '<?php ' . $this->config['echo'] . '(' . trim($block[1]) . ')' . '; ?>';
      $this->_result = str_replace($chunk, $codes, $this->_result);

The resulting string is <?php echo(input); ?>

The code then executes this string in one of two ways:

  1. tpl attempts to create a cache file with the malicious string as
    content. $cache_file = md5($this->_filename) . '.compiled'; $cache = $this->config['dir_cache'] . '/' . $cache_file; $fd = @fopen($cache, 'w+'); @fwrite($fd, $this->_result); @fclose($fd); If this succeeds, the code is executed by including the cache file: if (file_exists($cache)) { ob_start(); include $cache;
  2. If caching fails, tpl directly evals the malicious string else { ob_start(); eval('?>' . $this->_result . '<?php ');


Attempt to authenticate with the following username, and no password:

{{`ls -lah`}}

Notice the username field is replaced with the result of the command.


Update to PlaySMS version 1.4.3.

Vendor Communication:

01/25/20: Initial contact with PlaySMS maintainer
01/30/20: Vulnerability details sent to PlaySMS
02/04/20: Vulnerability patched
02/04/20: Patch tested by NCC Group
02/05/20: PlaySMS 1.4.3 released

Thanks to:

Anton Raharja for their responsiveness and quickly patching the vulnerability.

About NCC Group:

NCC Group is a global expert in cyber security and risk mitigation, working with businesses to protect their brand, value and reputation against the ever-evolving threat landscape.

With our knowledge, experience and global footprint, we are best placed to help businesses identify, assess, mitigate and respond to the risks they face.

We are passionate about making the Internet safer and revolutionizing the way in which organizations think about cybersecurity.

Call us before you need us.

Our experts will help you.

Get in touch
%d bloggers like this: