PHP SECURITY CALENDAR 2017 (Day 5 - 8)

 源码是这样的

 1 class Mailer {
 2   private function sanitize($email) {
 3     if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
 4       return '';
 5     }
 6 
 7     return escapeshellarg($email);
 8   }
 9 
10   public function send($data) {
11     if (!isset($data['to'])) {
12       $data['to'] = '[email protected]';
13     } else {
14       $data['to'] = $this->sanitize($data['to']);
15     }
16 
17     if (!isset($data['from'])) {
18       $data['from'] = '[email protected]';
19     } else {
20       $data['from'] = $this->sanitize($data['from']);
21     }
22 
23     if (!isset($data['subject'])) {
24       $data['subject'] = 'No Subject';
25     }
26 
27     if (!isset($data['message'])) {
28       $data['message'] = '';
29     }
30 
31     mail($data['to'], $data['subject'], $data['message'],
32       '', "-f" . $data['from']);
33   }
34 }
35 
36 $mailer = new Mailer();
37 $mailer->send($_POST);

 

这题主要考察由 php 内置函数 mail 所引发的命令执行漏洞。

php 自带的 mail 函数的用法:

bool mail (
    string $to ,                      //指定邮件接收者,即接收人
    string $subject ,                 //邮件的标题
    string $message [,                //邮件的正文内容
    string $additional_headers [,     //指定邮件发送时其他的额外头部,如发送者From,抄送CC,隐藏抄送BCC
    string $additional_parameters ]]  //指定传递给发送程序sendmail的额外参数
)

 

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

-O option = value
QueueDirectory = queuedir 选择队列消息

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

-f from email
这个参数可以让我们指定我们发送邮件的邮箱地址

 

举个例子理解就是,这段代码使用 -X 参数指定日志文件,最终会在 /var/www/html/rce.php 中写入木马

php
    $to = '[email protected]';
    $subject = 'Hello Alice!';
    $message = '';
    $headers = "CC:[email protected]";
    $options = '-OQueueDirectory=/tmp -X /var/www/html/rce.php';
    mail($to, $subject, $headers, $options);
?>
17220 <<< To: Alice@example.com
17220 <<< Subject: Hello Alice!
17220 <<< X-PHP-Originating-Script: 0:test.php
17220 <<< CC: somebodyelse@example.com
17220 <<<
17220 <<< phpinfo(); ?>
17220 <<< [EOF]

 

源码第三行中有这样一行代码,它的主要作用是确保只使用有效的电子邮件地址 $email 

filter_var 使用特定的过滤器过滤一个变量

filter_var($email, FILTER_VALIDATE_EMAIL)

filter_var() 问题在于,在双引号中嵌套转义空格仍然能够通过检测。同时由于底层正则表达式的原因,我们通过重叠单引号和双引号,欺骗 filter_val() 使其认为我们仍然在双引号中,这样就可以绕过检测,但是由于引入的特殊符号,虽然绕过了 filter_var() 针对邮箱的检测,但是由于PHP的 mail() 函数在底层实现中,调用了 escapeshellcmd() 函数,对用户输入的邮箱地址进行检测,导致即使存在特殊符号,也会被 escapeshellcmd() 函数处理转义,这样就没办法达到命令执行的目的了

 

继续往下看,第七行的代码,它的目的主要是处理 $email 传入的数据

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

return escapeshellarg($email);

escapeshellarg 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号

 

因为 php 的 mail() 函数在底层调用了 escapeshellcmd() 函数对用户输入的邮箱地址进行处理,即使我们使用带有特殊字符的payload,绕过 filter_var() 的检测,但还是会被 escapeshellcmd() 处理。然而 escapeshellcmd() 和 escapeshellarg 一起使用,会造成特殊字符逃逸,举个例子

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

分析一下这个过程

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 分割为三个部分

所以这个payload可以简化为  curl 127.0.0.1\ -v -d a=1',即向 127.0.0.1\ 发起请求,POST 数据为 a=1'

 

总结来说,这题实际上是考察绕过 filter_var() 函数的邮件名检测,通过 mail 函数底层实现中调用的 escapeshellcmd() 函数处理字符串,再结合 escapeshellarg() 函数,最终实现参数逃逸,导致远程代码执行,这道题的知识点出过一道 ctf 题,我刚好写过题解 https://www.cnblogs.com/wkzb/p/12286438.html

 

(*)修复

避免同时使用 escapeshellcmd() 和 escapeshellarg() 函数对参数进行过滤

 

参考:

https://xz.aliyun.com/t/2501

 

源码是这样的

 1 class TokenStorage {
 2   public function performAction($action, $data) {
 3     switch ($action) {
 4       case 'create':
 5         $this->createToken($data);
 6         break;
 7       case 'delete':
 8         $this->clearToken($data);
 9         break;
10       default:
11         throw new Exception('Unknown action');
12     }
13   }
14 
15   public function createToken($seed) {
16     $token = md5($seed);
17     file_put_contents('/tmp/tokens/' . $token, '...data');
18   }
19 
20   public function clearToken($token) {
21     $file = preg_replace("/[^a-z.-_]/", "", $token);
22     unlink('/tmp/tokens/' . $file);
23   }
24 }
25 
26 $storage = new TokenStorage();
27 $storage->performAction($_GET['action'], $_GET['data']);

 

