0x00 简介
前一段时间一直想解决一下字符编码问题,从而研究了Python (Python2.7版本)、PHP、MYSQL等在字符编码方面的原理,从根本上理解编码问题产生的根源,以及一些有效的解决办法。本文打算从两大部分出发分别介绍各个语言在编码上的处理机制,以及由编码问题造成的漏洞。总的来说字符编码问题可以归结为以下几点
1、以错误的方式解析字符编码
2、在编码转化时被转化的编码没有相应的值
3、在编码转化时没有相应的值与被转化的编码对应
简单介绍几种方式
UTF-8
UTF-8是一种变长字节编码方式。对于某一个字符的UTF-8编码,如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的位数,其余各字节均以10开头。UTF-8最多可用到6个字节。如表:
1字节 0xxxxxxx
2字节 110xxxxx 10xxxxxx
3字节 1110xxxx 10xxxxxx 10xxxxxx
4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
GBK
专门为解决汉字的编码而生成的解决方案。GBK的中文编码是双字节来表示的,英文用单字节表示,但GBK编码表中也有英文字符的双字节表示形式,所以英文字母可以有2种GBK表示方式。为区分中文,将其最高位都定成1。英文单字节最高位都为0。
0x01 Python&PHP&MYSQL编码问题
整体分为三大部分,从存储、解析到显示。python里string object和unicode object是两种不同的类型,string里的character是有多种编码方式的,比如单字节的ASCII,双字节的GB2312等等,再比如UTF-8。很明显要想解读string,必需知道string里的character是用哪种编码方式,然后才能进行。
Python文档编码
Python解析器从文本中解析代码,需要知道文档的编码方式一般在文档开头
# coding:utf-8# -*- coding:utf-8 -*-
例如:
# encoding:utf-8print "한"
此时文档应当按照utf-8编码存储,因为Python 解析器会以utf-8的格式读取存储在磁盘上的二进制,如果存储格式有误就会出现解析错误。例如下面报错信息
SyntaxError: Non-ASCII character '\xe5' in file F:\code\python\2.py on line 4, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
Python 解析器默认以ascii进行解析,'\xe5'超出ascii编码范围,因此解析错误。
另一种改变python 解析器的默认解析编码为使用UTF-8-BOM进行存储。
不用使用头部注释即可解析中文字符编码,利用hexdump工具可以看出其中的文件头有三个标志字符ef bb bf
至此Python加载文件的编码操作已经结束,下面就是在Python解析器中的操作,可以说是在内存中的操作。
Python解析器编码
由前面的介绍可以了解到Python中的字符类型分为string 和 unicode,其中string 又包括了gbk、utf-8等各种编码。他们之间的转化通过encode和decode进行
因为终端使用的是gbk编码,所以 啊的编码为'\xb0\xa1' 以gbk编码格式进行解析,解析成unicode编码,再使用encode编码方式编码成utf-8。在这里需要再次强调一下Python string的默认编码方式 ascii形式,也就是说在不指定string编码方式的情况下默认以ascii进行解析,因此会造成很多编码问题。比如下面两个例子,正好将encode和decode函数都有讲解到
Python 写入字符串到文件
这里只针对py2,write方法的参数类型是str,str是二进制流(不包含编码信息),当给出一个unicode对象时,会执行str函数转换成str类型再送给write方法。unicode转str包含一次编码,如不指定则默认使用ascii编。看下面的错误
这里产生错误的原因是,unicode首先要encode转化为ascii编码,显然有很多字节是对不上号的,这里的解决方法有两个,其一是改变其默认编码使用下面代码
其二将unicode转换为其他编码过后再次存入文件
绕过unicode直接进行编码转化
首先看下下面代码
a原本为gbk编码,如果直接进行encode那么首先会转换编码为unicode格式,因为默认解析格式为ascii所以在ascii转换成unicode编码时会产生以上错误。那么这时可以改变默认编码
成功解决该类编码问题
终端显示编码
在第二小节的图示中最后一个环节,是终端编码的问题。问题一般出在交给操作系统的字符编码与终端显示编码有所差别。在windows cmd终端采用的gbk编码,如果使用utf-8编码就会出现乱码
在linux 终端下采用的是utf-8编码形式![enter description here](./attachments/Little CMS.md "Little CMS")如果使用gbk编码就会出现乱码
PHP
加载
PHP编码相对 Python而言简单了许多,PHP直接使用ascii进行单字节解析,也就是说不论文件采用什么编码,PHP在解析是总是按字节处理,如下面例子:
该PHP脚本采用gbk编码方式编码,"誠" 字的字节码为"D55c" 5c为\使得PHP解析错误。
显示
PHP以及HTML脚本可以指定显示在浏览器上的编码方式
header("content-type:text/html;charset=utf-8")
MYSQL
mysql 编码数据比较复杂,大体上可以分为以下几个
1、character_set_client
2、character_set_connection
3、character_set_results
4、数据字段存储
character_set_client
无论客户端传递的是什么编码的数据,服务器都当成该编码来处理,例如该编码为UTF8,那么如果客户端发送过来的数据不是UTF8,那么就会出现乱码;
character_set_connection
connection 可以说是在SQL语句执行时的编码 ,当执行的是查询语句时,客户端发送过来的数据会先转换成connection指定的编码。但只要客户端发送过来的数据与client指定的编码一致,那么转换就不会出现问题;
数据字段存储
当insert数据时需要将数据存储在数据库中,这里就涉及到编码转化,当没用找到对应关系时就会令该位置字符为3F ,这也是数据库中乱码中3F出现的原因。
character_set_results
当select从数据库中取出时以前的编码要变为character_set_results设置的编码。
整个流程可参照下图:
从查询到取出整个流程经历了三次编码转换,每次转换都有可能产生编码问题。client编码指定了用户输入的编码格式,connection按照此编码格式将编码转化为connection指定的编码,然后再将编码转化为存储格式。当有查询操作时再将取出的数据按照results格式转化。这里盗用一张图
验证原理
设计以下实验
实验一 (验证数据存储时的转换)
写一个数据库交互的代码,在前端使用GBK编码方式插入数据,character_set_client设置为GBK,character_set_connection设置为GBK,数据存储设置为utf8,查看最后的存储字节为E8AAA0为正确的utf8编码
http://127.0.0.1/1.php?a=%d5%5c$conn->query("SET character_set_client = gbk"); $conn->query("SET character_set_connection = gbk"); ALTER TABLE `yz` CHANGE `a` `a` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
实验二(验证client到connection的转换)
在前端使用GBK编码方式插入数据,character_set_client设置为GBK,character_set_connection设置为utf8,数据存储设置为utf8,查看最后的存储字节为E8AAA0为正确的uft8编码
http://127.0.0.1/1.php?a=%d5%5c$conn->query("SET character_set_client = gbk"); $conn->query("SET character_set_connection = utf8"); ALTER TABLE `yz` CHANGE `a` `a` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
实验三(验证数据存储到取出的显示的转换)
将以utf8编码存储的内容用gbk格式取出
http://127.0.0.1/1.php?a=%d5%5c$conn->query("SET character_set_client = gbk"); $conn->query("SET character_set_connection = utf8"); $conn->query("SET character_set_results = gbk"); ALTER TABLE `yz` CHANGE `a` `a` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
成功转化为gbk格式,没有乱码出现enter code here
0x02 漏洞产生
GBK编码新问题
让我们回顾一下GBK宽字节注入的相关细节在可以使用单引号的前提下,使用addslashes将单引号转义,又因为database使用的是GBK编码,所以如果前面有%d5等字节存在,就会将反斜杠吃掉变为%d5%5c在gbk编码里面这是一个合法字符所以只剩下单引号,成功逃逸。
在研究编码问题的时候发现PHP是按照字节处理的所以如果对于%d5%5c这样的gbk编码如果使用addslashes的话会变成%d5%5c%5c,这样在SQL server处理的时候如果以gbk格式进行数据查询就会 多出一个反斜杠 ,利用该反斜杠可以转移其他特殊字符,具体使用方法如下:
源码链接
$conn->query("SET NAMES 'gbk'"); sql_get("select * from yz where b='$a' and c='$b'");
如果$a和$b两个参数分别传入
a=%d5%5c&b=or 1 %23
经过编码解析成为
select * from yz where b='誠\' and c='or 1 #'
成功进行注入
0x03 总结
系统的总结了在以前学习过程中遇到的编码问题,学习了编码问题产生的原因以及纠错方法,同时在研究过程中找到了新的漏洞所在。那么总结来看编码问题还是三大点
1、以错误的方式解析字符编码
2、在编码转化时被转化的编码没有相应的值
3、在编码转化时没有相应的值与被转化的编码对应
上图中A是UTF8编码,B是GBK编码,简单来讲上述问题可以这么描述
1、A字符串用GBK编码进行解析,必然会产生编码错误
2、现在A要转化为B种编码,但在解析A时默认编码为ascii,从而出现解析错误
3、现在A要转化为B种编码,以UTF-8格式解析A,但在转化成GBK编码时发现没有对应的字符编码,这里就出现了转换错误
简单的总结到这里,如果后面遇到了其他问题会及时补充,如果描述有错请及时指正。
网友评论