【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】

开始练习【红日团队】的PHP-Audit-Labs 代码审计 Day5
链接:https://github.com/hongriSec/PHP-Audit-Labs
感兴趣的同学可以去练习练习
预备知识:
内容题目均来自 PHP SECURITY CALENDAR 2017
Day 5 - Postcard代码如下:

class Mailer {
     
  private function sanitize($email) {
     
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
     
      return '';
    }

    return escapeshellarg($email);
  }

  public function send($data) {
     
    if (!isset($data['to'])) {
     
      $data['to'] = '[email protected]';
    } else {
     
      $data['to'] = $this->sanitize($data['to']);
    }

    if (!isset($data['from'])) {
     
      $data['from'] = '[email protected]';
    } else {
     
      $data['from'] = $this->sanitize($data['from']);
    }

    if (!isset($data['subject'])) {
     
      $data['subject'] = 'No Subject';
    }

    if (!isset($data['message'])) {
     
      $data['message'] = '';
    }

    mail($data['to'], $data['subject'], $data['message'],
      '', "-f" . $data['from']);
  }
}

$mailer = new Mailer();
$mailer->send($_POST);

漏洞解析 :
这道题其实是考察由 php 内置函数 mail 所引发的命令执行漏洞。我们先看看 php 自带的 mail 函数的用法:

mail () 函数的定义:

(PHP 4, PHP 5, PHP 7)

功能:

PHP mail() 函数用于从脚本中发送电子邮件。

定义:
mail(to,subject,message,headers,parameters)

注释:PHP 运行邮件函数需要一个已安装且正在运行的邮件系统(如:sendmail、postfix、qmail等)。所用的程序通过在 php.ini 文件中的配置设置进行定义。请在我们的 PHP Mail 参考手册 阅读更多内容。

说明:
参数 描述
to 必需。规定 email 接收者。
subject 必需。规定 email 的主题。注释:该参数不能包含任何新行字符。
message 必需。定义要发送的消息。应使用 LF (\n) 来分隔各行。每行应该限制在 70 个字符内。
headers 可选。规定附加的标题,比如 From、Cc 和 Bcc。应当使用 CRLF (\r\n) 分隔附加的标题。
parameters 可选。对邮件发送程序规定额外的参数。

在Linux系统上, php 的 mail 函数在底层中已经写好了,默认调用 Linux 的 sendmail 程序发送邮件。而在额外参数( parameters )中, sendmail 主要支持的选项有以下三种:

  • -O option = value

    QueueDirectory = queuedir 选择队列消息

  • -X logfile

    这个参数可以指定一个目录来记录发送邮件时的详细日志情况。

  • -f from email

    这个参数可以让我们指定我们发送邮件的邮箱地址。

范例

PHP 简易 E-Mail

通过 PHP 发送电子邮件的最简单的方式是发送一封文本 email。

在下面的实例中,我们首先声明变量($to, $subject, $message, $from, $headers),然后我们在 mail()函数中使用这些变量来发送了一封 E-mail:


$to = "[email protected]";         // 邮件接收者
$subject = "参数邮件";                // 邮件标题
$message = "Hello! 这是邮件的内容。";  // 邮件正文
$from = "[email protected]";   // 邮件发送者
$headers = "From:" . $from;         // 头部信息设置
mail($to,$subject,$message,$headers);
echo "邮件已发送";
?>

当然这题如果只是这一个问题的话,会显的太简单了,我们继续往下看,在 第3行有这样一串代码:

    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
     

这串代码的主要作用,是确保在第5个参数中只使用有效的电子邮件地址 $email 。我们先了解一下 filter_var() 函数的定义:

filter_var() 函数定义:

功能:

filter_var() 函数通过指定的过滤器过滤一个变量。
如果成功,则返回被过滤的数据。如果失败,则返回 FALSE

说明:

mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )

参数 描述
variable 必需。规定要过滤的变量。
filter 可选。规定要使用的过滤器的 ID。默认是 FILTER_SANITIZE_STRING。参见 完整的 PHP Filter 参考手册,查看可能的过滤器。过滤器 ID 可以是 ID 名称(比如 FILTER_VALIDATE_EMAIL)或 ID 号(比如 274)。
options 可选。规定一个包含标志/选项的关联数组或者一个单一的标志/选项。检查每个过滤器可能的标志和选项。

【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第1张图片
这里主要是根据第二个参数filter过滤一些想要过滤的东西。
关于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() 使其认为我们仍然在双引号中,这样我们就可以绕过检测。下面举个简单的例子,方便理解


