基础知识

字符、字符集与字符序

字符(character)是组成字符集(character set)的基本单位。对字符赋予一个数值(encoding)来确定这个字符在该字符集中的位置。

字符序(collation)指同一字符集内字符间的比较规则。

宽字节

GB2312、GBK、GB18030、BIG5、Shift_JIS等这些都是常说的宽字节,实际上只有两字节。宽字节带来的安全问题主要是吃ASCII字符(一字节)的现象。

GBK编码取值范围

GBK采用双字节表示,总体编码范围为8140-FEFE,首字节在81-FE 之间,尾字节在40-FE 之间

GB2313编码取值范围

gb2312编码的取值范围。它的高位范围是0xA1~0xF7,低位范围是0xA1~0xFE

MYSQL的字符集转换过程

  1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;

  2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集,其确定方法如下:

  • 使用每个数据字段的CHARACTER SET设定值;
  • 若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展,非SQL标准);
  • 若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
  • 若上述值不存在,则使用character_set_server设定值。

将操作结果从内部操作字符集转换为character_set_results。

重点:宽字节注入发生的位置就是PHP发送请求到MYSQL时字符集使用character_set_client设置值进行了一次编码。

addslashes()

addslashes() 函数返回在预定义字符之前添加反斜杠的字符串。

语法:

1
addslashes(string)

参数说明:

参数 描述
string 必需。规定要转义的字符串。

mysql_real_escape_string()

mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。
下列字符受影响:

1
2
3
4
5
6
7
8
\x00
\n
\r
\
'
"
\x1a
如果成功,则该函数返回被转义的字符串。如果失败,则返回 false。

语法:

1
mysql_real_escape_string(string,connection)

参数说明:

参数 描述
string 必需。规定要转义的字符串。
connection 可选。规定 MySQL 连接。如果未规定,则使用上一个连接。

incov()

(PHP 4 >= 4.0.5, PHP 5, PHP 7)
iconv — 字符串按要求的字符编码来转换

语法:

1
string iconv ( string $in_charset , string $out_charset , string $str )

将字符串 str 从 in_charset 转换编码到 out_charset。

参数说明:

参数 描述
in_charset 输入的字符集。
out_charset 输出的字符集。
str 要转换的字符串。

相关字符集设置

  • character_set_client:客户端发送过来的SQL语句编码,也就是PHP发送的SQL查询语句编码字符集。
  • character_set_connection:MySQL服务器接收客户端SQL查询语句后,在实施真正查询之前SQL查询语句编码字符集。
  • character_set_database:数据库缺省编码字符集。
  • character_set_filesystem:文件系统编码字符集。
  • character_set_results:SQL语句执行结果编码字符集。
  • character_set_server:服务器缺省编码字符集。
  • character_set_system:系统缺省编码字符集。
  • character_sets_dir:字符集存放目录,一般不要修改

宽字节注入原理

  GBK 占用两字节

  ASCII占用一字节

  PHP中编码为GBK,函数执行添加的是ASCII编码,MYSQL默认字符集是GBK等宽字节字符集。

  输入%df和函数执行添加的%5C,被合并成%df%5C。由于GBK是两字节,这个%df%5C被MYSQL识别为GBK。导致本应的%df\变成%df%5C。%df%5C在GBK编码中没有对应,所以被当成无效字符。

  %DF’ :会被PHP当中的addslashes函数转义为“%DF\’” ,“\”既URL里的“%5C”,那么也就是说,“%DF’”会被转成“%DF%5C%27”倘若网站的字符集是GBK,MYSQL使用的编码也是GBK的话,就会认为“%DF%5C%27”是一个宽字符。也就是“縗’”

MySQL中的宽字节注入

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
//连接数据库部分,注意使用了gbk编码
$conn = mysql_connect('localhost', 'root', 'uknow') or die('bad!');
mysql_query("SET NAMES 'gbk'");
mysql_select_db('test', $conn) OR emMsg("连接数据库失败,未找到您填写的数据库");
//执行sql语句
$id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
$sql = "SELECT * FROM news WHERE tid='{$id}'";
$result = mysql_query($sql, $conn) or die(mysql_error());
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="gbk" />
<title>新闻</title>
</head>
<body>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>

