request导致的安全性问题分析

说明

最近在看P神之前分析的漏洞的文章,本篇文章就是记录看了P神的贷齐乐系统最新版SQL注入之后的一点体会和收获。

本篇文章主要是要说明的是在PHP中由于对于$_REQUEST的认识不足或者是使用不当而导致的漏洞。目前漏洞类型主要是有两种,对$_REQUEST认识不足从而导致过滤失效,使用不当则指的存在HPP的漏洞从而导致全局WAF失效,这个漏洞也就是在P神文章中所说明的漏洞。

$_REQUEST使用不当绕过WAF

php手册上面对于$_REQUEST的说法是:

由于 $_REQUEST 中的变量通过 GET,POST 和 COOKIE 输入机制传递给脚本文件,因此可以被远程用户篡改而并不可信

这话是什么意思呢?表示的是$_REQUEST是直接从GET,POST 和 COOKIE中取值,不是他们的引用。即使后续GET,POST 和 COOKIE发生了变化,也不会影响$_REQUEST的结果。如下:

1
2
3
4
5
foreach ($_REQUEST as $key=>$value) {
$_REQUEST[$key] = md5($value);
}
var_dump($_REQUEST);
var_dump($_GET);

我们访问http://localhost/test/index.php?id=1&uname=spoock&email=1@163.com的结果如下:

可以看到$_REQUEST的结果发生了改变,但是$_GET的结果并没有改变。

漏洞代码

假设如果存在如下的代码:

1
2
3
4
5
6
7
8
9
foreach ($_REQUEST as $key=>$value) {
$_REQUEST[$key] = waf($value);
}
if(isset($_POST['submit'])) {
$id = $_POST['id'];
$sql = "select * from user where id=$id";
mysql_query($sql);
//....
}

虽然使用了waf进行过滤,但是waf过滤的是$_REQUEST,在业务代码中使用的是$_POST。这样就导致前面的WAF过滤没有任何的作用,防护完全失效。

这个特性在ripstech中的2017年的题目中就出现过。这种问题我相信一定会存在。

REQUEST导致的HPP漏洞

PHP中的特性

在说明这个漏洞之前,我们先需要了解一下在PHP中的几个小的特性。

  1. php自身在解析请求的时候,如果参数名字中包含空格、.[这几个字符,会将他们转换成_。测试如下:

  2. php在遇到相同参数时接受的是第二个参数。

  3. 通过$_SERVER['REQUEST_URI']方式获得的参数并不会进行转换。如:

基于以上三种PHP的特性,就可能会导致存在HPP漏洞,从而绕过系统防护。

漏洞代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
function dhtmlspecialchars($string) {
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = dhtmlspecialchars($val);
}
}
else {
$string = str_replace(array('&', '"', '<', '>', '(', ')'), array('&amp;', '&quot;', '&lt;', '&gt;', '(', ')'), $string);
if (strpos($string, '&amp;#') !== false) {
$string = preg_replace('/&amp;((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);
}
}
return $string;
}
function dowith_sql($str) {
$check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is', $str);
if ($check) {
echo "非法字符!";
exit();
}
return $str;
}
// 经过第一个waf处理
foreach ($_REQUEST as $key => $value) {
$_REQUEST[$key] = dowith_sql($value);
}
// 经过第二个WAF处理
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
var_dump($request_uri);
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));
}
}
}
// 业务处理
if (isset($_REQUEST['submit'])) {
$user_id = $_REQUEST['user_id'];
$sql = "select * from users where id=$user_id";
var_dump($sql);
}
  1. 第一个WAF,采用了dowith_sql()函数,如果$_REQUEST存在select|insert|update|delete等敏感关键字或者是字符,则直接exit()。如果不存在,则原字符串返回。
  2. 第二个WAF,通过$_SERVER['REQUEST_URI']得到请求参数,之后利用explode("&", $request_uri[1])得到每个参数,包括参数名和参数值。对每个参数值采用dhtmlspecialchars()过滤,对字符& " < > ( )都进行了替换。替换完毕之后重新得到_REQUEST
  3. 在最后的业务处理中,通过$_REQUEST获取参数进行处理。

如果能够利用HPP漏洞的原理,在WAF对参数进行过滤时处理的是一个参数,但是在进入到业务中处理的是第二个参数,那么我们就能够绕过WAF了。用一个简单的例子进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
// test.php
var_dump($_REQUEST);
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
$_REQUEST[$_value[0]] = addslashes($_value[1]);
}
}
}
var_dump($_REQUEST);

我们访问URL是:test.php?submit=&user_id=select&user.id=123。得到的结果如下:

第一次输出$_REQUEST仅仅只会输出user_id=123。因为php会将user.id替换为user_id,此时出现了两个user_id,而根据php的原则,则会选取第二个参数,所以第一次输出仅仅只会有user_id=123

第二次输出$_REQUEST会输出user_id=select&user.id=123是因为$_SERVER['REQUEST_URI']并不会对特殊的符号进行替换,因此所有的参数均不会变化,所有的参数都会原样输出。

按照以上的两种特性,我们利用参数污染的方法,是第一个WAF在处理时处理的是正常请求参数的URL,第二个WAF即使处理的是带有payload的参数,但是因为dhtmlspecialchars()的方法很容易被绕过,最终我们的payload就会进入到正常的业务请求中。用一个图来说明这个问题。

  1. 我们请求参数是user_id=payload&user.id=123
  2. 在经过第一个WAF时,由于$_REQUEST会将参数中的>替换为_,所以会得到两个user_id变为了user_id=payload&user_id=123。按照PHP的特性遇到相同的参数去第二个参数,所以第一个WAF取的是user_id=123,此时正常地通过第一个WAF。
  3. 进入到第二个WAF时,由于是通过$_SERVER['REQUEST_URI']取参数,user.id参数并不会被替换为user_id,所以两个参数都会第二个经过WAF
  4. 我们的payload绕过了第二个WAF之后(比较容易绕过),user_id=payload&user.id=123进入到业务请求中执行SQL语句,导致SQL注入。

漏洞利用

请求URL,http://localhost/test/index.php?submit=&user_id=1/**/union/**/select/**/1,2,3&user.id=123,得到的最后结果是:

小结

这个洞的挖掘包括利用其实很巧妙,利用的是hpp+php特性,来绕过CMS应用中变态的WAF。但是这个hpp的漏洞只是恰好在这个cms存在了,因为刚好使用了$_SERVER['REQUEST_URI']能够得到正常的请求,而$_REQUEST恰好会对特殊字符进行转义,这样就导致WAF处理的“正常”的参数,但是最后进入到业务数据中的确实我们的payload。
关于HPP的实际漏洞利用,可以参考HTTP参数污染

总结

以上的两个案例都和$_REQUEST的特性有关,对于开发者来说对于所使用的语言以及相关的函数都需要很了解否则就很容易出现漏洞。对于攻击者来说,同样需要了解一些函数的用法,参数、返回值等等,同时还需要掌握函数在一些特殊情况下的用法,这样就能够了解在开发者错误使用了函数时可能会存在的问题。