开始练习【红日团队】的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 函数的用法:
(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()
函数通过指定的过滤器过滤一个变量。
如果成功,则返回被过滤的数据。如果失败,则返回 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 | 可选。规定一个包含标志/选项的关联数组或者一个单一的标志/选项。检查每个过滤器可能的标志和选项。 |
这里主要是根据第二个参数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));
?>
当然由于引入的特殊符号,虽然绕过了 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
传入的数据。
(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"));
?>
那我们前面说过了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);
?>
传入的参数是
127.0.0.1' -v -d a=1
由于escapeshellarg
先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。所以处理之后的效果如下:
'127.0.0.1'\'' -v -d a=1'
接着 escapeshellcmd
函数对第二步处理后字符串中的 \
以及 a=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()
函数的邮件名检测,通过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
在github上直接diff一下,对比一下不同版本的 class.phpmailer.php 文件,差异如下:
这里在 sendmailSend
函数中加了 validateAddress
函数,来针对发送的数据进行判断,判断邮箱地址的合法性。另外针对传入的数据,调用了 escapeshellarg
函数来转义特殊符号,防止注入参数。然而这样做,就引入了我们上面讨论的问题,即同时使用 escapeshellarg
函数和 escapeshellcmd()
函数,导致单引号逃逸。由于程序没有对传命令参数的地方进行转义,所以我们可以结合 mail
函数的第五个参数-X 写入 webshell
。
下面详细看一下代码,漏洞具体位置在 class.phpmailer.php
中,我们截取部分相关代码如下 :
在上图第12行
处没有对 $params
变量进行严格过滤,只是简单地判断是否为 null
,所以可以直接传入命令。我们继续往下看,我们发现在上图第12行
,当 safe_mode
模式处于关闭状态时, mail()
函数才会传入$params
变量。
进一步跟跟进 $params
参数,看看它是怎么来的。这个参数的位置在 class.phpmailer.php
中,我们截取部分相关代码,具体看下图 第11行
:
很明显$params
是从 $this->Sender
传进来的,我们找一下$this->Sender
,发现这个函数在 class.phpmailer.php
中,截取部分相关代码,具体看下图 第10行
:
这里在 setFrom
函数中将$address
经过某些处理之后赋值给$this->Sender
。我们详细看看 $address
变量是如何处理的。主要处理函数均在class.phpmailer.php
文件中,我们截取了部分相关代码,在下图 第三行 中使用了 validateAddress
来处理 $address
变量。
所以跟进一下 validateAddress
函数,这个函数位置在class.phpmailer.php
文件中。我们看看程序流程,相关代码如下:
分析一下这段代码,大概意思就是对环境进行了判断,如果没有prce
并且 php 版本 <5.2.0
,则 $patternselect = 'noregex'
。接着往下看,在 class.phpmailer.php
文件中,有部分关于 $patternselect
的 swich
操作,我只选择了我们需要的那个,跟踪到下面的noregex
。
这里简单的只是根据@
符号来处理字符,所以这里的payload
很简单。
a( -OQueueDirectory=/tmp -X/var/www/html/x.php )@a.com
然后通过linux
自身的sendmail
写log
的方式,把log
写到web
根目录下。将日志文件后缀定义为.php
,即可成功写入webshell
。
diff一下5.2.20
和5.2.18
发现针对 escapeshellcmd
和 escapeshellarg
做了改动。
这里其实有个很奇妙的漏洞,针对用户输入使用 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
函数,最终导致单引号逃逸。
我们的 payload
最终在执行时变成了
'-fa'\\''\( -OQueueDirectory=/tmp -X/var/www/html/test.php \)@a.com\'
按照刚才上面的分析,我们将payload
化简分割一下就是-fa\(、-OQueueDirectory=/tmp、-X/var/www/html/test.php、)@a.com'
,这四个部分。最终的参数就是这样被注入的。
漏洞有一些基本要求:
- php version < 5.2.0 2
- phpmailer < 5.2.18 3
- php 没有安装 pcre(no default)
- safe_mode = false(default)
存在正则绕过之后,以及 escapeshellarg
和 escapeshellcmd
一起使用造成的神奇现象之后。
只需要 phpmailer < 5.2.20
环境,poc,exp相关
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 //部署环境
这里报错我以为一直有问题,环境安装不了,结果直接新开一个终端看,发现是正常运行的。
进入页面:
直接利用下载好的exp
./exploit.sh 127.0.0.1:8080
执行完成。
我们来看一下PHPMailer
官方给出的修复代码。官方对用户传入的参数进行检测,如果当中存在被转义的字符,则不传递-f
参数(-f 参数表示发邮件的人,如果不传递该参数,我们的payload就不会被带入 mail 函数,也就不会造成命令执行
),所以不建议大家同时使用 escapeshellcmd()
和 escapeshellarg()
函数对参数进行过滤,具体修复代码如下:
再次感谢【红日团队】