这题是由正则表达式不严谨导致的任意文件删除漏洞, 导致这一漏洞的原因在第21行, preg_replace 中的 pattern 部分 ,该正则表达式并未起到过滤目录路径字符的作用。[^a-z.-_] 表示匹配除了 a 字符到 z 字符、. 字符(46)到 _ 字符(95)之间的所有字符。因此,攻击者可以使用点和斜杠符号进行路径穿越,最终删除任意文件,例如使用 payload :action = delete&data = ../../ config.php,便可删除 config.php 文件

file_put_contents 将一个字符串写入文件

preg_replace 执行一个正则表达式的搜索和替换

unlink 删除文件

 

(*)修复

在传入的参数中过滤 ../ 等目录阶层字符,避免目录穿越

 

参考:

https://xz.aliyun.com/t/2523

 

 源代码是这样的

 1 function getUser($id) {
 2   global $config, $db;
 3   if (!is_resource($db)) {
 4     $db = new MySQLi(
 5       $config['dbhost'],
 6       $config['dbuser'],
 7       $config['dbpass'],
 8       $config['dbname']
 9     );
10   }
11   $sql = "SELECT username FROM users WHERE id = ?";
12   $stmt = $db->prepare($sql);
13   $stmt->bind_param('i', $id);
14   $stmt->bind_result($name);
15   $stmt->execute();
16   $stmt->fetch();
17   return $name;
18 }
19 
20 $var = parse_url($_SERVER['HTTP_REFERER']);
21 parse_str($var['query']);
22 $currentUser = getUser($id);
23 echo '

'.htmlspecialchars($currentUser).'

';

 

这题考察变量覆盖漏洞,⽽导致这⼀漏洞的发⽣则是不安全的使⽤ parse_str 函数。 由于第21行中的 parse_str() 调用,其行为非常类似于注册全局变量。我们通过提交类似 config[dbhost]=127.0.0.1 这样类型的数据,这样因此我们可以控制 getUser() 中第5到8行的全局变量 $config 。如果目标存在登陆验证的过程,那么我们就可以通过变量覆盖的方法,远程连接我们自己的mysql服务器,从而绕过这块的登陆验证,进而进行攻击,例如payload:http://host/?config[dbhost]=10.0.0.5&config[dbuser]=root&config[dbpass]=root&config[dbname]=malicious&id=1

is_resource 检测变量是否为资源类型 

parse_url 解析 URL,返回其组成部分

parse_str 解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否存在,所以会直接覆盖掉当前作用域中原有的变量

htmlspecialchars 将特殊字符转换为 HTML 实体

 

(*)修复

为了解决变量覆盖问题,可以在注册变量前先判断变量是否存在,如果使用 extract 函数可以配置第二个参数是 EXTR_SKIP 。使用 parse_str 函数之前先自行通过代码判断变量是否存在

extract 从数组中将变量导入到当前的符号表,检查每个键名看是否可以作为一个合法的变量名,同时也检查和符号表中已有的变量名的冲突(EXTR_SKIP如果有冲突,不覆盖已有的变量)

漏洞 demo

php
    $b=3;
    parse_str($_GET['test']);
    print_r($b); //关闭警告的前提下?test=b=2输出2
?>

修复 demo

php
    $b=3;
    if(isset($b)){
        echo '$b 已经set','
'; }else{ echo '$test 没有set','
'; parse_str($_GET['test']); } print_r($b); ?>

 

参考:

https://xz.aliyun.com/t/2541

 

 源码是这样的

 1 header("Content-Type: text/plain");
 2 
 3 function complexStrtolower($regex, $value) {
 4   return preg_replace(
 5     '/(' . $regex . ')/ei',
 6     'strtolower("\\1")',
 7     $value
 8   );
 9 }
10 
11 foreach ($_GET as $regex => $value) {
12   echo complexStrtolower($regex, $value) . "\n";
13 }

 

preg_replace 执行一个正则表达式的搜索和替换

preg_replace( mixed $pattern, mixed $replacement, mixed $subject[, int $limit = -1[, int &$count]] ) : mixed

搜索 subject 中匹配 pattern 的部分,以 replacement 进行替换

$pattern 存在 /e 模式修正符,允许代码执行

/e 模式修正符,是 preg_replace() 将 $replacement 当做 php 代码来执行

 

这道题目考察的是 preg_replace 函数使用 /e 模式,导致代码执行的问题。在代码第11行处,将 GET 请求方式传来的参数用在了 complexStrtolower 函数中,而变量 $regex 和 $value 又用在了存在代码执行模式的 preg_replace 函数中。所以,可以通过控制 preg_replace 函数第1个、第3个参数,来执行代码。但是可被当做代码执行的第2个参数,却固定为 'strtolower("\\1")' 。实际上,这里涉及到正则表达式反向引用的知识,即此处的 \\1(上面的命令执行,相当于 eval('strtolower("\\1");') 结果,当中的 \\1 实际上就是 \1 ,而 \1 在正则表达式中有自己的含义,即反向引用)

 

反向引用:对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数

 

在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)

 

所以这道题要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这叫做PHP的可变变量,所以 payload : \S*=${phpinfo()}

(\S非空白符,空白符包括空格、换行、tab缩进等;*是贪婪模式,会尽可能匹配更多的字符)

 

(*)修复

避免在 preg_replace() 中使用 /e 模式修正符

 

参考:

https://xz.aliyun.com/t/2557

https://xz.aliyun.com/t/2577

 

你可能感兴趣的:(PHP SECURITY CALENDAR 2017 (Day 5 - 8))