Why mail() is dangerous in PHP
During our advent of PHP application vulnerabilities, we reported a remote command execution vulnerability in the popular webmailer Roundcube (CVE-2016-9920). This vulnerability allowed a malicious user to execute arbitrary system commands on the targeted server by simply writing an email via the Roundcube interface. After we reported the vulnerability to the vendor and released our blog post, similar security vulnerabilities that base on PHP’s built-in mail() function popped up in other PHP applications:
The PHP mail() function
PHP comes with the built-in function mail() for sending emails from a PHP application. The mail delivery can be configured by using the following five parameters.
1 bool mail( 2 string $to, 3 string $subject, 4 string $message [, 5 string $additional_headers [, 6 string $additional_parameters ]] 7 )
The first three parameters of this function are self-explanatory and less sensitive, as these are not affected by injection attacks. Still, be aware that if the to parameter can be controlled by the user, she can send spam emails to an arbitrary address.
Email header injection
The last two optional parameters are more concerning. The fourth parameter $additional_headers receives a string which is appended to the email header. Here, additional email headers can be specified, for example From: and Reply-To:. Since mail headers are separated by the CRLF newline character \r\n, an attacker can use these characters to append additional email headers when user input is used unsanitized in the fourth parameter. This attack is known as Email Header Injection (or short Email Injection). It can be abused to send out multiple spam emails by adding several email addresses to an injected CC: or BCC: header. Note that some mail programs replace \n to \r\n automatically.
Why the 5th parameter of mail() is extremely dangerous
In order to use the mail() function in PHP, an email program or server has to be configured. The following two options can be used in the php.ini configuration file:
- Configure an SMTP server’s hostname and port to which PHP connects
- Configure the file path of a mail program that PHP uses as a Mail Transfer Agent (MTA)
When PHP is configured with the second option, calls to the mail() function will result in the execution of the configured MTA program. Although PHP internally applies escapeshellcmd()to the program call which prevents an injection of new shell commands, the 5th argument $additional_parameters in mail() allows the addition of new program arguments to the MTA. Thus, an attacker can append program flags which in some MTA’s enables the creation of a file with user-controlled content.
mail("email@example.com", "subject", "message", "", "-f" . $_GET['from']);
The code shown above is prone to a remote command execution that is easily overlooked. The GET parameter from is used unsanitized and allows an attacker to pass additional parameters to the mail program. For example, in sendmail, the parameter -O can be used to reconfigure sendmail options and the parameter -X specifies the location of a log file.
Proof of Concept
firstname.lastname@example.org -OQueueDirectory=/tmp -X/var/www/html/rce.php
The proof of concept will drop a PHP shell in the web directory of the application. This file contains log information that can be tainted with PHP code. Thus, an attacker is able to execute arbitrary PHP code on the web server when accessing the rce.php file. You can find more information on how to exploit this issue here.
Latest related security vulnerabilities
The 5th parameter is indeed used in a vulnerable way in many real-world applications. The following popular PHP applications were lately found to be affected, all by the same previously described security issue (mostly reported by Dawid Golunski).
|Zend Framework||< 2.4.11||CVE-2016-10034|
Why escapeshellarg() is not secure
PHP offers escapeshellcmd() and escapeshellarg() to secure user input used in system commands or arguments. Intuitively, the following PHP statement looks secure and prevents a break out of the -param1 parameter:
system(escapeshellcmd("./program -param1 ".escapeshellarg($_GET['arg'])));
However, against all instincts, this statement is insecure when the program has other exploitable parameters. An attacker can break out of the -param1 parameter by injecting "foobar' -param2 payload ". After both escapeshell* functions processed this input, the following string will reach the system() function.
./program -param1 'foobar'\\'' -param2 payload \'
As it can be seen from the executed command, the two nested escaping functions confuse the quoting and allow to append another parameter param2.
PHP’s function mail() internally uses the escapeshellcmd() function in order to secure against command injection attacks. This is exactly why escapeshellarg() does not prevent the attack when used for the 5th parameter of mail(). The developers of Roundcube and PHPMailer implemented this faulty patch at first.
Why FILTER_VALIDATE_EMAIL is not secure
Another intuitive approach is to use PHP’s email filter in order to ensure that only a valid email address is used in the 5th parameter of mail().
However, not all characters that are necessary to exploit the security issue in mail() are forbidden by this filter. It allows the usage of escaped whitespaces nested in double quotes. Due to the nature of the underlying regular expression it is possible to overlap single and double quotes and trick filter_var() into thinking we are inside of double quotes, although mail()s internal escapeshellcmd() thinks we are not.
'a."'\ -OQueueDirectory=\%0D<?=eval($_GET[c])?>\ -X/var/www/html/"@a.php
For the here given url-encoded input, the filter_var() function returns true and rates the payload as a valid email address. This has a critical impact when using this function as a sole security measure: Similar as in our original attack, our malicious "email address" would cause sendmail to print the following error into our newly generated shell "@a.php in our webroot.
<?=eval($_GET[c])?>\/): No such file or directoy
Remember that filter_var() is not appropriate to be used for user-input sanitization and was never designed for such cases, as it is too loose regarding several characters.
How to use mail() securely
Carefully analyze the arguments of each call to mail() in your application for the following conditions:
- Argument (to): Unless intended, no user input is used directly
- Argument (subject): Safe to use
- Argument (message): Safe to use
- Argument (headers): All \r and \n characters are stripped
- Argument (parameters): No user input is used
In fact, there is no guaranteed safe way to use user-supplied data on shell commands and you should not try your luck. In case your application does require user input in the 5th argument, a restrictive email filter can be applied that limits any input to a minimal set of characters, even though it breaks RFC compliance. We recommend to not trust any escaping or quoting routine as history has shown these functions can or will be broken, especially when used in different environments. An alternative approach is developed by Paul Buonopane and can be found here.
Many PHP applications send emails to their users, for example reminders and notifications. While email header injections are widely known, a remote command execution vulnerability is rarely considered when using mail(). In this post, we have highlighted the risks of the 5th mail() parameter and how to protect against attacks that can result in full server compromise. Make sure your application uses this built-in function safely!