S1xHcL's Blog.

PHP-Audit Day12 误用htmlentities函数引发的漏洞

Word count: 1.7kReading time: 8 min
2022/10/18 Share

函数学习

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

1
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:两种引号都不转换。

Demo学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$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>";

?>

看到结尾的响应标签内容猜测这题考察的可能是XSS,这里一共有两处过滤点 intvalhtmlentities

在第4~6行处,使用 foreach() 函数对 GET 请求的数据进行处理,但第5行的 intval() 函数只过滤了 $value ,未过滤 $key

第15行 htmlentities() 函数作用是将字符串转化为HTML实体,但是默认不对单引号进行转义

1
'onclick%3dalert(1)//=1

利用的是a标签的onclick事件来进行XSS攻击,F12查看闭合后源码

1
<a href="/images/size.php?" onclick="alert(1)//=1'">link</a>

案例分析

案例分析选择 DM企业建站系统 v201710 中的 sql注入漏洞 来进行分析,在 CNVD 中查看登录处存在SQL注入。找到漏洞入口文件 dm/admindm-yourname/g.php

打开文件后提示重定向到 dm/admindm-yourname/mod_common/login.php ,漏洞点位于第 78 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$act= @htmlentitiesdm($_GET['act']);
if($act=='login'){

$user= @htmlentitiesdm(trim($_POST['user']));
$ps= @htmlentitiesdm(trim($_POST['password']));


if(strlen($user)<2 or strlen($ps)<2){
alert('字符不够 sorry,user need more long'); jump($jumpv);
}

require_once WEB_ROOT.'component/dm-config/mysql.php';
// $salt = '00';is in config.php
$pscrypt= crypt($ps, $salt);
//echo $pscrypt;
$ss_P="select * from ".TABLE_USER." where email='$user' and ps='$pscrypt' order by id desc limit 1";
// echo $ss_P;exit;
if(getnum($ss_P)>0){
$row=getrow($ss_P);
$userid=$row['id'];

第17行很明显存在sql注入,未做任何过滤,直接将 $user 拼接进sql语句中,回溯 $user 变量,在第 5 行通过POST提交后使用 htmlentitiesdm() 函数进行过滤,数据可控。跟进该函数,文件位于 dm/component/dm-config/global.common.php 中421~423

1
2
3
function htmlentitiesdm($v){   
return htmlentities(trim($v),ENT_NOQUOTES,"utf-8");
}//end func

htmlentities() 参数选择使用 ENT_NOQUOTES ,即两种引号都不转换。所以payload闭合单引号直接使用延迟注入进行探测。

漏洞复现

案例学习

源码

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
// index.php
<?php
require 'db.inc.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);
}
return htmlentities($str, ENT_QUOTES);
}

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


$query='SELECT * FROM ctf.users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';

#echo $query;

$result=mysql_query($query);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}

?>
1
2
3
4
5
6
7
8
// db.inc.php
<?php
$mysql_server_name="localhost";
$mysql_database="Day12"; /** 数据库的名称 */
$mysql_username="Hongri"; /** MySQL数据库用户名 */
$mysql_password="Hongri"; /** MySQL数据库密码 */
$conn = mysql_connect($mysql_server_name, $mysql_username,$mysql_password,'utf-8');
?>
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
# Host: localhost  (Version: 5.5.53)
# Date: 2018-08-05 12:55:29
# Generator: MySQL-Front 5.3 (Build 4.234)

/*!40101 SET NAMES utf8 */;

#
# Structure for table "users"
#

DROP TABLE IF EXISTS `users`;
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;

#
# Data for table "users"
#

/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES (1,'admin','qwer!@#zxca','hrctf{sql_Inject1on_Is_1nterEst1ng}');
/*!40000 ALTER TABLE `users` ENABLE KEYS */;

分析

通过第 27 行可以看到本题为 sql注入,但存在两个限制条件

限制1

1
2
3
4
5
6
7
8
9
10
11
12
function clean($str){
if(get_magic_quotes_gpc()){
$str=stripslashes($str);
}
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.'\';';

usernamepassword 参数通过 GET 方式获取,使用 clean() 对获取到的值进行处理,在第 5 行中使用 htmlentities() 转义特殊字符,过滤规则使用 ENT_QUOTES 模式对单引号和双引号等进行转义。但这里并没有限制使用反斜杠 \ ,即转义符,通过传入转义符使 sql 语句中的单引号失效

1
SELECT * FROM day12.users WHERE name='$username' AND pass='$password';

转义符使 username 后的单引号失效,从而闭合 $password 前的单引号,那么 $password 可跳出单引号限制,并且 $password 参数可控,最后再插入注释符注释掉语句中最后面的单引号

限制2

index.php 文件中第 4~14 行处

1
2
3
4
5
6
7
8
9
10
11
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!!!");
}
}

系统最开始会判断 $usernamepassowrd ,通过正则过滤参数中是否存在关键词,如果存在就退出并且输出 Attack detected!!!

可以看到是通过 $_REQUEST() 传入数据,而php中 REQUEST 变量默认情况下包含了 GETPOSTCOOKIE 的数组。在 php.ini 配置文件中,有一个参数 variables_order ,这参数有以下可选项目

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

这些字母分别对应的是 E: EnvironmentG:GetP:PostC:CookieS:Server。这些字母的出现顺序,表明了数据的加载顺序。而 php.ini 中这个参数默认的配置是 GPCS ,也就是说如果以 POSTGET 方式传入相同的变量,那么用 REQUEST 获取该变量的值将为 POST 该变量的值

最终payload为

1
2
3
4
5
6
POST /php/day12/index.php?username=\&password=%20union%20select%201,flag,3,4%20from%20day12.users%23 HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 29

username=admin&password=admin

通过调试可以看到参数值被覆盖

CATALOG
  1. 1. 函数学习
  2. 2. Demo学习
  3. 3. 案例分析
    1. 3.1. 漏洞复现
  4. 4. 案例学习
    1. 4.1. 源码
    2. 4.2. 分析
      1. 4.2.1. 限制1
      2. 4.2.2. 限制2