var_dump(filter_var('\'is."\'\ not\ 1"@admin.com',FILTER_VALIDATE_EMAIL));
var_dump(filter_var('"is.\ not\ 2"@admin.com',FILTER_VALIDATE_EMAIL));
var_dump(filter_var('"is.""\ not\ 3"@admin.com',FILTER_VALIDATE_EMAIL));
?>

【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第2张图片
当然由于引入的特殊符号,虽然绕过了 filter_var()针对邮箱的检测,但是由于PHP的 mail()函数在底层实现中,调用了 escapeshellcmd() 函数,对用户输入的邮箱地址进行检测,导致即使存在特殊符号,也会被 escapeshellcmd()函数处理转义,这样就没办法达到命令执行的目的了。 escapeshellcmd() 函数在底层代码如下(详细点 这里 (167-177行)):

	if (force_extra_parameters) {
     
		extra_cmd = php_escape_shell_cmd(force_extra_parameters);
	} else if (extra_cmd) {
     
		extra_cmd = php_escape_shell_cmd(extra_cmd);
	}

	if (php_mail(to_r, subject_r, message, headers_trimmed, extra_cmd TSRMLS_CC)) {
     
		RETVAL_TRUE;
	} else {
     
		RETVAL_FALSE;
	}

我们继续分析题目代码第七行

    return escapeshellarg($email);

这句代码主要是处理$email 传入的数据。

escapeshellarg 函数的定义:

(PHP 4 >= 4.0.3, PHP 5, PHP 7)

功能:

escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数

说明:
escapeshellarg ( string $arg ) : string

escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含 exec(), system() 执行运算符

参数 描述
arg 需要被转码的参数。

返回值:

转换之后字符串。

具体功能作用,可以参考如下案例:


var_dump(escapeshellarg("1234"));
var_dump(escapeshellarg("1'2'3"));
var_dump(escapeshellarg("12'  3"));
?>

【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第3张图片
那我们前面说过了PHP的mail()函数在底层调用了 escapeshellcmd()函数对用户输入的邮箱地址进行处理,即使我们使用带有特殊字符的payload,绕过 filter_var() 的检测,但还是会被 escapeshellcmd() 处理。然而 escapeshellcmd()escapeshellarg 一起使用,会造成特殊字符逃逸,下面我们给个简单例子理解一下:


$param="127.0.0.1' -v -d a=1";
$a=escapeshellarg($param);
$b=escapeshellcmd($a);
$cmd="curl ".$b;
var_dump($a)."\n";
var_dump($b)."\n";
var_dump($cmd)."\n";
system($cmd);
?>

【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第4张图片
详细分析一下这个过程:

  1. 传入的参数是

    127.0.0.1' -v -d a=1
    
  2. 由于escapeshellarg先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。所以处理之后的效果如下:

    '127.0.0.1'\'' -v -d a=1'
    
  3. 接着 escapeshellcmd 函数对第二步处理后字符串中的 \ 以及 a=1' 中的单引号进行转义处理,结果如下所示:

    '127.0.0.1'\\'' -v -d a=1\'
    
  4. 由于第三步处理之后的payload中的 \\ 被解释成了 \ 而不再是转义字符,所以单引号配对连接之后将payload分割为三个部分,具体如下所示:
    【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第5张图片
    所以这个payload可以简化为 curl 127.0.0.1\ -v -d a=1' ,即向 127.0.0.1\ 发起请求,POST 数据为 a=1'

总结一下,这题实际上是考察绕过 filter_var() 函数的邮件名检测,通过 mail 函数底层实现中调用的escapeshellcmd() 函数处理字符串,再结合 escapeshellarg() 函数,最终实现参数逃逸,导致 远程代码执行 。

实例分析:

这里实例分析选择 PHPMailer 命令执行漏洞CVE-2016-10045 和 CVE-2016-10033 )。项目代码可以通过以下方式下载:

git clone https://github.com/PHPMailer/PHPMailer
cd PHPMailer
git checkout -b CVE-2016-10033 v5.2.17

漏洞POC 本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!

漏洞分析:

  • CVE-2016-10033

在github上直接diff一下,对比一下不同版本的 class.phpmailer.php 文件,差异如下:
【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第6张图片
这里在 sendmailSend 函数中加了 validateAddress函数,来针对发送的数据进行判断,判断邮箱地址的合法性。另外针对传入的数据,调用了 escapeshellarg函数来转义特殊符号,防止注入参数。然而这样做,就引入了我们上面讨论的问题,即同时使用 escapeshellarg函数和 escapeshellcmd()函数,导致单引号逃逸。由于程序没有对传命令参数的地方进行转义,所以我们可以结合 mail函数的第五个参数-X 写入 webshell

