0%

PHPMailer漏洞总结

16年,PHPMailer漏洞爆出了RCE漏洞(CVE-2016-10033),影响了众多的CMS;在进行修复之后,由于不完善出现了绕过方法(CVE-2016-10045)。17年的时候,又出现了任意文件读取漏洞(CVE-2017-5223)。在PHPMailer小于5.2.23版本时,又出现了XSS漏洞。这篇文章梳理了下PHPMailer的漏洞。

mail()函数介绍

在进行具体的漏洞分析之前,先介绍一下mail()函数。mail()函数最后是调用系统的/usr/bin/sendmail命令来发送文件的。漏洞关键点在mail()函数的第5个参数。直接来看看第5个参数的定义。

该参数可选,且可通过添加附加的命令作为发送邮件时候的配置,如-f参数设置邮件发件人。传入的参数会经过escapeshellcmd()函数过滤以阻止命令执行。整个问题转化为了找到能够绕过过滤并且能利用的命令参数。可查看sendmail MTA手册http://www.sendmail.org/~ca/email/man/sendmail.html

1
2
3
-X logfile //将所有的log写入logfile。
-C file //临时加载一个配置文件,可读文件。
-O option=value //临时设置一个邮件存储的临时目录的配置。

RCE漏洞

  • 影响版本
    • phpmailer < 5.2.20 (CVE-2016-10033 < 5.2.18)

漏洞分析

然后再来了解一下,PHPMailer的程序调用流程。正常情况下,使用PHPMailer的案例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
  
<?php
require('phpmailer5.2.17/PHPMailerAutoload.php');

$mail = new PHPMailer;
$mail->setFrom($_GET['x'],'Server');
$mail->Subject = '<?php phpinfo();?>';
$mail->addAddress('test@123.com','attacker');
$mail->msgHTML('test');
$mail->AltBody = 'Body';

$mail->send();

从代码上来看,它先调用setFrom()

setFrom()函数中,对传入的address先进行trim,然后进入validateAddress()函数,该函数将传入的address参数进行正则匹配验证邮箱的格式。然后将Subject属性赋值,添加收件人,设置msgHTML,AltBody。然后调用send()函数。

在send()函数中调用了preSend()postSend()。来看看postSend()。

在这里面调用了mailSend()函数。

mailSend()函数中,然后调用了mailPassthru()函数。

在mailPassthru()函数中,最终调用了mail()函数。

漏洞利用

从后往前看,需要调用mail()函数的第5个参数,需要满足1)未开启safe_mode; 2)UseSendmailOptions为True。第二个条件默认满足,只需要满足第一个条件。

在mailSend()函数中,第五个参数为-f加上Sender的值。那么payload的需放在Sender中。Sender是在setFrom()函数中做初始化的,值来自address。在该函数中,会将首位的空格、\r、\n空白字符去掉。然后判断是否含有@字符。之后调用validateAddress()函数对address进行验证。

命令执行

这里的绕过的payload是aaa( -X/var/www/html/success.php )@qq.com

任意文件读取

任意文件读取漏洞用到了上面所述的-C-X参数。当第5个参数为a@qq.com -C/etc/passwd -X/tmp/456,加载临时配置文件/etc/passwd发送邮件,将日志信息保存在/tmp/456中。

在非ubuntu系统下,可能会存在权限问题,可加上 -OQueueDirectory=/tmp。则payload可转化为aaa( -X/var/www/html/success.php -OQueueDirectory=/tmp )@qq.com。或者使用aaa( -X./success.php -oQ/tmp )@qq.com

漏洞修复

在5.2.18中的修复方式是在添加-f参数前,对Sender进行escapeshellarg()进行过滤。

修复绕过

前面介绍过mail()函数对第5个参数本身就会进行escapeshellcmd()的过滤,这就导致了先进行escapeshellarg()后进行escapeshellcmd(),这俩对单引号的处理不同,导致单引号逃逸。关于这个的细节可以阅读https://paper.seebug.org/164/

绕过的payload为:a'( -OQueueDirectory=/tmp -X/tmp/123.php )@qq.com

再次修复

在5.2.20版本中,添加了isShellSafe()函数。


isShellSafe()函数中先用escapeshellcmd()和escapeshellarg()对字符串进行判断,然后过滤了除@_-.以及数字字母之外的字符。

其他利用方式

当系统使用Exim4发送邮件时,Exim4的-be参数支持运行扩展模式。在这些字符串扩展中,如下内容可被利用,更多的可查看https://linux.die.net/man/8/exim

1
2
3
4
${run{<command <args>>}{<string1>}{<string2>}} // 执行<command> <args>,成功返回string1,失败返回string2
${substr{pos}{num}{string}}
${readfile{<file name>}{eol string}} //读取文件,以eol string分割
${readsocket{<name>}{request}{timeout}{eol string}{fail string}}

在Exim4中,可以使用${substr{pos}{num}{string}}从系统变量中截取想要的特殊字符。

参考