PHP中mail()函数安全问题与防御
/0x01 函数简介
(PHP 4, PHP 5, PHP 7)
mail()函数允许您从脚本中直接发送电子邮件。如果邮件的投递被成功地接收,则返回 true,否则返回 false。
函数定义如下:
1 | mail ( string $to , string $subject , string $message [, mixed $additional_headers [, string $additional_parameters ]] ) : bool |
参数说明:
参数 | 描述 |
---|---|
to | 必需。规定邮件的接收者。 |
subject | 必需。规定邮件的主题。该参数不能包含任何换行字符。 |
message | 必需。规定要发送的消息。 |
additional_headers | 可选。规定额外的报头,比如 From, Cc 以及 Bcc。 |
additional_parameters | 可选。规定 sendmail 程序的额外参数。 |
在Linux系统上,mail()函数是默认调用sendmail程序发送邮件的。而这里我们看到,通过mail()函数的第五个参数即additional_parameters可以传递给发送程序sendmail额外参数。
发送一封简单的邮件的Demo代码:
1 |
|
0x02 安全问题
在PHP中,mail()函数无论是自身问题或者助攻其他漏洞方面都是出现很多它的身影的。
我们从参数开始说起,其实可以明显看到mail()函数的前三个必须的参数是不存在类似其他参数所存在的注入风险的,安全问题主要集中在后面两个参数。
垃圾邮件发送
可控点:第一个参数to
要说在mail()函数的三个必须的参数中是否有安全风险,那就是第一个参数即to。
当to参数可由用户从外部输入来控制时,攻击者就可以利用to参数来设定受害者的邮箱来不断地发送垃圾邮件。对服务端本身危害不大,但值得注意。
邮件头注入
可控点:第四个参数additional_headers
前提条件:PHP 5.4.42 和 5.5.27 之前
我们知道additional_headers参数是用于添加额外的报头,由于邮件标题由CRLF换行符\r\n来分隔的,因此当未初始化第四个参数时,攻击者可以使用这些字符来附加其他电子邮件标题。此攻击称为电子邮件头注入(或短电子邮件注入)。通过向注入CC:或BCC:标头添加多个电子邮件地址来发送多个垃圾邮件可能会被滥用。
具体email头注入的示例可参考:PHP Email Injection Example [Updated 2018]
参数注入
可控点:第五个参数additional_parameters
我们知道,mail()函数的第五个参数即additional_parameters可以传递给发送程序sendmail额外参数。
sendmail是Linux中发送邮件的程序。在其额外参数中,支持主要选项有以下三种:
-O option = value
QueueDirectory = queuedir 选择队列消息-X logfile
这个参数可以指定一个目录来记录发送邮件时的详细日志情况,我们正式利用这个参数来达到我们的目的。-f from email
这个参数可以让我们指定我们发送邮件的邮箱地址。-C file
这个参数用File变量指定的备用配置文件启动sendmail命令。
利用1——向Web目录写日志shell
主要原理就是利用mail()第五个参数additional_parameters向sendmail程序发送额外参数-O QueueDirectory=queuedir和-X logfile,其中logfile即详细日志文件设置为Web目录中的PHP文件,而邮件中有部分内容设置为恶意PHP代码,当访问该文件时就会在Web目录生成PHP日志文件、其中详细记录包含了恶意PHP代码,再访问该PHP日志文件即可触发恶意代码执行:
1 |
|
访问该文件后,过段时间(等sendmail程序反应)查看在当前Web目录生成了log-shell.php文件,其中包含我们输入的php代码:
直接访问log-shell.php即可触发代码执行:
利用2——读取任意文件内容
看个参数注入原意为向指定输入邮箱发送mail.txt文件内容,但是应用escapeshellcmd()的方式不对,不能防止用户输入在参数选项的位置,会导致参数注入问题:
1 |
|
利用-C参数选项读取任意文件内容,输入payload`a@b.com -C/etc/passwd -X/tmp/output.txt`:
escapeshellarg()逃逸
对于第五个参数addiional_parameters外部可控即存在参数注入的问题,有小伙伴会通过调用escapeshellarg()来对addiional_parameters参数进行过滤,但是由于在mail()函数的源码中是会调用escapeshellcmd()函数来过滤addiional_parameters参数的,从而导致了escapeshellarg>escapeshellcmd参数注入,逃逸了escapeshellarg()的过滤。
具体原理和Demo参考:《浅谈escapeshellarg逃逸与参数注入》
受该漏洞影响的一些扩展应用组件信息如下表:
Application | Version | Reference |
---|---|---|
Roundcube | <= 1.2.2 | CVE-2016-9920 |
MediaWiki | < 1.29 | Discussion |
PHPMailer | <= 5.2.18 | CVE-2016-10033 |
Zend Framework | < 2.4.11 | CVE-2016-10034 |
SwiftMailer | <= 5.4.5-DEV | CVE-2016-10074 |
SquirrelMail | <= 1.4.23 | CVE-2017-7692 |
不安全的FILTER_VALIDATE_EMAIL
有些程序员会将第五个参数additional_parameters通过调用FILTER_VALIDATE_EMAIL过滤器来进行email地址的校验,但是仅仅靠FILTER_VALIDATE_EMAIL来实现防御还是存在安全问题的。
下面先看下几个基本概念。
filter_var()函数
(PHP 5 >= 5.2.0, PHP 7)
filter_var() 函数通过指定的过滤器过滤一个变量。如果成功,则返回被过滤的数据。如果失败,则返回 FALSE。
函数定义如下:
1 | filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] ) : mixed |
参数 | 描述 |
---|---|
variable | 必需。规定要过滤的变量。 |
filter | 可选。规定要使用的过滤器的 ID。默认是 FILTER_SANITIZE_STRING。参见 完整的 PHP Filter 参考手册,查看可能的过滤器。过滤器 ID 可以是 ID 名称(比如 FILTER_VALIDATE_EMAIL)或 ID 号(比如 274)。 |
options | 可选。规定一个包含标志/选项的关联数组或者一个单一的标志/选项。检查每个过滤器可能的标志和选项。 |
可以看到,该函数第二个参数是可以设置指定过滤器的,其中对于email地址的过滤可以设置FILTER_VALIDATE_EMAIL过滤器。
FILTER_VALIDATE_EMAIL过滤器
FILTER_VALIDATE_EMAIL过滤器把值作为email地址来验证。
示例Demo:
1 |
|
安全缺陷
关于 filter_var() 中 FILTER_VALIDATE_EMAIL 这个选项作用,我们可以看看这个帖子 PHP FILTER_VALIDATE_EMAIL 。这里面有个结论引起了我的注意: none of the special characters in this local part are allowed outside quotation marks ,表示所有的特殊符号必须放在双引号中。 filter_var() 问题在于,我们能够在双引号中嵌套转义空格仍然能够通过检测。同时由于底层正则表达式的原因,我们通过重叠单引号和双引号,欺骗 filter_val() 使其认为我们仍然在双引号中,我们就可以绕过检测。
我们写个简单的例子就知道:
1 |
|
可以看到,这样就能逃逸出FILTER_VALIDATE_EMAIL来输入单引号和空格等字符了,进而可以结合其他漏洞打出组合拳进行高阶利用。
助攻disable_functions绕过
Bash破壳漏洞
具体原理和示例可看:浅谈几种Bypass disable_functions的方法
这里简单说下mail()函数为啥进行了助攻:因为mail()函数的第五个参数additional_parameters在mail.c的源码中会直接拼接成一条新的命令然后传入popen()中执行,当目标系统存在Bash破壳漏洞时,攻击者就可以借助mail()函数来注入第五个参数从而实现disable_functions的绕过。
LD_PRELOAD
具体原理和示例可看:浅谈几种Bypass disable_functions的方法
这里简单说下mail()函数为啥进行了助攻:我们知道这种方法绕过disable_functions主要是通过劫持某个函数如getuid()或劫持启动进程来实现的,而调用mail()函数会启动进程或调用系统sendmail程序,进而调用getuid()函数从而实现劫持,最终导致disable_functions被绕过。
More
当然,以上列的只是常见的遇到,外国佬研究的比较深,有多个新的攻击利用向量可以借鉴,具体的看文章就好:
原文:Pwning PHP mail() function For Fun And RCE
0x03 防御方法
对于直接的mail()函数的问题,主要针对几个参数进行防御:
- to参数:除非特定场景,一般不让用户从外部输入;
- subject参数:安全使用;
- message参数:安全使用;
- headers参数:若用户可从外部输入,则过滤\r和\n字符,防御CRLF注入;
- parameters参数:尽量不使用用户输入,实在不行要按照业务场景进行严格过滤;
对于由mail()间接助攻引起的一系列问题,需按业务场景来规划:
- 如果非必需,直接disable_functions禁用掉;
- 如果业务需要,不用或严格过滤第五个参数,起到点缓解的作用;