下面详细看一下代码,漏洞具体位置在 class.phpmailer.php 中,我们截取部分相关代码如下 :
【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第7张图片
在上图第12行处没有对 $params变量进行严格过滤,只是简单地判断是否为 null,所以可以直接传入命令。我们继续往下看,我们发现在上图第12行,当 safe_mode 模式处于关闭状态时, mail()函数才会传入$params变量。

进一步跟跟进 $params参数,看看它是怎么来的。这个参数的位置在 class.phpmailer.php中,我们截取部分相关代码,具体看下图 第11行
【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第8张图片
很明显$params 是从 $this->Sender传进来的,我们找一下$this->Sender,发现这个函数在 class.phpmailer.php中,截取部分相关代码,具体看下图 第10行

【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第9张图片
这里在 setFrom 函数中将$address 经过某些处理之后赋值给$this->Sender。我们详细看看 $address变量是如何处理的。主要处理函数均在class.phpmailer.php 文件中,我们截取了部分相关代码,在下图 第三行 中使用了 validateAddress来处理 $address变量。
【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第10张图片
所以跟进一下 validateAddress函数,这个函数位置在class.phpmailer.php 文件中。我们看看程序流程,相关代码如下:
【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第11张图片
分析一下这段代码,大概意思就是对环境进行了判断,如果没有prce并且 php 版本 <5.2.0 ,则 $patternselect = 'noregex'。接着往下看,在 class.phpmailer.php文件中,有部分关于 $patternselectswich操作,我只选择了我们需要的那个,跟踪到下面的noregex
【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第12张图片
这里简单的只是根据@符号来处理字符,所以这里的payload很简单。

a( -OQueueDirectory=/tmp -X/var/www/html/x.php )@a.com

然后通过linux自身的sendmaillog的方式,把log写到web根目录下。将日志文件后缀定义为.php ,即可成功写入webshell

  • CVE-2016-10045

diff一下5.2.205.2.18发现针对 escapeshellcmdescapeshellarg做了改动。
这里其实有个很奇妙的漏洞,针对用户输入使用 escapeshellarg函数进行处理。所以,在最新版本中使用之前的 payload 进行攻击会失败,例如:

a( -OQueueDirectory=/tmp -X/var/www/html/x.php )@a.com

但是,却可以使用下面这个 payload 进行攻击:

a'( -OQueueDirectory=/tmp -X/var/www/html/x.php )@a.com

实际上,可用于攻击的代码只是在之前的基础上多了一个单引号。之所以这次的攻击代码能够成功,是因为修复代码多了 escapeshellcmd函数,结合上 mail()函数底层调用的 escapeshellarg 函数,最终导致单引号逃逸。
【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第13张图片
我们的 payload 最终在执行时变成了

'-fa'\\''\( -OQueueDirectory=/tmp -X/var/www/html/test.php \)@a.com\'

按照刚才上面的分析,我们将payload化简分割一下就是-fa\(、-OQueueDirectory=/tmp、-X/var/www/html/test.php、)@a.com',这四个部分。最终的参数就是这样被注入的。

漏洞利用:

漏洞有一些基本要求:

  1. php version < 5.2.0 2
  2. phpmailer < 5.2.18 3
  3. php 没有安装 pcre(no default)
  4. safe_mode = false(default)

存在正则绕过之后,以及 escapeshellargescapeshellcmd一起使用造成的神奇现象之后。
只需要 phpmailer < 5.2.20

环境,poc,exp相关

这里用docker复现:

git clone https://github.com/opsxcq/exploit-CVE-2016-10033.git      //下载文件
cd exploit-CVE-2016-10033              													//进入文件
docker run --rm -it -p 8080:80 vulnerables/cve-2016-10033       		//部署环境

这里报错我以为一直有问题,环境安装不了,结果直接新开一个终端看,发现是正常运行的。

在这里插入图片描述
进入页面:
【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第14张图片
直接利用下载好的exp

./exploit.sh 127.0.0.1:8080


执行完成。

修复建议:

我们来看一下PHPMailer官方给出的修复代码。官方对用户传入的参数进行检测,如果当中存在被转义的字符,则不传递-f参数(-f 参数表示发邮件的人,如果不传递该参数,我们的payload就不会被带入 mail 函数,也就不会造成命令执行),所以不建议大家同时使用 escapeshellcmd()escapeshellarg() 函数对参数进行过滤,具体修复代码如下:

【漏洞练习-Day5】PHPMailer 命令执行漏洞 【CVE-2016-10045 和 CVE-2016-10033】_第15张图片

结语

再次感谢【红日团队】

你可能感兴趣的:(技术)