利用过程

SQL语句是SELECT * FROM news WHERE tid=’{$id}’,就是根据文章的id把文章从news表中取出来。

在这个sql语句前面,使用了一个addslashes函数,将$id的值转义。这是通常cms中对sql注入进行的操作,只要输入参数在单引号中,就逃逸不出单引号的限制,无法注入,


一般绕过addslashes的方式就是,想办法处理\’前面的\:

  • 1.想办法给\前面再加一个\(或单数个即可),变成\’,这样\被转义了,’逃出了限制
  • 2.想办法把\弄没有。
    宽字节注入是利用mysql的一个特性,mysql在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ascii码要大于128,才到汉字的范围)。如果输入%df’看会怎样:


其中的�\是一个汉字 我们可以改成其他的。根据gbk编码,第一个字节ascii码大于128,基本上就可以了。比如我们不用%df,用%dd也可以:

为什么从刚才到现在,只是在’也就是%27前面加了一个%df就报错了?而且从图中可以看到,报错的原因就是多了一个单引号,而单引号前面的反斜杠不见了。

这就是mysql的特性,因为gbk是多字节编码,他认为两个字节代表一个汉字,所以%df和后面的\也就是%5c变成了一个汉字“輁”,而’逃逸了出来。

因为两个字节代表一个汉字,所以我们可以试试%df%df%27:

不报错了。因为%df%df是一个汉字,%5c%27不是汉字,仍然是\’。

我们可以利用宽字节注入的特点进行手注,也可以在url后面加上%df’丢给sqlmap

1
http://localhost/1/0x01/index.php?id=1%df'

GB2312与GBK的不同

GB2312也是属于宽字节,那么使用GB2312连接数据库,看能否进行宽字节注入

结果就是不能注入了:

gb2312编码的取值范围。它的高位范围是0xA1~0xF7,低位范围是0xA1~0xFE,而\是0x5c,是不在低位范围中的。所以,0x5c根本不是gb2312中的编码,所以自然也是不会被吃掉的。

宽字符注入的修复

  • 将character_set_client设置为binary(二进制)
    只需在所有sql语句前指定一下连接的形式是二进制:
    1
    SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary

当mysql接受到客户端的数据后,会认为他的编码是character_set_client,然后会将之将换成character_set_connection的编码,然后进入具体表和字段后,再转换成字段对应的编码。

然后,当查询结果产生后,会从表和字段的编码,转换成character_set_results编码,返回给客户端。

所以,将character_set_client设置成binary,就不存在宽字节或多字节的问题了,所有数据以二进制的形式传递,就能有效避免宽字符注入。

已经不能注入了

inconv导致的致命后果

测试代码

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
<?php
//连接数据库部分,注意使用了gbk编码
$conn = mysql_connect('localhost', 'root', 'toor!@#$') or die('bad!');
mysql_query("SET NAMES 'gbk'");
mysql_select_db('test', $conn) OR emMsg("连接数据库失败,未找到您填写的数据库");
//执行sql语句
mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary", $conn);
$id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
$id = iconv('utf-8', 'gbk', $id);
$sql = "SELECT * FROM news WHERE tid='{$id}'";
$result = mysql_query($sql, $conn) or die(mysql_error());
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="gbk" />
<title>新闻</title>
</head>
<body>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>

可以看到,它在sql语句执行前,将character_set_client设置成了binary,所以可以避免宽字符注入的问题。但之后其调用了iconv将已经过滤过的参数$id给转换了一下。

给id参数一个值:錦’

报错了。说明可以注入。

“錦“这个字,它的utf-8编码是0xe98ca6,它的gbk编码是0xe55c。
\的ascii码正是5c。那么,当我们的錦被iconv从utf-8转换成gbk后,变成了%e5%5c,而后面的’被addslashes变成了%5c%27,这样组合起来就是%e5%5c%5c%27,两个%5c就是\,正好把反斜杠转义了,导致’逃逸出单引号,产生注入。

正利用了绕过addslashes的两种方式的第一种:将\转义掉。

Reference

浅析白盒审计中的字符编码及SQL注入