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

PHP-Audit-Labs

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);//要转换成 integer 的数量值,类型转换
}

$queryParts = array_map(function ($key, $value) {//array_map() 函数将用户自定义函数作用到数组中的每个值上
return $key . '=' . $value;
}, array_keys($sanitized), array_values($sanitized));

$query = implode('&', $queryParts);//根据 & 这个符号进行分割,然后拼接到 第14行 的 echo 语句中

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

?>

漏洞解析

根据题目意思,这里考察的应该是个 xss漏洞 , 漏洞触发点应该在代码中的 第13-14行 。这两行代码的作用是直接输出一个html的 <a> 标签。代码中的 第3-5行foreach循环$_GET 传入的参数进行了处理,但是这里有个问题。我们看下 第四行 的代码,这行代码针对 $value 进行类型转换,强制变成int类型。但是这部分代码只处理了 $value 变量,没针对 $key 变量进行处理。经过了 第3-5行 的代码处理之后,根据 & 这个符号进行分割,然后拼接到 第13行echo 语句中,在输出的时候又进行了一次 htmlentities 函数处理。 htmlentities 函数主要是会对一些特殊符号进行HTML实体编码。具体定义如下:

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

1
2
> 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 中有用的字符实体表

img

经过上面的分析,我们再回到题目,想想如何构造一下攻击 payload 。我们先梳理一些已知信息:

  • 这里的 $query 参数可控
  • htmlentities 函数在这里可逃逸单引号
  • xss的漏洞触发点在 <a> 标签。

<a> 中,我们可以通过 javascript 事件来执行js代码,例如: onclick 这类事件,因此最后的poc构造如下:

1
/?a'onclick%3dalert(1)%2f%2f=c

实例分析

本次实例分析选择 DM企业建站系统 v201710 中的 sql注入漏洞 来进行分析。首先,我们可以从cnvd上面看到一些相关信息,如下:

img

从漏洞通告中可以发现一些有用的信息,漏洞位置在登陆处,搭建的时候提示后台登陆口位置在 admindm-yourname/g.php 文件中,打开这个文件,发现重定向到 admindm-yournamemod_common/login.php 文件中,所以漏洞触发点应该就在这个文件中。

img

打开 admindm-yournamemod_common/login.php 这个文件,一眼就看到漏洞位置,截取部分相关代码如下:

img

第15行 很明显存在sql注入漏洞,通过拼接的方式直接插入到select语句中。 第15行 中的 $user 变量是通过 POST 方式提交上来,其值可控。但是上图的 第3行 代码调用 htmlentitiesdm 函数,对 POST 数据进行了处理,我们跟进这个 htmlentitiesdm 函数。该函数位置在 component/dm-config/global.common.php 文件中,截取关键代码如下:

img

这个函数是调用 htmlentities 函数针对输入的数据进行处理。前面我们已经介绍过了这个函数的用法,这里这个函数的可选参数是 ENT_NOQUOTES ,也就是说两种引号都不转换。下面我们来看个小例子:

img

这里我猜测开发者应该是考虑到了xss的问题,但是由于 htmlentities 这个函数选择的参数出现了偏差,导致这里我们可以引入单引号造成注入的问题。

我们看看最新版是怎么修复,使用 beyond compare 对比两个版本代码的差别。

img

新版修复的时候将可选参数修改为 ENT_QUOTES ,这个参数的作用就是过滤单引号加双引号,我们来看看下面这个例子,就很容易明白了这个参数的作用了。

img

漏洞验证

这里因为没有回显,所以是盲注,下面是验证截图:

img

漏洞修复

针对 htmlentities 这个函数,我们建议大家在使用的时候,尽量加上可选参数,并且选择 ENT_QUOTES 参数。

img

我们看看对比的效果

img

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
38
39
<?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>";
}

?>

sql.sql

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注入,但是这里有两个考察点,我们分别来看一下。

第一部分

第23行第24行 针对 GET 方式获取到的 usernamepassword 进行了处理,处理函数为 clean 。该函数在 第16-20行 处定义,函数的主要功能就是使用 htmlentities 函数处理变量中带有的特殊字符,而这里加入了 htmlentities 函数的可选参数 ENT_QUOTES ,因此这里会对 单引号双引号 等特殊字符进行转义处理。由于这里的注入是字符型的,需要闭合单引号或者逃逸单引号,因此这里需要绕过这个函数。我们可以通过下面这个例子观察 clean 函数的处理效果:

img

题目 第36行 是进入数据库查询,并且返回 name 列字段的值。而这里的sql语句是这样的:

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

那我们如果输入的 usernameadminpasswordadmin ,自然就构成了正常要执行的sql语句。

img

这道题的问题就在于可以引入反斜杠,也就是转义符,官方针对 转义符 是这么解释的。

比如,如果你希望匹配一个 "*" 字符,就需要在模式中写为 \*。 这适用于一个字符在不进行转义会有特殊含义的情况下。

这里我们看个简单的例子理解一下这个转义符号。

img

转义符号会让当前的特殊符号失去它的作用,这道题由于可以引入反斜杠,也就是转义符号,来让

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

username后面的 ' 失效,只要这个 ' 失效,就能闭合pass=后面的 '。最后组合的payload就如下图所示

img

所以实际上目前 name 的值是 admin' AND pass= ,这时候 password 的值是一个可控的输入点,我们可以通过这个值来构造 sql联合查询 ,并且注释掉最后的 单引号

img

最后我们看看在mysql中执行的结果。

img

第二部分

好了第一部分我们其实已经成功构造好了payload,但是回头来看看题目,题目 第6行第16行 有两个正则表达式,作用就是如果参数中带有 or、and 、union 等数据,就退出,并输出 Attack detected!!!

img

这里当然我们可以正面硬刚这个正则表达式。但是这里我们来聊一个比较有趣的解法。

我们看到是通过 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 该变量的值。

img

我们举个简单的例子方便大家理解:

img

我们可以看到这里的 post 方式传入的数据覆盖了 get 方式传入的数据,因此这里最后的payload如下:

img

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